packages feed

plexus-synapse-0.3.0.0: src/Synapse/CLI/Support.hs

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

-- | CLI representability checks for IR types
--
-- Determines whether types from the IR can be represented as CLI flags.
-- This enables:
--
-- 1. Showing appropriate help (simple flags vs JSON input required)
-- 2. Tab completion for types that can be enumerated
-- 3. Validation before attempting CLI invocation
--
-- = Representability Rules
--
-- A type is CLI-representable if it can be expressed as --flag value pairs:
--
-- - Primitives: string, integer, number, boolean (always representable)
-- - UUID/formatted strings: representable as strings
-- - Optional T: representable if T is representable (flag is omittable)
-- - Arrays: representable via repeated flags (--key val1 --key val2 --key val3)
-- - Structs: representable if all required fields are representable
-- - Enums (discriminated): representable via --field.type variant --field.x ...
-- - String enums: representable (finite set of string values)
-- - Any/Unknown: NOT representable (need JSON input)
--
-- = Example
--
-- @
-- data SupportLevel
--   = FullSupport        -- All params work as CLI flags
--   | PartialSupport     -- Some params need JSON
--   | NoSupport          -- Method requires JSON input
-- @
module Synapse.CLI.Support
  ( -- * Support Levels
    SupportLevel(..)
  , SupportReason(..)

    -- * Method Support
  , methodSupport
  , methodSupportDetails

    -- * Type Checks
  , canCLIRepresent
  , canCLIRepresentVariant
  , canCLIRepresentField

    -- * Utilities
  , unsupportedParams
  , requiredJsonParams
  ) where

import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Maybe (mapMaybe)
import Data.Text (Text)
import qualified Data.Text as T

import Synapse.IR.Types

-- ============================================================================
-- Support Levels
-- ============================================================================

-- | Level of CLI support for a method
data SupportLevel
  = FullSupport
    -- ^ All parameters can be expressed as CLI flags
  | PartialSupport [Text]
    -- ^ Some parameters require JSON input (listed by name)
  | NoSupport [Text]
    -- ^ Method requires JSON input for critical parameters
  deriving stock (Show, Eq)

-- | Reason why a type is not CLI representable
data SupportReason
  = ReasonArray
    -- ^ Arrays need JSON or special handling
  | ReasonNested
    -- ^ Deeply nested structure
  | ReasonAny
    -- ^ Type is dynamic (any)
  | ReasonUnknown
    -- ^ Type information missing
  | ReasonComplexUnion
    -- ^ Union with non-representable variants
  deriving stock (Show, Eq)

-- ============================================================================
-- Method Support
-- ============================================================================

-- | Check the CLI support level for a method
methodSupport :: IR -> MethodDef -> SupportLevel
methodSupport ir method =
  let unsupported = unsupportedParams ir method
      required = filter (isRequired method) unsupported
  in case (null unsupported, null required) of
    (True, _)     -> FullSupport
    (False, True) -> PartialSupport unsupported
    (False, False) -> NoSupport required
  where
    isRequired m name = any (\p -> pdName p == name && pdRequired p) (mdParams m)

-- | Get detailed support information for a method
methodSupportDetails :: IR -> MethodDef -> [(Text, Either SupportReason ())]
methodSupportDetails ir method =
  [ (pdName p, checkParam p)
  | p <- mdParams method
  ]
  where
    checkParam p = case canCLIRepresentWithReason ir (pdType p) of
      Left reason -> Left reason
      Right () -> Right ()

-- ============================================================================
-- Type Representability
-- ============================================================================

-- | Check if a type can be represented as CLI flags
canCLIRepresent :: IR -> TypeRef -> Bool
canCLIRepresent ir typeRef = case canCLIRepresentWithReason ir typeRef of
  Right () -> True
  Left _ -> False

-- | Check representability with reason
canCLIRepresentWithReason :: IR -> TypeRef -> Either SupportReason ()
canCLIRepresentWithReason ir = \case
  -- Primitives are always representable
  RefPrimitive _ _ -> Right ()

  -- Optional types: representable if inner type is
  RefOptional inner -> canCLIRepresentWithReason ir inner

  -- Arrays are representable via repeated flags if the element type is representable
  RefArray innerType -> canCLIRepresentWithReason ir innerType

  -- Any/Unknown require JSON
  RefAny -> Left ReasonAny
  RefUnknown -> Left ReasonUnknown

  -- Named types: look up and check
  RefNamed qn -> canCLIRepresentNamed ir (qualifiedNameFull qn)

-- | Check if a named type is CLI representable
canCLIRepresentNamed :: IR -> Text -> Either SupportReason ()
canCLIRepresentNamed ir name =
  case Map.lookup name (irTypes ir) of
    Nothing -> Left ReasonUnknown
    Just typeDef -> canCLIRepresentTypeDef ir typeDef

-- | Check if a type definition is CLI representable
canCLIRepresentTypeDef :: IR -> TypeDef -> Either SupportReason ()
canCLIRepresentTypeDef ir TypeDef{..} = case tdKind of
  -- Primitives are representable
  KindPrimitive _ _ -> Right ()

  -- Alias: check the target
  KindAlias target -> canCLIRepresentWithReason ir target

  -- Structs: all fields must be representable
  KindStruct fields ->
    let checks = map (canCLIRepresentField ir) fields
    in case filter isLeftE checks of
      [] -> Right ()
      _ -> Left ReasonNested

  -- Enums: check based on discriminator type
  KindEnum discriminator variants
    -- String enums (discriminator is "value") are representable
    | discriminator == "value" -> Right ()
    -- Discriminated unions: all variants must be representable
    | otherwise ->
        let checks = map (canCLIRepresentVariantWithReason ir) variants
        in case filter isLeftE checks of
          [] -> Right ()
          _ -> Left ReasonComplexUnion

  -- String enums are representable (simple string literals)
  KindStringEnum _ -> Right ()

isLeftE :: Either a b -> Bool
isLeftE (Left _) = True
isLeftE (Right _) = False

-- | Check if a variant is CLI representable
canCLIRepresentVariant :: IR -> VariantDef -> Bool
canCLIRepresentVariant ir VariantDef{..} =
  all (canCLIRepresentField' ir) vdFields

-- | Check if a variant is representable (with reason)
canCLIRepresentVariantWithReason :: IR -> VariantDef -> Either SupportReason ()
canCLIRepresentVariantWithReason ir VariantDef{..} =
  let checks = map (canCLIRepresentField ir) vdFields
  in case filter isLeftE checks of
    [] -> Right ()
    _ -> Left ReasonNested

-- | Check if a field is CLI representable
canCLIRepresentField :: IR -> FieldDef -> Either SupportReason ()
canCLIRepresentField ir FieldDef{..} =
  canCLIRepresentWithReason ir fdType

-- | Check if a field is representable (Bool version)
canCLIRepresentField' :: IR -> FieldDef -> Bool
canCLIRepresentField' ir field = case canCLIRepresentField ir field of
  Right () -> True
  Left _ -> False

-- ============================================================================
-- Utilities
-- ============================================================================

-- | Get list of parameters that cannot be represented as CLI flags
unsupportedParams :: IR -> MethodDef -> [Text]
unsupportedParams ir method =
  [ pdName p
  | p <- mdParams method
  , not (canCLIRepresent ir (pdType p))
  ]

-- | Get parameters that require JSON input (non-representable + required)
requiredJsonParams :: IR -> MethodDef -> [Text]
requiredJsonParams ir method =
  [ pdName p
  | p <- mdParams method
  , pdRequired p
  , not (canCLIRepresent ir (pdType p))
  ]

-- ============================================================================
-- Depth Checking (for recursive/deeply nested types)
-- ============================================================================

-- | Maximum nesting depth for CLI representability
maxNestingDepth :: Int
maxNestingDepth = 3

-- | Check if a type is representable within a depth limit
canCLIRepresentWithDepth :: IR -> Int -> TypeRef -> Bool
canCLIRepresentWithDepth _ depth _
  | depth <= 0 = False
canCLIRepresentWithDepth ir depth typeRef = case typeRef of
  RefPrimitive _ _ -> True
  RefOptional inner -> canCLIRepresentWithDepth ir depth inner
  RefArray innerType -> canCLIRepresentWithDepth ir depth innerType
  RefAny -> False
  RefUnknown -> False
  RefNamed qn -> canCLIRepresentNamedWithDepth ir (depth - 1) (qualifiedNameFull qn)

-- | Check named type with depth
canCLIRepresentNamedWithDepth :: IR -> Int -> Text -> Bool
canCLIRepresentNamedWithDepth ir depth name
  | depth <= 0 = False
  | otherwise = case Map.lookup name (irTypes ir) of
      Nothing -> False
      Just TypeDef{..} -> case tdKind of
        KindPrimitive _ _ -> True
        KindAlias target -> canCLIRepresentWithDepth ir depth target
        KindStruct fields -> all (canCLIRepresentFieldWithDepth ir (depth - 1)) fields
        KindEnum "value" _ -> True  -- String enum
        KindEnum _ variants -> all (canCLIRepresentVariantWithDepth ir (depth - 1)) variants

-- | Check field with depth
canCLIRepresentFieldWithDepth :: IR -> Int -> FieldDef -> Bool
canCLIRepresentFieldWithDepth ir depth FieldDef{..} =
  canCLIRepresentWithDepth ir depth fdType

-- | Check variant with depth
canCLIRepresentVariantWithDepth :: IR -> Int -> VariantDef -> Bool
canCLIRepresentVariantWithDepth ir depth VariantDef{..} =
  all (canCLIRepresentFieldWithDepth ir depth) vdFields