plexus-synapse-0.3.0.0: src/Synapse/IR/Types.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Intermediate Representation for code generation
--
-- The IR is a deduplicated, structured representation of the full
-- plugin graph, optimized for transpilers to consume.
--
-- = Design
--
-- Schema (per-plugin, self-contained, may duplicate types)
-- ↓ walk + transform
-- IR (global, deduplicated, compiler-ready)
-- ↓ transpilers
-- TypeScript / Python / Swift / etc.
--
-- = Structure
--
-- - Types are hoisted to top level and deduplicated by name
-- - Methods reference types by name, not inline definitions
-- - Streaming is inferred from return type structure
-- - Discriminated unions are first-class
module Synapse.IR.Types
( -- * Top-level IR
IR(..)
, emptyIR
, mergeIR
-- * Version Information
, synapseVersion
-- * Generation Metadata
, GeneratorInfo(..)
, GenerationMetadata(..)
-- * Plugin Hash Information
, PluginHashInfo(..)
-- * Type Definitions
, TypeDef(..)
, tdFullName
, TypeKind(..)
, FieldDef(..)
, VariantDef(..)
-- * Method Definitions
, MethodDef(..)
, ParamDef(..)
-- * Type References
, QualifiedName(..)
, qualifiedNameFull
, TypeRef(..)
, typeRefName
-- * Utilities
, isStreaming
, inferStreaming
) where
import Control.Applicative ((<|>))
import Data.Aeson
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import qualified Data.Text as T
import GHC.Generics (Generic)
-- ============================================================================
-- Version Information
-- ============================================================================
-- | Synapse version (from cabal file: plexus-synapse.cabal)
synapseVersion :: Text
synapseVersion = "0.2.0.0"
-- ============================================================================
-- Generation Metadata
-- ============================================================================
-- | Generator tool version information
data GeneratorInfo = GeneratorInfo
{ giTool :: Text -- ^ Tool name (e.g., "synapse", "synapse-cc")
, giVersion :: Text -- ^ Version string (e.g., "0.2.0.0")
}
deriving stock (Show, Eq, Generic)
deriving anyclass (ToJSON, FromJSON)
-- | Generation metadata tracking the full toolchain
data GenerationMetadata = GenerationMetadata
{ gmGenerators :: [GeneratorInfo] -- ^ All tools in the generation chain
, gmTimestamp :: Text -- ^ ISO 8601 timestamp of generation
, gmIrVersion :: Text -- ^ IR format version
}
deriving stock (Show, Eq, Generic)
deriving anyclass (ToJSON, FromJSON)
-- ============================================================================
-- Plugin Hash Information
-- ============================================================================
-- | V2 hash information for a plugin
-- Supports granular cache invalidation by tracking:
-- - Composite hash (backward compatible with V1)
-- - Self hash (methods-only, for detecting method changes)
-- - Children hash (children-only, for detecting dependency changes)
data PluginHashInfo = PluginHashInfo
{ phiHash :: Text -- ^ Composite hash (backward compatible)
, phiSelfHash :: Text -- ^ V2: Methods-only hash
, phiChildrenHash :: Text -- ^ V2: Children-only hash
}
deriving stock (Show, Eq, Generic)
deriving anyclass (ToJSON, FromJSON)
-- ============================================================================
-- Top-level IR
-- ============================================================================
-- | The complete IR for code generation
data IR = IR
{ irVersion :: Text -- ^ IR format version
, irBackend :: Text -- ^ Backend name (e.g., "substrate", "plexus")
, irHash :: Maybe Text -- ^ Plexus hash for versioning
, irMetadata :: Maybe GenerationMetadata -- ^ Generation toolchain metadata
, irTypes :: Map Text TypeDef -- ^ All types, deduplicated by name
, irMethods :: Map Text MethodDef -- ^ All methods, keyed by full path (e.g., "cone.chat")
, irPlugins :: Map Text [Text] -- ^ Plugin -> method names mapping
, irPluginHashes :: Maybe (Map Text PluginHashInfo) -- ^ V2: Hash information per plugin
}
deriving stock (Show, Eq, Generic)
deriving anyclass (ToJSON)
-- | Empty IR for folding
emptyIR :: IR
emptyIR = IR
{ irVersion = "2.0" -- Bumped: TypeRef now uses structured QualifiedName
, irBackend = "" -- Will be set by buildIR
, irHash = Nothing
, irMetadata = Nothing
, irTypes = Map.empty
, irMethods = Map.empty
, irPlugins = Map.empty
, irPluginHashes = Nothing
}
-- | Merge two IRs (for combining results from tree walk)
mergeIR :: IR -> IR -> IR
mergeIR a b = IR
{ irVersion = irVersion a
, irBackend = irBackend a -- Take from left (parent)
, irHash = irHash a <|> irHash b -- Take first available hash
, irMetadata = irMetadata a <|> irMetadata b -- Take first available metadata
, irTypes = Map.union (irTypes a) (irTypes b) -- Left-biased, first definition wins
, irMethods = Map.union (irMethods a) (irMethods b)
, irPlugins = Map.unionWith (++) (irPlugins a) (irPlugins b)
, irPluginHashes = case (irPluginHashes a, irPluginHashes b) of
(Just ha, Just hb) -> Just (Map.union ha hb) -- Merge hash maps
(Just ha, Nothing) -> Just ha
(Nothing, Just hb) -> Just hb
(Nothing, Nothing) -> Nothing
}
-- ============================================================================
-- Type Definitions
-- ============================================================================
-- | A type definition extracted from schemas
data TypeDef = TypeDef
{ tdName :: Text -- ^ Type name (e.g., "ListResult", "ChatEvent")
, tdNamespace :: Text -- ^ Namespace (e.g., "cone", "arbor")
, tdDescription :: Maybe Text -- ^ Documentation
, tdKind :: TypeKind -- ^ What kind of type this is
}
deriving stock (Show, Eq, Generic)
deriving anyclass (ToJSON)
-- | Compute the fully qualified type name
tdFullName :: TypeDef -> Text
tdFullName td = tdNamespace td <> "." <> tdName td
-- | The kind of a type definition
data TypeKind
= KindStruct
{ ksFields :: [FieldDef] -- ^ Struct fields
}
| KindEnum
{ keDiscriminator :: Text -- ^ Field that discriminates (e.g., "type")
, keVariants :: [VariantDef] -- ^ Possible variants
}
| KindStringEnum
{ kseValues :: [Text] -- ^ String literal values (e.g., ["pending", "completed"])
}
| KindAlias
{ kaTarget :: TypeRef -- ^ What this aliases to
}
| KindPrimitive
{ kpType :: Text -- ^ "string", "integer", "boolean", etc.
, kpFormat :: Maybe Text -- ^ Optional format hint (e.g., "uuid", "int64")
}
deriving stock (Show, Eq, Generic)
deriving anyclass (ToJSON)
-- | A field in a struct
data FieldDef = FieldDef
{ fdName :: Text -- ^ Field name
, fdType :: TypeRef -- ^ Field type
, fdDescription :: Maybe Text -- ^ Documentation
, fdRequired :: Bool -- ^ Is this field required?
, fdDefault :: Maybe Value -- ^ Default value if any
}
deriving stock (Show, Eq, Generic)
deriving anyclass (ToJSON)
-- | A variant in a discriminated union
data VariantDef = VariantDef
{ vdName :: Text -- ^ Variant name (the discriminator value)
, vdDescription :: Maybe Text -- ^ Documentation
, vdFields :: [FieldDef] -- ^ Fields specific to this variant
}
deriving stock (Show, Eq, Generic)
deriving anyclass (ToJSON)
-- ============================================================================
-- Method Definitions
-- ============================================================================
-- | A method definition
data MethodDef = MethodDef
{ mdName :: Text -- ^ Method name (e.g., "chat")
, mdFullPath :: Text -- ^ Full path (e.g., "cone.chat")
, mdNamespace :: Text -- ^ Parent namespace (e.g., "cone")
, mdDescription :: Maybe Text -- ^ Documentation
, mdStreaming :: Bool -- ^ Does this method stream multiple events?
, mdParams :: [ParamDef] -- ^ Input parameters
, mdReturns :: TypeRef -- ^ Return type reference
, mdBidirType :: Maybe TypeRef
-- ^ Bidirectional channel type parameter T, when the method uses
-- BidirChannel<StandardRequest<T>, StandardResponse<T>>.
--
-- Populated from the "bidirectional" / "request_type" fields in the
-- MethodSchema (emitted by the hub-macro when #[bidirectional] is set).
--
-- - Nothing → method is not bidirectional, OR uses the default
-- T = serde_json::Value (StandardBidirChannel)
-- - Just RefAny → bidirectional with T=Value (explicit marker)
-- - Just (RefNamed ...) → bidirectional with a specific named T type
--
-- NOTE: The substrate schema currently emits 'bidirectional: true' but
-- does not yet include a structured 'bidir_type' field. When that field
-- is added to MethodSchema, populate it here from methodRequestType.
-- For now this is always Nothing.
}
deriving stock (Show, Eq, Generic)
deriving anyclass (ToJSON)
-- | A parameter definition
data ParamDef = ParamDef
{ pdName :: Text -- ^ Parameter name
, pdType :: TypeRef -- ^ Parameter type
, pdDescription :: Maybe Text -- ^ Documentation
, pdRequired :: Bool -- ^ Is this required?
, pdDefault :: Maybe Value -- ^ Default value if any
}
deriving stock (Show, Eq, Generic)
deriving anyclass (ToJSON)
-- ============================================================================
-- Type References
-- ============================================================================
-- | A qualified name for a type, with namespace and local name
data QualifiedName = QualifiedName
{ qnNamespace :: Text -- ^ Namespace (can be empty for global types)
, qnLocalName :: Text -- ^ Local name within namespace
}
deriving stock (Show, Eq, Generic)
deriving anyclass (ToJSON)
-- | Get the full qualified name as a single Text
-- Returns just the local name if namespace is empty, otherwise "namespace.localName"
qualifiedNameFull :: QualifiedName -> Text
qualifiedNameFull qn
| T.null (qnNamespace qn) = qnLocalName qn
| otherwise = qnNamespace qn <> "." <> qnLocalName qn
-- | A reference to a type
data TypeRef
= RefNamed QualifiedName -- ^ Reference to a named type (e.g., QualifiedName "cone" "UUID")
| RefPrimitive Text (Maybe Text) -- ^ Primitive type with optional format
| RefArray TypeRef -- ^ Array of some type
| RefOptional TypeRef -- ^ Optional (nullable) type
| RefAny -- ^ Intentionally dynamic (serde_json::Value) - accepts any JSON
| RefUnknown -- ^ Unknown type (schema gap) - should warn
deriving stock (Show, Eq, Generic)
deriving anyclass (ToJSON)
-- | Get the name from a type reference (if it's a named ref)
typeRefName :: TypeRef -> Maybe Text
typeRefName (RefNamed qn) = Just (qualifiedNameFull qn)
typeRefName _ = Nothing
-- ============================================================================
-- Utilities
-- ============================================================================
-- | Check if a method definition is streaming
isStreaming :: MethodDef -> Bool
isStreaming = mdStreaming
-- | Infer streaming from a return type's structure
-- A method is streaming if its return type is an enum with more than 2 variants
-- (more than 1 success variant + error variant)
inferStreaming :: TypeDef -> Bool
inferStreaming td = case tdKind td of
KindEnum _ variants ->
let nonErrorVariants = filter (not . isErrorVariant) variants
in length nonErrorVariants > 1
_ -> False
where
isErrorVariant v = vdName v == "error"