agentic-0.2.1.0: test/ProjectionExample.hs
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
{-# OPTIONS_GHC -Wno-orphans #-}
-- | An independent adapter fixture. All storage conventions are local to this
-- module; Agentic owns neither the identifier nor this storage protocol.
module ProjectionExample where
import Agentic hiding (model)
import Agentic.Codec (CaseCodec (..), ObjectCodec (..))
import Agentic.Contract (Structure (..))
import Agentic.Schema (Field (..), Shape (..), Variant (..))
import Data.Fixed (E2, Fixed (..), showFixed)
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Generics (Generic)
import Text.Read (readMaybe)
import Type.Reflection
newtype FactId = FactId Text deriving (Eq, Show)
instance Contract FactId where
contract = scalar (ScalarInfo "example.FactId" []) (Just identifierCodec)
identifierCodec :: Codec FactId
identifierCodec = Codec (schemaOf (SString Nothing)) (\(FactId t) -> String t) $ \case
String t | not (T.null t) -> Right (FactId t)
_ -> Left "expected a nonempty identifier"
-- An adapter supplied once for a library type, not for every containing record.
instance Contract (Fixed E2) where
contract = scalar (ScalarInfo "Data.Fixed.Fixed" [("resolution", "100")]) (Just decimalCodec)
decimalCodec :: Codec (Fixed E2)
decimalCodec = Codec (schemaOf (SString Nothing)) (String . T.pack . showFixed False) $ \case
String t -> do
let (sign, unsigned) = case T.stripPrefix "-" t of Just u -> (-1, u); Nothing -> (1, t)
case T.splitOn "." unsigned of
[whole, frac] | digits whole && digits frac && T.length frac == 2 ->
case (readMaybe (T.unpack whole), readMaybe (T.unpack frac)) of
(Just w, Just f) ->
let result = MkFixed (sign * (w * 100 + f))
in if T.pack (showFixed False result) == t then Right result else Left "noncanonical decimal"
_ -> Left "invalid decimal"
_ -> Left "expected decimal text with two fractional digits"
_ -> Left "expected exact decimal text"
where
digits t = not (T.null t) && T.all (\c -> c >= '0' && c <= '9') t
data Sample = Sample {key :: FactId, amount :: Fixed E2, confidence :: Probability}
deriving (Generic, Contract, Eq, Show)
data Box a = Box {value :: a} deriving (Generic, Contract, Eq, Show)
data Payload = Empty | Amount (Fixed E2) | Confidence Probability
deriving (Generic, Contract, Eq, Show)
sample :: Sample
sample = Sample (FactId "sample-1") (MkFixed 1234) (fromBasisPoints 8500)
integerText :: Codec Integer
integerText = Codec (schemaOf (SString Nothing)) (String . T.pack . show) $ \case
String t | Just n <- readMaybe (T.unpack t), T.pack (show (n :: Integer)) == t -> Right n
_ -> Left "expected a canonical integer string"
probabilityStorage :: Codec Probability
probabilityStorage = Codec integerText.schema (integerText.encode . toInteger . basisPoints) $ \v -> do
n <- integerText.decode v
if n < 0 || n > 10000
then Left "basis points must be between 0 and 10000"
else Right (fromBasisPoints (fromInteger n))
-- | An independent projection for upstream and downstream leaves. Named types,
-- mappings, validation and lists reuse the compiler's traversal.
-- Overrides are typed, and return the schema together with its matching codec.
storage :: Structure a -> Either ProjectionError (Codec a)
storage = compileWith storageRules
storageRules :: forall a. Structure a -> Maybe (Either ProjectionError (Codec a))
storageRules = \case
Scalar info modelDefault -> Just $ case () of
_
| Just HRefl <- eqTypeRep (typeRep @a) (typeRep @Probability) -> Right probabilityStorage
| Just HRefl <- eqTypeRep (typeRep @a) (typeRep @(Fixed E2)) -> Right decimalCodec
| Just HRefl <- eqTypeRep (typeRep @a) (typeRep @FactId) -> Right identifierCodec
| Just HRefl <- eqTypeRep (typeRep @a) (typeRep @Integer) -> Right integerText
| Just HRefl <- eqTypeRep (typeRep @a) (typeRep @Int) ->
Right $
Codec
integerText.schema
(integerText.encode . toInteger)
( \v -> do
n <- integerText.decode v
if n < toInteger (minBound :: Int) || n > toInteger (maxBound :: Int)
then Left "Int out of range"
else Right (fromInteger n)
)
| Just HRefl <- eqTypeRep (typeRep @a) (typeRep @Text) -> defaultCodec info modelDefault
| Just HRefl <- eqTypeRep (typeRep @a) (typeRep @Bool) -> defaultCodec info modelDefault
| otherwise -> Left (ProjectionError [] ("unsupported storage scalar " <> info.name))
Optional child -> Just $ do
c <- storage child
pure $
Codec
(schemaOf (SSum [Variant "None" Nothing [], Variant "Some" Nothing [Field "value" c.schema True]]))
(\case Nothing -> Object [("tag", String "None")]; Just x -> Object [("tag", String "Some"), ("value", c.encode x)])
( \case
Object [("tag", String "None")] -> Right Nothing
Object fields | lookup "tag" fields == Just (String "Some"), Just v <- lookup "value" fields -> Just <$> c.decode v
_ -> Left "expected a None/Some tagged optional"
)
Record con fields -> Just $ do
fs <- compileFieldsWith storage fields
let tagged = con.style == PositionalFields && not (null fs.fields)
tagField = Field "tag" (schemaOf (SEnum [(con.name, Nothing)])) True
pure $
Codec
(schemaOf (SObject (if tagged then tagField : fs.fields else fs.fields)))
(\a -> Object ((if tagged then [("tag", String con.name)] else []) <> fs.encode a))
( \case
Object values | not tagged || lookup "tag" values == Just (String con.name) -> fs.decode values
_ -> Left "expected a record with its constructor tag"
)
Sum cases -> Just $ do
compiled <- traverse (compileCaseWith storage) cases
pure $
Codec
(schemaOf (SSum [Variant c.tag c.doc (if null c.fields then [] else [Field "payload" (schemaOf (SObject c.fields)) True]) | c <- compiled]))
( \a -> case [(c.tag, values) | c <- compiled, Just values <- [c.encode a]] of
(tag, values) : _ -> Object ([("tag", String tag)] <> if null values then [] else [("payload", Object values)])
[] -> Null
)
( \case
Object values | Just (String tag) <- lookup "tag" values ->
case [c | c <- compiled, c.tag == tag] of
c : _
| null c.fields -> c.decode []
| Just (Object payload) <- lookup "payload" values -> c.decode payload
_ -> Left "unknown constructor or missing payload"
_ -> Left "expected a tagged sum"
)
Unit -> Just $ Right $ Codec (schemaOf (SObject [])) (const (Object [])) $ \case
Object [] -> Right ()
_ -> Left "expected an empty object"
_ -> Nothing
where
defaultCodec info = maybe (Left (ProjectionError [] ("unsupported storage scalar " <> info.name))) Right