json-spec-elm-0.1.0.1: src/Data/JsonSpec/Elm.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
module Data.JsonSpec.Elm (
elmDefs,
Definitions,
HasType(..),
) where
import Bound (Scope(Scope), Var(B, F), toScope)
import Control.Monad.Writer (MonadWriter(tell), Writer, execWriter)
import Data.JsonSpec (Specification(JsonArray, JsonBool, JsonDateTime,
JsonEither, JsonInt, JsonLet, JsonNullable, JsonNum, JsonObject,
JsonRef, JsonString, JsonTag))
import Data.Proxy (Proxy(Proxy))
import Data.Set (Set)
import Data.String (IsString(fromString))
import Data.Text (Text)
import Data.Void (Void, absurd)
import GHC.TypeLits (ErrorMessage((:$$:), (:<>:)), KnownSymbol, Symbol,
TypeError, symbolVal)
import Language.Elm.Definition (Definition)
import Language.Elm.Expression ((|>), Expression, bind, if_)
import Language.Elm.Name (Constructor, Qualified)
import Language.Elm.Type (Type)
import Prelude (Applicative(pure), Foldable(foldl), Functor(fmap),
Maybe(Just, Nothing), Monad((>>)), Semigroup((<>)), Show(show), ($),
(++), (.), (<$>), Int, error, reverse, zip)
import qualified Data.Char as Char
import qualified Data.Set as Set
import qualified Data.Text as Text
import qualified GHC.TypeLits as Lits
import qualified Language.Elm.Definition as Def
import qualified Language.Elm.Expression as Expr
import qualified Language.Elm.Name as Name
import qualified Language.Elm.Pattern as Pat
import qualified Language.Elm.Type as Type
elmDefs
:: forall spec. (HasType spec)
=> Proxy (spec :: Specification)
-> Set Definition
elmDefs _ =
execWriter $ typeOf @spec >> decoderOf @spec
class Record (spec :: [(Symbol, Specification)]) where
recordDefs :: forall v. Definitions [(Name.Field, Type v)]
recordEncoders :: Definitions [(Text, Name.Field, Expression Void)]
instance Record '[] where
recordDefs = pure []
recordEncoders = pure []
instance
( HasType spec
, KnownSymbol name
, Record more
)
=>
Record ( '(name, spec) : more )
where
recordDefs = do
type_ <- typeOf @spec
moreFields <- recordDefs @more
pure $ (fieldName @name, type_) : moreFields
recordEncoders = do
encoder <- encoderOf @spec
moreFields <- recordEncoders @more
pure $ (sym @name, fieldName @name, encoder) : moreFields
class HasType (spec :: Specification) where
typeOf :: forall v. Definitions (Type v)
decoderOf :: Definitions (Expression Void)
encoderOf :: Definitions (Expression Void)
instance HasType JsonString where
typeOf = pure "String.String"
decoderOf = pure "Json.Decode.string"
encoderOf = pure "Json.Encode.string"
instance HasType JsonNum where
typeOf = pure "Basics.Float"
decoderOf = pure "Json.Decode.float"
encoderOf = pure "Json.Encode.float"
instance HasType JsonInt where
typeOf = pure "Basics.Int"
decoderOf = pure "Json.Decode.int"
encoderOf = pure "Json.Encode.int"
instance {- HasType (JsonObject fields) -}
( Record fields
, BaseFields (Reverse fields)
, Lambda (LambdaDepth (Reverse fields))
, Decoders fields
)
=>
HasType (JsonObject fields)
where
typeOf = Type.Record <$> recordDefs @fields
decoderOf = do
decoders <- fieldDecoders @fields
pure $
foldl
(\expr (_, decoder) ->
expr |> ("Json.Decode.andThen" `Expr.App`
Expr.Lam (toScope (
"Json.Decode.map"
`Expr.App` Expr.Var (B ())
`Expr.App` bind Expr.Global absurd decoder
))
)
)
("Json.Decode.succeed" `Expr.App` lambda)
decoders
where
lambda =
lam . Expr.Record . reverse $
[ (name, var)
| (name, var) <- baseFields @(Reverse fields)
]
encoderOf = do
fields <- recordEncoders @fields
pure $
Expr.Lam . toScope $
"Json.Encode.object"
`Expr.App`
Expr.List
[ Expr.apps "Basics.," [
Expr.String jsonField,
Expr.bind Expr.Global absurd encoder `Expr.App`
(Expr.Proj elmField `Expr.App` Expr.Var var)
]
| (jsonField, elmField, encoder) <- fields
]
where
var :: Bound.Var () a
var = B ()
instance (HasType spec) => HasType (JsonArray spec) where
typeOf = do
elemType <- typeOf @spec
pure $ Type.App "Basics.List" elemType
decoderOf = do
dec <- decoderOf @spec
pure $ Expr.App "Json.Decode.list" dec
encoderOf = do
encoder <- encoderOf @spec
pure $ "Json.Encode.list" `Expr.App` encoder
instance HasType JsonBool where
typeOf = pure "Basics.Bool"
decoderOf = pure "Json.Decode.bool"
encoderOf =
pure "Json.Encode.bool"
instance (HasType spec) => HasType (JsonNullable spec) where
typeOf = do
type_ <- typeOf @spec
pure $ Type.App "Maybe.Maybe" type_
decoderOf = do
dec <- decoderOf @spec
pure $ Expr.App "Json.Decode.nullable" dec
encoderOf = do
encoder <- encoderOf @spec
pure $
Expr.Lam . toScope $
Expr.apps
"Maybe.withDefault"
[ "Json.Decode.null"
, Expr.apps
"Maybe.map"
[ Expr.bind Expr.Global absurd encoder
, Expr.Var (B ())
]
]
instance (KnownSymbol const) => HasType (JsonTag const) where
typeOf = pure "Basics.()"
decoderOf =
pure $
"Json.Decode.string"
|> Expr.apps "Json.Decode.andThen"
[ Expr.Lam . toScope $
if_
(
Expr.apps
"Basics.=="
[ Expr.Var (B ())
, Expr.String (sym @const)
]
)
(Expr.App "Json.Decode.succeed" "Basics.()")
(Expr.App "Json.Decode.fail" (Expr.String "Tag mismatch"))
]
encoderOf =
pure $
"Basics.always" `Expr.App`
("Json.Encode.string" `Expr.App` Expr.String (sym @const))
instance HasType JsonDateTime where
typeOf = pure "Time.Posix"
decoderOf = pure "Iso8601.decoder"
encoderOf = pure "Iso8601.encode"
instance (KnownSymbol name) => HasType (JsonRef name) where
typeOf =
pure
. Type.Global
. localName
$ sym @name
decoderOf =
pure . Expr.Global $ decoderName @name
encoderOf =
pure . Expr.Global $ encoderName @name
instance (HasType spec) => HasType (JsonLet '[] spec) where
typeOf = typeOf @spec
decoderOf = decoderOf @spec
encoderOf = encoderOf @spec
instance {- HasType (JsonLet ( def : more ) spec) -}
( ElmDef def
, HasType (JsonLet more spec)
)
=>
HasType (JsonLet ( def : more ) spec)
where
typeOf = do
defs @def
typeOf @(JsonLet more spec)
decoderOf = do
defs @def
decoderOf @(JsonLet more spec)
encoderOf = do
defs @def
encoderOf @(JsonLet more spec)
instance {- HasType (JsonEither left right) -}
( TypeError
( Lits.Text "Elm doesn't support anonymous sum types, so if you "
:<>: Lits.Text "want to use (possibly nested) `JsonEither` "
:<>: Lits.Text "you must give it a name using `JsonLet`, e.g:"
:$$: Lits.Text ""
:$$: Lits.Text "> JsonLet"
:$$: Lits.Text "> '[ '( \"MySum\""
:$$: Lits.Text "> , JsonEither"
:$$: Lits.Text "> ( JsonEither"
:$$: Lits.Text "> JsonInt"
:$$: Lits.Text "> JsonString"
:$$: Lits.Text "> )"
:$$: Lits.Text "> ( JsonEither"
:$$: Lits.Text "> JsonFloat"
:$$: Lits.Text "> JsonBool"
:$$: Lits.Text "> )"
:$$: Lits.Text "> )"
:$$: Lits.Text "> ]"
:$$: Lits.Text "> (JsonRef \"MySum\")"
:$$: Lits.Text ""
:$$: Lits.Text "This will produce the Elm type"
:$$: Lits.Text ""
:$$: Lits.Text "> type MySum"
:$$: Lits.Text "> = MySum_1 Int"
:$$: Lits.Text "> | MySum_2 String"
:$$: Lits.Text "> | MySum_3 Float"
:$$: Lits.Text "> | MySum_4 Bool"
:$$: Lits.Text ""
)
)
=>
HasType (JsonEither left right)
where
typeOf = error "undefinable"
decoderOf = error "undefinable"
encoderOf = error "undefinable"
type family LambdaDepth (record :: [k]) where
LambdaDepth '[] = Void
LambdaDepth (a : more) =
Bound.Var () (LambdaDepth more)
class Lambda depth where
lam :: Expression depth -> Expression Void
instance {-# OVERLAPS #-} Lambda (Bound.Var () Void) where
lam e = Expr.Lam (toScope e)
instance (Lambda deeper) => Lambda (Bound.Var () deeper) where
lam e = lam (Expr.Lam (toScope e))
class BaseFields (record :: [(Symbol, Specification)]) where
baseFields :: [(Name.Field, Expression (LambdaDepth record))]
instance BaseFields '[] where
baseFields = []
instance {- BaseFields ('(name, spec) : more) -}
(BaseFields more, KnownSymbol name)
=>
BaseFields ('(name, spec) : more)
where
baseFields =
(fieldName @name, Expr.Var (B ())) :
[ (name, b var)
| (name, var) <- baseFields @more
]
where
b = bind Expr.Global (Expr.Var . F)
type family Reverse (l :: [k]) where
Reverse '[] = '[]
Reverse (a : more) = Concat (Reverse more) '[a]
type family Concat (a :: [k]) (b :: [k]) where
Concat '[] b = b
Concat (a : more) b =
a : Concat more b
class Decoders (spec :: [(Symbol, Specification)]) where
fieldDecoders :: Definitions [(Text, Expression Void)]
instance Decoders '[] where
fieldDecoders = pure []
instance {- Decoders ('(name, spec) : more) -}
(HasType spec, Decoders more, KnownSymbol name)
=>
Decoders ('(name, spec) : more)
where
fieldDecoders = do
dec <- decoderOf @spec
more <- fieldDecoders @more
pure $ ( sym @name , dec) : more
class ElmDef (def :: (Symbol, Specification)) where
defs :: Definitions ()
instance {-# OVERLAPS #-}
( KnownSymbol name
, SumDef (JsonEither left right)
)
=>
ElmDef '(name, JsonEither left right)
where
defs = do
branches <- sumDef @(JsonEither left right)
let
constructors :: [(Constructor, [Scope Int Type Void])]
constructors =
[ ( Name.Constructor (constructorName n)
, [Scope type_]
)
| (n, type_) <- zip [(1 :: Int) ..] branches
]
decoders <- sumDecoders @(JsonEither left right)
encoders <- sumEncoders @(JsonEither left right)
tell . Set.fromList $
[ Def.Type (localName name) 0 constructors
, Def.Constant
(decoderName @name)
0
( Scope
( "Json.Decode.Decoder"
`Type.App`
Type.Global (localName name)
)
)
(
"Json.Decode.oneOf"
`Expr.App`
Expr.List
[ "Json.Decode.map"
`Expr.App` Expr.Global (localName (constructorName n))
`Expr.App` dec
| (n, dec) <- zip [(1 :: Int) ..] decoders
]
)
, Def.Constant
(encoderName @name)
0
(
toScope $
Type.Fun
(Type.Global (localName name))
"Json.Encode.Value"
)
(
Expr.Lam . toScope $
Expr.Case
(Expr.Var (B ()))
[ ( Pat.Con (localName (constructorName n)) [Pat.Var 0]
, toScope $
fmap absurd encoder `Expr.App`
Expr.Var (B (0 :: Int))
)
| (n, encoder) <- zip [1..] encoders
]
)
]
where
constructorName :: Int -> Text
constructorName n = name <> "_" <> showt n
name :: Text
name = sym @name
instance (HasType spec, KnownSymbol name) => ElmDef '(name, spec) where
defs = do
type_ <- typeOf @spec
dec <- decoderOf @spec
enc <- encoderOf @spec
tell . Set.fromList $
[ Def.Alias
(localName (sym @name))
0
(Scope type_)
, Def.Constant
(decoderName @name)
0
( Scope
( Type.App
"Json.Decode.Decoder"
(Type.Global $ localName (sym @name))
)
)
dec
, Def.Constant
(encoderName @name)
0
( Scope
( Type.Fun
(Type.Global $ localName (sym @name))
"Json.Encode.Value"
)
)
enc
]
class SumDef (spec :: Specification) where
sumDef :: forall v. Definitions [Type v]
sumDecoders :: Definitions [Expression Void]
sumEncoders :: Definitions [Expression Void]
instance {-# OVERLAPS #-}
(SumDef (JsonEither a b), SumDef (JsonEither c d))
=>
SumDef (JsonEither (JsonEither a b) (JsonEither c d))
where
sumDef = do
left <- sumDef @(JsonEither a b)
right <- sumDef @(JsonEither c d)
pure $ left ++ right
sumDecoders = do
left <- sumDecoders @(JsonEither a b)
right <- sumDecoders @(JsonEither c d)
pure (left ++ right)
sumEncoders = do
left <- sumEncoders @(JsonEither a b)
right <- sumEncoders @(JsonEither c d)
pure (left ++ right)
instance {-# OVERLAPS #-}
(SumDef (JsonEither a b), HasType right)
=>
SumDef (JsonEither (JsonEither a b) right)
where
sumDef = do
left <- sumDef @(JsonEither a b)
right <- typeOf @right
pure $ left ++ [right]
sumDecoders = do
left <- sumDecoders @(JsonEither a b)
right <- decoderOf @right
pure $ left ++ [right]
sumEncoders = do
left <- sumEncoders @(JsonEither a b)
right <- encoderOf @right
pure $ left ++ [right]
instance {-# OVERLAPS #-}
(SumDef (JsonEither c d), HasType left)
=>
SumDef (JsonEither left (JsonEither c d))
where
sumDef = do
left <- typeOf @left
right <- sumDef @(JsonEither c d)
pure $ left : right
sumDecoders = do
left <- decoderOf @left
right <- sumDecoders @(JsonEither c d)
pure $ left : right
sumEncoders = do
left <- encoderOf @left
right <- sumEncoders @(JsonEither c d)
pure $ left : right
instance
(HasType left, HasType right)
=>
SumDef (JsonEither left right)
where
sumDef = do
left <- typeOf @left
right <- typeOf @right
pure [left, right]
sumDecoders = do
left <- decoderOf @left
right <- decoderOf @right
pure [left, right]
sumEncoders = do
left <- encoderOf @left
right <- encoderOf @right
pure [left, right]
localName :: Text -> Qualified
localName =
Name.Qualified ["Api", "Data"]
type Definitions = Writer (Set Definition)
sym :: forall a b. (KnownSymbol a, IsString b) => b
sym = fromString $ symbolVal (Proxy @a)
showt :: (Show a, IsString b) => a -> b
showt = fromString . show
lower :: Text -> Text
lower txt =
case Text.uncons txt of
Nothing -> txt
Just (c, more) -> Text.cons (Char.toLower c) more
decoderName :: forall name. (KnownSymbol name) => Qualified
decoderName = localName (lower (sym @name) <> "Decoder")
encoderName :: forall name. (KnownSymbol name) => Qualified
encoderName = localName (lower (sym @name) <> "Encoder")
fieldName :: forall name. (KnownSymbol name) => Name.Field
fieldName =
Name.Field $
case sym @name of
"type" -> "type_"
other -> Text.replace "-" "_" other