{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -fprint-explicit-kinds #-}
module Data.Schematic.Schema where
import Control.Applicative ((<|>))
import Control.Monad
import Data.Aeson as J
import Data.Aeson.Types as J
import Data.HashMap.Strict as H
import Data.Kind
import Data.Maybe
import Data.Schematic.Instances ()
import Data.Schematic.Generator
import Data.Scientific
import Data.Singletons.Prelude.List hiding (All, Union)
import Data.Singletons.TH
import Data.Singletons.TypeLits
import Data.Text as T
import Data.Union
import Data.Vector as V
import Data.Vinyl hiding (Dict)
import qualified Data.Vinyl.TypeLevel as V
import GHC.Exts
import GHC.Generics (Generic)
import GHC.TypeLits
(SomeNat(..), SomeSymbol(..), someNatVal, someSymbolVal)
import Prelude as P
import Test.SmallCheck.Series
type family CRepr (s :: Schema) :: Type where
CRepr ('SchemaText cs) = TextConstraint
CRepr ('SchemaNumber cs) = NumberConstraint
CRepr ('SchemaObject fs) = (String, Schema)
CRepr ('SchemaArray ar s) = ArrayConstraint
data TextConstraint
= TEq Nat
| TLt Nat
| TLe Nat
| TGt Nat
| TGe Nat
| TRegex Symbol
| TEnum [Symbol]
deriving (Generic)
instance SingKind TextConstraint where
type Demote TextConstraint = DemotedTextConstraint
fromSing = \case
STEq n -> withKnownNat n (DTEq . fromIntegral $ natVal n)
STLt n -> withKnownNat n (DTLt . fromIntegral $ natVal n)
STLe n -> withKnownNat n (DTLe . fromIntegral $ natVal n)
STGt n -> withKnownNat n (DTGt . fromIntegral $ natVal n)
STGe n -> withKnownNat n (DTGe . fromIntegral $ natVal n)
STRegex s -> withKnownSymbol s (DTRegex $ T.pack $ symbolVal s)
STEnum s -> let
d :: Sing (s :: [Symbol]) -> [Text]
d SNil = []
d (SCons ss@SSym fs) = T.pack (symbolVal ss) : d fs
in DTEnum $ d s
toSing = \case
DTEq n -> case someNatVal n of
Just (SomeNat (_ :: Proxy n)) -> SomeSing (STEq (SNat :: Sing n))
Nothing -> error "Negative singleton nat"
DTLt n -> case someNatVal n of
Just (SomeNat (_ :: Proxy n)) -> SomeSing (STLt (SNat :: Sing n))
Nothing -> error "Negative singleton nat"
DTLe n -> case someNatVal n of
Just (SomeNat (_ :: Proxy n)) -> SomeSing (STLe (SNat :: Sing n))
Nothing -> error "Negative singleton nat"
DTGt n -> case someNatVal n of
Just (SomeNat (_ :: Proxy n)) -> SomeSing (STGt (SNat :: Sing n))
Nothing -> error "Negative singleton nat"
DTGe n -> case someNatVal n of
Just (SomeNat (_ :: Proxy n)) -> SomeSing (STGe (SNat :: Sing n))
Nothing -> error "Negative singleton nat"
DTRegex s -> case someSymbolVal (T.unpack s) of
SomeSymbol (_ :: Proxy n) -> SomeSing (STRegex (SSym :: Sing n))
DTEnum ss -> case toSing ss of
SomeSing l -> SomeSing (STEnum l)
data DemotedTextConstraint
= DTEq Integer
| DTLt Integer
| DTLe Integer
| DTGt Integer
| DTGe Integer
| DTRegex Text
| DTEnum [Text]
deriving (Generic, Eq, Show)
data instance Sing (tc :: TextConstraint) where
STEq :: Sing n -> Sing ('TEq n)
STLt :: Sing n -> Sing ('TLt n)
STLe :: Sing n -> Sing ('TLe n)
STGt :: Sing n -> Sing ('TGt n)
STGe :: Sing n -> Sing ('TGe n)
STRegex :: Sing s -> Sing ('TRegex s)
STEnum :: Sing ss -> Sing ('TEnum ss)
instance (KnownNat n) => SingI ('TEq n) where sing = STEq sing
instance (KnownNat n) => SingI ('TGt n) where sing = STGt sing
instance (KnownNat n) => SingI ('TGe n) where sing = STGe sing
instance (KnownNat n) => SingI ('TLt n) where sing = STLt sing
instance (KnownNat n) => SingI ('TLe n) where sing = STLe sing
instance (KnownSymbol s, SingI s) => SingI ('TRegex s) where sing = STRegex sing
instance (SingI ss) => SingI ('TEnum ss) where sing = STEnum sing
instance Eq (Sing ('TEq n)) where _ == _ = True
instance Eq (Sing ('TLt n)) where _ == _ = True
instance Eq (Sing ('TLe n)) where _ == _ = True
instance Eq (Sing ('TGt n)) where _ == _ = True
instance Eq (Sing ('TGe n)) where _ == _ = True
instance Eq (Sing ('TRegex t)) where _ == _ = True
instance Eq (Sing ('TEnum ss)) where _ == _ = True
data NumberConstraint
= NLe Nat
| NLt Nat
| NGt Nat
| NGe Nat
| NEq Nat
deriving (Generic)
data DemotedNumberConstraint
= DNLe Integer
| DNLt Integer
| DNGt Integer
| DNGe Integer
| DNEq Integer
deriving (Generic, Eq, Show)
data instance Sing (nc :: NumberConstraint) where
SNEq :: Sing n -> Sing ('NEq n)
SNGt :: Sing n -> Sing ('NGt n)
SNGe :: Sing n -> Sing ('NGe n)
SNLt :: Sing n -> Sing ('NLt n)
SNLe :: Sing n -> Sing ('NLe n)
instance KnownNat n => SingI ('NEq n) where sing = SNEq sing
instance KnownNat n => SingI ('NGt n) where sing = SNGt sing
instance KnownNat n => SingI ('NGe n) where sing = SNGe sing
instance KnownNat n => SingI ('NLt n) where sing = SNLt sing
instance KnownNat n => SingI ('NLe n) where sing = SNLe sing
instance Eq (Sing ('NEq n)) where _ == _ = True
instance Eq (Sing ('NLt n)) where _ == _ = True
instance Eq (Sing ('NLe n)) where _ == _ = True
instance Eq (Sing ('NGt n)) where _ == _ = True
instance Eq (Sing ('NGe n)) where _ == _ = True
instance SingKind NumberConstraint where
type Demote NumberConstraint = DemotedNumberConstraint
fromSing = \case
SNEq n -> withKnownNat n (DNEq . fromIntegral $ natVal n)
SNGt n -> withKnownNat n (DNGt . fromIntegral $ natVal n)
SNGe n -> withKnownNat n (DNGe . fromIntegral $ natVal n)
SNLt n -> withKnownNat n (DNLt . fromIntegral $ natVal n)
SNLe n -> withKnownNat n (DNLe . fromIntegral $ natVal n)
toSing = \case
DNEq n -> case someNatVal n of
Just (SomeNat (_ :: Proxy n)) -> SomeSing (SNEq (SNat :: Sing n))
Nothing -> error "Negative singleton nat"
DNGt n -> case someNatVal n of
Just (SomeNat (_ :: Proxy n)) -> SomeSing (SNGt (SNat :: Sing n))
Nothing -> error "Negative singleton nat"
DNGe n -> case someNatVal n of
Just (SomeNat (_ :: Proxy n)) -> SomeSing (SNGe (SNat :: Sing n))
Nothing -> error "Negative singleton nat"
DNLt n -> case someNatVal n of
Just (SomeNat (_ :: Proxy n)) -> SomeSing (SNLt (SNat :: Sing n))
Nothing -> error "Negative singleton nat"
DNLe n -> case someNatVal n of
Just (SomeNat (_ :: Proxy n)) -> SomeSing (SNLe (SNat :: Sing n))
Nothing -> error "Negative singleton nat"
data ArrayConstraint
= AEq Nat
deriving (Generic)
data DemotedArrayConstraint
= DAEq Integer
deriving (Generic, Eq, Show)
data instance Sing (ac :: ArrayConstraint) where
SAEq :: Sing n -> Sing ('AEq n)
instance KnownNat n => SingI ('AEq n) where sing = SAEq sing
instance Eq (Sing ('AEq n)) where _ == _ = True
instance SingKind ArrayConstraint where
type Demote ArrayConstraint = DemotedArrayConstraint
fromSing = \case
SAEq n -> withKnownNat n (DAEq . fromIntegral $ natVal n)
toSing = \case
DAEq n -> case someNatVal n of
Just (SomeNat (_ :: Proxy n)) -> SomeSing (SAEq (SNat :: Sing n))
Nothing -> error "Negative singleton nat"
data Schema
= SchemaText [TextConstraint]
| SchemaBoolean
| SchemaNumber [NumberConstraint]
| SchemaObject [(Symbol, Schema)]
| SchemaArray [ArrayConstraint] Schema
| SchemaNull
| SchemaOptional Schema
| SchemaUnion [Schema]
deriving (Generic)
data DemotedSchema
= DSchemaText [DemotedTextConstraint]
| DSchemaNumber [DemotedNumberConstraint]
| DSchemaBoolean
| DSchemaObject [(Text, DemotedSchema)]
| DSchemaArray [DemotedArrayConstraint] DemotedSchema
| DSchemaNull
| DSchemaOptional DemotedSchema
| DSchemaUnion [DemotedSchema]
deriving (Generic, Eq, Show)
data instance Sing (schema :: Schema) where
SSchemaText :: Sing tcs -> Sing ('SchemaText tcs)
SSchemaNumber :: Sing ncs -> Sing ('SchemaNumber ncs)
SSchemaBoolean :: Sing 'SchemaBoolean
SSchemaArray :: Sing acs -> Sing schema -> Sing ('SchemaArray acs schema)
SSchemaObject :: Sing fields -> Sing ('SchemaObject fields)
SSchemaOptional :: Sing s -> Sing ('SchemaOptional s)
SSchemaNull :: Sing 'SchemaNull
SSchemaUnion :: Sing ss -> Sing ('SchemaUnion ss)
instance SingI sl => SingI ('SchemaText sl) where
sing = SSchemaText sing
instance SingI sl => SingI ('SchemaNumber sl) where
sing = SSchemaNumber sing
instance SingI 'SchemaNull where
sing = SSchemaNull
instance SingI 'SchemaBoolean where
sing = SSchemaBoolean
instance (SingI ac, SingI s) => SingI ('SchemaArray ac s) where
sing = SSchemaArray sing sing
instance SingI stl => SingI ('SchemaObject stl) where
sing = SSchemaObject sing
instance SingI s => SingI ('SchemaOptional s) where
sing = SSchemaOptional sing
instance SingI s => SingI ('SchemaUnion s) where
sing = SSchemaUnion sing
instance Eq (Sing ('SchemaText cs)) where _ == _ = True
instance Eq (Sing ('SchemaNumber cs)) where _ == _ = True
instance Eq (Sing 'SchemaNull) where _ == _ = True
instance Eq (Sing 'SchemaBoolean) where _ == _ = True
instance Eq (Sing ('SchemaArray as s)) where _ == _ = True
instance Eq (Sing ('SchemaObject cs)) where _ == _ = True
instance Eq (Sing ('SchemaOptional s)) where _ == _ = True
instance Eq (Sing ('SchemaUnion s)) where _ == _ = True
instance SingKind Schema where
type Demote Schema = DemotedSchema
fromSing = \case
SSchemaText cs -> DSchemaText $ fromSing cs
SSchemaNumber cs -> DSchemaNumber $ fromSing cs
SSchemaBoolean -> DSchemaBoolean
SSchemaArray cs s -> DSchemaArray (fromSing cs) (fromSing s)
SSchemaOptional s -> DSchemaOptional $ fromSing s
SSchemaNull -> DSchemaNull
SSchemaObject cs -> DSchemaObject $ fromSing cs
SSchemaUnion ss -> DSchemaUnion $ fromSing ss
toSing = \case
DSchemaText cs -> case toSing cs of
SomeSing scs -> SomeSing $ SSchemaText scs
DSchemaNumber cs -> case toSing cs of
SomeSing scs -> SomeSing $ SSchemaNumber scs
DSchemaBoolean -> SomeSing $ SSchemaBoolean
DSchemaArray cs sch -> case (toSing cs, toSing sch) of
(SomeSing scs, SomeSing ssch) -> SomeSing $ SSchemaArray scs ssch
DSchemaOptional sch -> case toSing sch of
SomeSing ssch -> SomeSing $ SSchemaOptional ssch
DSchemaNull -> SomeSing SSchemaNull
DSchemaObject cs -> case toSing cs of
SomeSing scs -> SomeSing $ SSchemaObject scs
DSchemaUnion ss -> case toSing ss of
SomeSing sss -> SomeSing $ SSchemaUnion sss
data FieldRepr :: (Symbol, Schema) -> Type where
FieldRepr
:: (SingI schema, KnownSymbol name)
=> JsonRepr schema
-> FieldRepr '(name, schema)
toJsonRepr :: FieldRepr '(fn, sch) -> JsonRepr sch
toJsonRepr (FieldRepr x) = x
knownFieldName
:: forall proxy (fieldName :: Symbol) schema
. KnownSymbol fieldName
=> proxy '(fieldName, schema)
-> Text
knownFieldName _ = T.pack $ symbolVal (Proxy @fieldName)
knownFieldSchema
:: forall proxy fieldName schema
. SingI schema
=> proxy '(fieldName, schema)
-> Sing schema
knownFieldSchema _ = sing
deriving instance Show (JsonRepr schema) => Show (FieldRepr '(name, schema))
instance Eq (JsonRepr schema) => Eq (FieldRepr '(name, schema)) where
FieldRepr a == FieldRepr b = a == b
instance
( KnownSymbol name
, SingI schema
, Serial m (JsonRepr schema) )
=> Serial m (FieldRepr '(name, schema)) where
series = FieldRepr <$> series
type family USubsets (u :: [k]) :: Constraint where
USubsets '[] = ()
USubsets (h ': tl) = (USubset tl (h ': tl) (V.RImage tl (h ': tl)), USubsets tl)
data JsonRepr :: Schema -> Type where
ReprText :: Text -> JsonRepr ('SchemaText cs)
ReprNumber :: Scientific -> JsonRepr ('SchemaNumber cs)
ReprBoolean :: Bool -> JsonRepr 'SchemaBoolean
ReprNull :: JsonRepr 'SchemaNull
ReprArray :: V.Vector (JsonRepr s) -> JsonRepr ('SchemaArray cs s)
ReprObject :: Rec FieldRepr fs -> JsonRepr ('SchemaObject fs)
ReprOptional :: Maybe (JsonRepr s) -> JsonRepr ('SchemaOptional s)
ReprUnion :: Union JsonRepr (h ': tl) -> JsonRepr ('SchemaUnion (h ': tl))
instance (Monad m, Serial m Text, SingI cs)
=> Serial m (JsonRepr ('SchemaText cs)) where
series = decDepth $ fmap ReprText $ textSeries $ fromSing (sing :: Sing cs)
instance (Monad m, Serial m Scientific, SingI cs)
=> Serial m (JsonRepr ('SchemaNumber cs)) where
series = decDepth $ fmap ReprNumber
$ numberSeries $ fromSing (sing :: Sing cs)
instance Monad m => Serial m (JsonRepr 'SchemaNull) where
series = cons0 ReprNull
instance (Serial m (JsonRepr s), Serial m (V.Vector (JsonRepr s)), SingI cs)
=> Serial m (JsonRepr ('SchemaArray cs s)) where
series = decDepth $ fmap ReprArray
$ arraySeries $ fromSing (sing :: Sing cs)
instance (Serial m (JsonRepr s))
=> Serial m (JsonRepr ('SchemaOptional s)) where
series = cons1 ReprOptional
instance (Monad m, Serial m (Rec FieldRepr fs))
=> Serial m (JsonRepr ('SchemaObject fs)) where
series = cons1 ReprObject
instance Show (JsonRepr ('SchemaText cs)) where
show (ReprText t) = "ReprText " P.++ show t
instance Show (JsonRepr ('SchemaNumber cs)) where
show (ReprNumber n) = "ReprNumber " P.++ show n
instance Show (JsonRepr 'SchemaNull) where show _ = "ReprNull"
instance Show (JsonRepr s) => Show (JsonRepr ('SchemaArray acs s)) where
show (ReprArray v) = "ReprArray " P.++ show v
instance V.RecAll FieldRepr fs Show => Show (JsonRepr ('SchemaObject fs)) where
show (ReprObject fs) = "ReprObject " P.++ show fs
instance Show (JsonRepr s) => Show (JsonRepr ('SchemaOptional s)) where
show (ReprOptional s) = "ReprOptional " P.++ show s
instance Eq (Rec FieldRepr fs) => Eq (JsonRepr ('SchemaObject fs)) where
ReprObject a == ReprObject b = a == b
instance Eq (JsonRepr ('SchemaText cs)) where
ReprText a == ReprText b = a == b
instance Eq (JsonRepr ('SchemaNumber cs)) where
ReprNumber a == ReprNumber b = a == b
instance Eq (JsonRepr 'SchemaNull) where
ReprNull == ReprNull = True
instance Eq (JsonRepr s) => Eq (JsonRepr ('SchemaArray as s)) where
ReprArray a == ReprArray b = a == b
instance Eq (JsonRepr s) => Eq (JsonRepr ('SchemaOptional s)) where
ReprOptional a == ReprOptional b = a == b
instance Ord (Rec FieldRepr fs) => Ord (JsonRepr ('SchemaObject fs)) where
ReprObject a `compare` ReprObject b = a `compare` b
instance Ord (JsonRepr ('SchemaText cs)) where
ReprText a `compare` ReprText b = a `compare` b
instance Ord (JsonRepr ('SchemaNumber cs)) where
ReprNumber a `compare` ReprNumber b = a `compare` b
instance Ord (JsonRepr 'SchemaNull) where
compare _ _ = EQ
instance Ord (JsonRepr s) => Ord (JsonRepr ('SchemaArray as s)) where
ReprArray a `compare` ReprArray b = a `compare` b
instance Ord (JsonRepr s) => Ord (JsonRepr ('SchemaOptional s)) where
ReprOptional a `compare` ReprOptional b = a `compare` b
instance IsList (JsonRepr ('SchemaArray cs s)) where
type Item (JsonRepr ('SchemaArray cs s)) = JsonRepr s
fromList = ReprArray . GHC.Exts.fromList
toList (ReprArray v) = GHC.Exts.toList v
instance Num (JsonRepr ('SchemaNumber cs)) where
ReprNumber a + ReprNumber b = ReprNumber $ a + b
ReprNumber a - ReprNumber b = ReprNumber $ a - b
ReprNumber a * ReprNumber b = ReprNumber $ a * b
abs (ReprNumber a) = ReprNumber $ abs a
signum (ReprNumber a) = ReprNumber $ signum a
fromInteger = ReprNumber . fromIntegral
instance IsString (JsonRepr ('SchemaText cs)) where
fromString = ReprText . fromString
fromOptional
:: SingI s
=> Sing ('SchemaOptional s)
-> J.Value
-> Parser (Maybe (JsonRepr s))
fromOptional _ = parseJSON
parseUnion
:: FromJSON (JsonRepr ('SchemaUnion ss))
=> sing (ss :: [Schema])
-> Value
-> Parser (JsonRepr ('SchemaUnion ss))
parseUnion _ val = parseJSON val
instance FromJSON (Union JsonRepr '[]) where
parseJSON = fail "empty union"
instance (SingI a, FromJSON (Union JsonRepr as)) => FromJSON (Union JsonRepr (a ': as)) where
parseJSON val = (This <$> parseJSON val)
<|> (That <$> (parseJSON val :: Parser (Union JsonRepr as)))
instance ToJSON (Union JsonRepr as) where
toJSON (This fa) = toJSON fa
toJSON (That u) = toJSON u
instance SingI schema => J.FromJSON (JsonRepr schema) where
parseJSON value = case sing :: Sing schema of
SSchemaText _ -> withText "String" (pure . ReprText) value
SSchemaNumber _ -> withScientific "Number" (pure . ReprNumber) value
SSchemaBoolean -> ReprBoolean <$> parseJSON value
SSchemaNull -> case value of
J.Null -> pure ReprNull
_ -> typeMismatch "Null" value
so@(SSchemaOptional s) -> withSingI s $ ReprOptional <$> fromOptional so value
SSchemaArray sa sb -> withSingI sa $ withSingI sb
$ withArray "Array" (fmap ReprArray . traverse parseJSON) value
SSchemaObject fs -> do
let
demoteFields :: SList s -> H.HashMap Text J.Value -> Parser (Rec FieldRepr s)
demoteFields SNil _ = pure RNil
demoteFields (SCons (STuple2 (n :: Sing fn) s) tl) h = withKnownSymbol n $ do
let fieldName = T.pack $ symbolVal (Proxy @fn)
fieldRepr <- case s of
SSchemaText so -> case H.lookup fieldName h of
Just v -> withSingI so $ FieldRepr <$> parseJSON v
Nothing -> fail $ "No text field: " P.++ show fieldName
SSchemaNumber so -> case H.lookup fieldName h of
Just v -> withSingI so $ FieldRepr <$> parseJSON v
Nothing -> fail $ "No number field: " P.++ show fieldName
SSchemaBoolean -> case H.lookup fieldName h of
Just v -> FieldRepr <$> parseJSON v
Nothing -> fail $ "No boolean field: " P.++ show fieldName
SSchemaNull -> case H.lookup fieldName h of
Just v -> FieldRepr <$> parseJSON v
Nothing -> fail $ "No null field: " P.++ show fieldName
SSchemaArray sa sb -> case H.lookup fieldName h of
Just v -> withSingI sa $ withSingI sb $ FieldRepr <$> parseJSON v
Nothing -> fail $ "No array field: " P.++ show fieldName
SSchemaObject so -> case H.lookup fieldName h of
Just v -> withSingI so $ FieldRepr <$> parseJSON v
Nothing -> fail $ "No object field" P.++ show fieldName
SSchemaOptional so -> case H.lookup fieldName h of
Just v -> withSingI so $ FieldRepr <$> parseJSON v
Nothing -> withSingI so $ pure $ FieldRepr $ ReprOptional Nothing
SSchemaUnion ss -> withSingI ss $ FieldRepr <$> parseUnion ss value
(fieldRepr :&) <$> demoteFields tl h
ReprObject <$> withObject "Object" (demoteFields fs) value
SSchemaUnion ss -> parseUnion ss value
instance J.ToJSON (JsonRepr a) where
toJSON ReprNull = J.Null
toJSON (ReprBoolean b) = J.Bool b
toJSON (ReprText t) = J.String t
toJSON (ReprNumber n) = J.Number n
toJSON (ReprOptional s) = case s of
Just v -> toJSON v
Nothing -> J.Null
toJSON (ReprArray v) = J.Array $ toJSON <$> v
toJSON (ReprObject r) = J.Object . H.fromList . fold $ r
where
extract :: forall name s . (KnownSymbol name)
=> FieldRepr '(name, s)
-> (Text, Value)
extract (FieldRepr s) = (T.pack $ symbolVal $ Proxy @name, toJSON s)
fold :: Rec FieldRepr fs -> [(Text, J.Value)]
fold = \case
RNil -> []
fr@(FieldRepr _) :& tl -> (extract fr) : fold tl
toJSON (ReprUnion u) = toJSON u
class FalseConstraint a
type family TopLevel (schema :: Schema) :: Constraint where
TopLevel ('SchemaArray acs s) = ()
TopLevel ('SchemaObject o) = ()
TopLevel spec = 'True ~ 'False