json-spec-elm-0.6.0.0: src/Data/JsonSpec/Elm.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-|
This module provide a way to generate Elm types, encoders, and
decoders for [json-spec](https://hackage.haskell.org/package/json-spec)
[Specification](https://hackage.haskell.org/package/json-spec/docs/Data-JsonSpec.html#t:Specification)s
Generally you will probably want `elmDefs`, but sometimes you might
want to directly use the methods of `HasType`.
Since not every part of a 'Specification' may have a name, we can
generate encoders and decoders for anonymous Elm types like records,
as well as named Elm types and type aliases. This package figures out
how to name things given the following rules:
* If a name appears in a 'JsonLet' binding, then it gets a name in Elm as a
type or type alias.
* If a second 'JsonLet' binding, with exactly one definition, of the
form @JsonLet '[ '(name, def) ] (JsonRef name)@ appears as the RHS of
a 'JsonLet'binding, then that is interpreted as a constructor name,
and the generated Elm definition will be a regular type instead of a
type alias. See 'Named' for an easy shorthand way to spell @JsonLet '[
'(name, def) ] (JsonRef name)@
* For any 'Named' leaf of a tree of 'JsonEither's, the name is interpreted as
a data constructor name, otherwise a data constructor name is
auto-generated.
== Examples:
=== Type alias
The specification
> Named "MyType" JsonString
will produce the Elm type
> type alias MyType = String
=== Type with a constructor
The specification
> Named "MyType" (Named "MyDataConstructor" JsonString)
will produce the Elm type
> type MyType = MyDataConstructor String
=== Sum Type
Note that the /root/ of a tree of 'JsonEither's /must/ be named, because
Elm has no way to represent anonymous sum types.
The specification
> Named "MySumType"
> ( JsonEither '[
> Named "AnInt" JsonInt,
> JsonFloat, -- note the omitted name
> Named "AString" JsonString
> ])
will produce the Elm type
> type MySumType
> = AnInt Int
> | MySumType_2 Float -- auto-generated constructor name.
> | AString String
== Producing actual Elm code
This package gets you as far as having a collection of
'Definition's in hand, which come from the 'elm-syntax'
package. You will need to use the pretty printing
features of that package to actually produce code. See
https://hackage.haskell.org/package/elm-syntax/docs/Language-Elm-Pretty.html,
or you can look at the source code for the tests in this package.
-}
module Data.JsonSpec.Elm (
elmDefs,
Definitions,
HasType(..),
Named,
) where
import Bound (Scope(Scope), Var(B), abstract1, closed, toScope)
import Control.Monad.Writer
( MonadTrans(lift), MonadWriter(tell), Writer, execWriter
)
import Data.JsonSpec
( FieldSpec(Optional, Required)
, Specification
( JsonArray, JsonBool, JsonDateTime, JsonDict, 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, if_)
import Language.Elm.Name (Constructor, Qualified)
import Language.Elm.Type (Type)
import Prelude
( Applicative(pure), Bool(False, True), Foldable(foldl, foldr), Functor(fmap)
, Maybe(Just, Nothing), Monad((>>)), Semigroup((<>)), Show(show), ($), (++)
, (.), (<$>), Int, error, 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
{-|
Generate Elm type, encoder, and decoder 'Definition's for all /named/
types in a 'Specification'. Note that this will not produce any types,
decoders, or encoders for anonymous parts of the 'Specification',
since we wouldn't know what to names to give those things in Elm.
-}
elmDefs
:: forall spec. (HasType spec)
=> Proxy (spec :: Specification)
-> Set Definition
elmDefs _ =
execWriter $ typeOf @spec >> decoderOf @spec
{-| Describes how a field in a record should be encoded in Elm. -}
data FieldEncoding = FieldEncoding
{ required :: Bool
{-^
'True' if the fields presence is required in the JSON
object, 'False' if it is not (which implies that the
field is a @Maybe something@, though that information
is not tracked here.
-}
, jsonField :: Text {-^ The name of the encoded JSON field. -}
, elmField :: Name.Field {-^ The name of the Elm record field. -}
, encoderFun :: Expression Void
{-^
The Elm function which can decode field value. The
expression will be a lambda expression that accepts
an Elm value and produces Elm's representation of a
JSON value (i.e. @Json.Encode.Value@)
-}
}
{-| Describes how a field in a record should be decoded. -}
data FieldDecoding = FieldDecoding
{ jsonField :: Text {-^ The name of the decoded field in JSON -}
, decoder :: Expression Void
{-^
An Elm expression containing the decoder for the
field value. I.e. of the Elm type @Json.Decode.Decoder
something@.
-}
}
{-|
How to define, encode, and decode an Elm record from a list of JSON
object field specifications.
-}
class Record (spec :: [FieldSpec]) where
recordDefs :: forall v. Definitions [(Name.Field, Type v)]
recordEncoders :: Definitions [FieldEncoding]
recordDecoders :: Definitions [FieldDecoding]
instance Record '[] where
recordDefs = pure []
recordEncoders = pure []
recordDecoders = pure []
instance
( HasType spec
, KnownSymbol name
, Record more
)
=>
Record ( Required name spec : more )
where
recordDefs = do
type_ <- typeOf @spec
moreFields <- recordDefs @more
pure $ (fieldName (sym @name), type_) : moreFields
recordEncoders = do
encoder <- encoderOf @spec
moreFields <- recordEncoders @more
pure $
FieldEncoding
{ required = True
, jsonField = sym @name
, elmField = fieldName (sym @name)
, encoderFun = encoder
}
: moreFields
recordDecoders = do
dec <- decoderOf @spec
more <- recordDecoders @more
pure $
FieldDecoding
{ jsonField = sym @name
, decoder = "Json.Decode.field" `a` Expr.String (sym @name) `a` dec
}
: more
instance
( HasType spec
, KnownSymbol name
, Record more
)
=>
Record ( Optional name spec : more )
where
recordDefs = do
type_ <- ta "Maybe.Maybe" <$> typeOf @spec
moreFields <- recordDefs @more
pure $ (fieldName (sym @name), type_) : moreFields
recordEncoders = do
encoder <- encoderOf @spec
moreFields <- recordEncoders @more
pure $
FieldEncoding
{ required = False
, jsonField = sym @name
, elmField = fieldName (sym @name)
, encoderFun = encoder
}
: moreFields
recordDecoders = do
dec <- decoderOf @spec
more <- recordDecoders @more
pure $
FieldDecoding
{ jsonField = sym @name
, decoder =
"Json.Decode.maybe"
`a` ("Json.Decode.field" `a` Expr.String (sym @name) `a` dec)
}
: more
{-|
Translates 'Specification's into "anonymous" Elm types (where
"anonymous" really means the RHS of a definition, which could be truly
anonymous but might in fact be a reference to something previously named
'Definition').
-}
class HasType (spec :: Specification) where
{-|
Produce the anonymous Elm type for the spec, collecting any necessary
'Definition's along the way.
-}
typeOf :: forall v. Definitions (Type v)
{-|
Produce the Elm Decode for the spec, collecting any necessary
'Definition's along the way
-}
decoderOf :: Definitions (Expression Void)
{-|
Produce the Elm Encoder for the spec, collecting any necessary
'Definition's along the way.
-}
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 (Record fields) => HasType (JsonObject fields) where
typeOf = Type.Record <$> recordDefs @fields
decoderOf = do
decodings <- recordDecoders @fields
pure $
foldl
(\expr decoder ->
expr |>
(
"Json.Decode.andThen" `a`
lam (\var -> "Json.Decode.map" `a` var `a` (absurd <$> decoder))
)
)
("Json.Decode.succeed" `a` recordConstructor ((.jsonField) <$> decodings))
((.decoder) <$> decodings)
encoderOf = do
fields <- recordEncoders @fields
pure $
lam (\var ->
"Json.Encode.object" `a`
(
"List.filterMap"
`a` "Basics.identity"
`a` Expr.List
[ if required then
"Maybe.Just" `a`
Expr.apps "Basics.,"
[
Expr.String jsonField,
fmap absurd encoderFun `a`
(Expr.Proj elmField `a` var)
]
else
"Maybe.map"
`a` lam (\inner ->
Expr.apps "Basics.,"
[
Expr.String jsonField,
fmap absurd encoderFun `a` inner
]
)
`a` (Expr.Proj elmField `a` var)
| FieldEncoding {required, jsonField, elmField, encoderFun}
<- fields
]
)
)
instance (HasType spec) => HasType (JsonArray spec) where
typeOf = do
elemType <- typeOf @spec
pure $ "Basics.List" `ta` elemType
decoderOf = do
dec <- decoderOf @spec
pure $ "Json.Decode.list" `a` dec
encoderOf = do
encoder <- encoderOf @spec
pure $ "Json.Encode.list" `a` encoder
instance (HasType spec) => HasType (JsonDict spec) where
typeOf = do
elemType <- typeOf @spec
pure $ "Dict.Dict" `ta` "String.String" `ta` elemType
decoderOf = do
dec <- decoderOf @spec
pure $ "Json.Decode.dict" `a` dec
encoderOf = do
encoder <- encoderOf @spec
pure $ "Json.Encode.dict" `a` "Basics.identity" `a` 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 $ "Maybe.Maybe" `ta` type_
decoderOf = do
dec <- decoderOf @spec
pure $ a "Json.Decode.nullable" dec
encoderOf = do
encoder <- encoderOf @spec
pure $
Expr.Lam . toScope $
Expr.apps
"Maybe.withDefault"
[ "Json.Encode.null"
, Expr.apps
"Maybe.map"
[ fmap 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)
]
)
(a "Json.Decode.succeed" "Basics.()")
(a "Json.Decode.fail" (Expr.String "Tag mismatch"))
]
encoderOf =
pure $
"Basics.always" `a`
("Json.Encode.string" `a` 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) -}
( HasDef 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 branches) -}
(TypeError AnonSumTypeError)
=>
HasType (JsonEither branches)
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)
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 HasDef (def :: (Symbol, Specification)) where
defs :: Definitions ()
instance {- HasDef '(name, JsonEither branches) -}
( KnownSymbol name
, SumDef (JsonEither branches)
)
=>
HasDef '(name, JsonEither branches)
where
defs = do
branches_ <- sumDef @(JsonEither branches)
let
constructors :: [(Constructor, [Scope Int Type Void])]
constructors =
[ ( Name.Constructor (constructorName conName n)
, [Scope type_]
)
| (n, (conName, type_)) <- zip [1..] branches_
]
decoders <- sumDecoders @(JsonEither branches)
encoders <- sumEncoders @(JsonEither branches)
tell . Set.fromList $
[ Def.Type (localName name) 0 constructors
, Def.Constant
(decoderName @name)
0
(Scope ("Json.Decode.Decoder" `ta` Type.Global (localName name)))
(
"Json.Decode.oneOf"
`a`
Expr.List
[ "Json.Decode.map"
`a` Expr.Global (localName (constructorName conName n))
`a` dec
| (n, (conName, dec)) <- zip [1..] 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 conName n))
[Pat.Var 0]
, toScope $
fmap absurd encoder `a`
Expr.Var (B (0 :: Int))
)
| (n, (conName, encoder)) <- zip [1..] encoders
]
)
]
where
constructorName :: Maybe Text -> Int -> Text
constructorName = \cases
Nothing n -> name <> "_" <> showt n
(Just consName) _ -> consName
name :: Text
name = sym @name
instance {- HasDef '(name, Named consName spec) -}
( HasType spec
, KnownSymbol consName
, KnownSymbol name
)
=>
HasDef '(name, Named consName spec)
where
defs = do
typ <- typeOf @spec
dec <- decoderOf @spec
enc <- encoderOf @spec
tell . Set.fromList $
[ Def.Type (localName (sym @name)) 0
[ ( Name.Constructor (sym @consName)
, [ lift typ ]
)
]
, Def.Constant
(decoderName @name)
0
( Scope
(
"Json.Decode.Decoder" `ta`
Type.Global (localName (sym @name))
)
)
( "Json.Decode.map"
`a` Expr.Global (localName (sym @consName))
`a` dec
)
, Def.Constant
(encoderName @name)
0
( Scope
( Type.Fun
(Type.Global $ localName (sym @name))
"Json.Encode.Value"
)
)
( lam $ \var ->
Expr.Case
var
[ (Pat.Con
(localName (sym @consName))
[ Pat.Var 0 ]
, toScope $
(absurd <$> enc) `a` Expr.Var (B 0)
)
]
)
]
instance {- HasDef '(name, spec) -}
{-# overlaps #-} (HasType spec, KnownSymbol name)
=>
HasDef '(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
(
"Json.Decode.Decoder" `ta`
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 [(Maybe Text, Type v)]
sumDecoders :: Definitions [(Maybe Text, Expression Void)]
sumEncoders :: Definitions [(Maybe Text, Expression Void)]
instance SumDef (JsonEither '[]) where
sumDef = pure []
sumDecoders = pure []
sumEncoders = pure []
instance {- SumDef (JsonEither (a ': as)) -}
(SumDef a, SumDef (JsonEither as))
=>
SumDef (JsonEither (a ': as))
where
sumDef = do
aDef <- sumDef @a
asDef <- sumDef @(JsonEither as)
pure $ aDef ++ asDef
sumDecoders = do
aDec <- sumDecoders @a
asDec <- sumDecoders @(JsonEither as)
pure (aDec ++ asDec)
sumEncoders = do
aEnc <- sumEncoders @a
asEnc <- sumEncoders @(JsonEither as)
pure (aEnc ++ asEnc)
instance {- SumDef (JsonLet '[ '(name, def) ] (JsonRef name)) -}
( HasType def
, KnownSymbol name
)
=>
SumDef (JsonLet '[ '(name, def) ] (JsonRef name))
where
sumDef = do
typ <- typeOf @def
pure [(Just (sym @name), typ)]
sumDecoders = do
dec <- decoderOf @def
pure [(Just (sym @name), dec)]
sumEncoders = do
enc <- encoderOf @def
pure [(Just (sym @name), enc)]
instance {-# overlaps #-} (HasType a) => SumDef a where
sumDef = do
typ <- typeOf @a
pure [(Nothing, typ)]
sumDecoders = do
dec <- decoderOf @a
pure [(Nothing, dec)]
sumEncoders = do
enc <- encoderOf @a
pure [(Nothing, enc)]
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 :: Text -> Name.Field
fieldName specName =
Name.Field $
case specName of
"type" -> "type_"
other -> Text.replace "-" "_" other
a :: Expression v -> Expression v -> Expression v
a = Expr.App
ta :: Type v -> Type v -> Type v
ta = Type.App
recordConstructor :: [Text] -> Expression v
recordConstructor records =
case
closed $
foldr
(\field expr ->
Expr.Lam $ abstract1 field expr
)
unboundRecord
records
of
Nothing -> error "can't happen"
Just expr -> expr
where
unboundRecord :: Expression Text
unboundRecord =
Expr.Record
[ (fieldName field, Expr.Var field)
| field <- records
]
{-|
Produce lambda in Elm out of a haskell function.
> lam (\var ->
> "elmFunction" `a` var
> )
produces an Elm lambda expression of the form
> (\var -> elmFunction var)
-}
lam
:: (Expression (Var () a) -> Expression (Var () v))
-> Expression v
lam f =
Expr.Lam . toScope $ f (Expr.Var (B ()))
{-|
Helper for giving a specification a name. This is especially useful for
making sure sum type data constructors have meaningful names.
-}
type Named name def = JsonLet '[ '(name, def) ] (JsonRef name)
type AnonSumTypeError =
( 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 '[JsonInt, JsonString, JsonFloat, JsonBool]"
:$$: 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 ""
)