packages feed

plexus-synapse-0.3.0.0: examples/SchemaDiscovery.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

-- | Demonstrates the two-call schema discovery pattern
--
-- 1. Call substrate.schema → get list of activations
-- 2. For each activation, call substrate.activation_schema → get method schemas
--
-- The ActivationInfo structure drives both:
-- - CLI subcommand generation (arbor, cone, health)
-- - Schema enrichment requests (fetch enriched schema for each namespace)

module Main where

import Data.Aeson (Value, encode, toJSON)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import qualified Streaming.Prelude as S

import Plexus (connect, disconnect, defaultConfig)
import Substrate.Client (SubstrateConnection, substrateRpc)
import Plexus.Schema
  ( PlexusSchema(..)
  , PlexusSchemaEvent(..)
  , ActivationInfo(..)
  , EnrichedSchema(..)
  , ActivationSchemaEvent(..)
  , extractSchemaEvent
  , extractActivationSchemaEvent
  )

main :: IO ()
main = do
  putStrLn "=== Schema Discovery Demo ==="
  putStrLn ""

  conn <- connect defaultConfig

  -- STEP 1: Discover all activations
  putStrLn "Step 1: Calling substrate.schema to discover activations..."
  mSchema <- S.head_ $ S.mapMaybe extractSchemaEvent $
    substrateRpc conn "substrate.schema" (toJSON ([] :: [Value]))

  case mSchema of
    Nothing -> putStrLn "Failed to get schema"
    Just (SchemaError err) -> T.putStrLn $ "Schema error: " <> err
    Just (SchemaData schema) -> do
      putStrLn $ "Found " <> show (length (schemaActivations schema)) <> " activations"
      putStrLn ""

      -- For each activation, show what we'd do
      mapM_ (demonstrateActivation conn) (schemaActivations schema)

  disconnect conn

-- | Demonstrate how an ActivationInfo drives both CLI and schema requests
demonstrateActivation :: SubstrateConnection -> ActivationInfo -> IO ()
demonstrateActivation plexusConn act = do
  let ns = activationNamespace act
      methods = activationMethods act

  putStrLn $ "Activation: " <> T.unpack ns
  putStrLn $ "  Description: " <> T.unpack (activationDescription act)
  putStrLn $ "  Methods: " <> show (length methods)

  -- STEP 2: Use the namespace to request enriched schema
  putStrLn $ "  → Requesting enriched schema for '" <> T.unpack ns <> "'..."

  mEnriched <- S.head_ $ S.mapMaybe extractActivationSchemaEvent $
    substrateRpc plexusConn "substrate.activation_schema" (toJSON [ns])

  case mEnriched of
    Nothing -> putStrLn "    Failed to get enriched schema"
    Just (ActivationSchemaError err) ->
      T.putStrLn $ "    Schema error: " <> err
    Just (ActivationSchemaData enriched) -> do
      let variantCount = maybe 0 length (schemaOneOf enriched)
      putStrLn $ "    ✓ Got enriched schema with " <> show variantCount <> " method variants"

      -- Show the mapping: methods[i] corresponds to oneOf[i]
      putStrLn "    Mapping (index-based):"
      mapM_ (\(idx, method) ->
        putStrLn $ "      [" <> show idx <> "] " <> T.unpack method)
        (zip [0..] methods)

      putStrLn ""

      -- This demonstrates:
      -- 1. ActivationInfo.namespace → used for substrate.activation_schema RPC
      -- 2. ActivationInfo.methods → used to know which method corresponds to which oneOf variant
      -- 3. Same structure drives CLI: "symbols-dyn <namespace> <method>"

-- | The key insight: ActivationInfo is the source of truth for both
--
-- For CLI generation:
--   - activationNamespace → subcommand name ("arbor")
--   - activationMethods → sub-subcommand names ("tree-create", "tree-list")
--
-- For schema enrichment:
--   - activationNamespace → RPC parameter: substrate.activation_schema("arbor")
--   - activationMethods[i] → maps to enrichedSchema.oneOf[i]
--
-- This creates a direct correspondence:
--
--   CLI Command              | Schema Lookup
--   -------------------------|----------------------------------
--   arbor tree-create        | enriched["arbor"].oneOf[0]
--   arbor tree-get           | enriched["arbor"].oneOf[1]
--   arbor tree-list          | enriched["arbor"].oneOf[3]
--   cone list                | enriched["cone"].oneOf[1]
--
-- The mapping is guaranteed by the order in ActivationInfo.methods
-- matching the order in the enriched schema's oneOf array.