packages feed

json-spec-elm 0.1.0.1 → 0.2.0.0

raw patch · 4 files changed

+489/−200 lines, 4 filesdep +directorydep +processPVP ok

version bump matches the API change (PVP)

Dependencies added: directory, process

API changes (from Hackage documentation)

- Data.JsonSpec.Elm: instance (Data.JsonSpec.Elm.BaseFields more, GHC.TypeLits.KnownSymbol name) => Data.JsonSpec.Elm.BaseFields ('(name, spec) : more)
- Data.JsonSpec.Elm: instance (Data.JsonSpec.Elm.ElmDef def, Data.JsonSpec.Elm.HasType ('Data.JsonSpec.Spec.JsonLet more spec)) => Data.JsonSpec.Elm.HasType ('Data.JsonSpec.Spec.JsonLet (def : more) spec)
- Data.JsonSpec.Elm: instance (Data.JsonSpec.Elm.HasType left, Data.JsonSpec.Elm.HasType right) => Data.JsonSpec.Elm.SumDef ('Data.JsonSpec.Spec.JsonEither left right)
- Data.JsonSpec.Elm: instance (Data.JsonSpec.Elm.HasType spec, Data.JsonSpec.Elm.Decoders more, GHC.TypeLits.KnownSymbol name) => Data.JsonSpec.Elm.Decoders ('(name, spec) : more)
- Data.JsonSpec.Elm: instance (Data.JsonSpec.Elm.HasType spec, GHC.TypeLits.KnownSymbol name) => Data.JsonSpec.Elm.ElmDef '(name, spec)
- Data.JsonSpec.Elm: instance (Data.JsonSpec.Elm.Record fields, Data.JsonSpec.Elm.BaseFields (Data.JsonSpec.Elm.Reverse fields), Data.JsonSpec.Elm.Lambda (Data.JsonSpec.Elm.LambdaDepth (Data.JsonSpec.Elm.Reverse fields)), Data.JsonSpec.Elm.Decoders fields) => Data.JsonSpec.Elm.HasType ('Data.JsonSpec.Spec.JsonObject fields)
- Data.JsonSpec.Elm: instance (Data.JsonSpec.Elm.SumDef ('Data.JsonSpec.Spec.JsonEither a b), Data.JsonSpec.Elm.HasType right) => Data.JsonSpec.Elm.SumDef ('Data.JsonSpec.Spec.JsonEither ('Data.JsonSpec.Spec.JsonEither a b) right)
- Data.JsonSpec.Elm: instance (Data.JsonSpec.Elm.SumDef ('Data.JsonSpec.Spec.JsonEither a b), Data.JsonSpec.Elm.SumDef ('Data.JsonSpec.Spec.JsonEither c d)) => Data.JsonSpec.Elm.SumDef ('Data.JsonSpec.Spec.JsonEither ('Data.JsonSpec.Spec.JsonEither a b) ('Data.JsonSpec.Spec.JsonEither c d))
- Data.JsonSpec.Elm: instance (Data.JsonSpec.Elm.SumDef ('Data.JsonSpec.Spec.JsonEither c d), Data.JsonSpec.Elm.HasType left) => Data.JsonSpec.Elm.SumDef ('Data.JsonSpec.Spec.JsonEither left ('Data.JsonSpec.Spec.JsonEither c d))
- Data.JsonSpec.Elm: instance (GHC.TypeLits.KnownSymbol name, Data.JsonSpec.Elm.SumDef ('Data.JsonSpec.Spec.JsonEither left right)) => Data.JsonSpec.Elm.ElmDef '(name, 'Data.JsonSpec.Spec.JsonEither left right)
- Data.JsonSpec.Elm: instance Data.JsonSpec.Elm.BaseFields '[]
- Data.JsonSpec.Elm: instance Data.JsonSpec.Elm.Decoders '[]
- Data.JsonSpec.Elm: instance Data.JsonSpec.Elm.Lambda (Bound.Var.Var () Data.Void.Void)
- Data.JsonSpec.Elm: instance Data.JsonSpec.Elm.Lambda deeper => Data.JsonSpec.Elm.Lambda (Bound.Var.Var () deeper)
+ Data.JsonSpec.Elm: instance (Data.JsonSpec.Elm.HasDef def, Data.JsonSpec.Elm.HasType ('Data.JsonSpec.Spec.JsonLet more spec)) => Data.JsonSpec.Elm.HasType ('Data.JsonSpec.Spec.JsonLet (def : more) spec)
+ Data.JsonSpec.Elm: instance (Data.JsonSpec.Elm.HasType def, GHC.TypeLits.KnownSymbol name) => Data.JsonSpec.Elm.SumDef ('Data.JsonSpec.Spec.JsonLet '[ '(name, def)] ('Data.JsonSpec.Spec.JsonRef name))
+ Data.JsonSpec.Elm: instance (Data.JsonSpec.Elm.HasType spec, GHC.TypeLits.KnownSymbol name) => Data.JsonSpec.Elm.HasDef '(name, spec)
+ Data.JsonSpec.Elm: instance (Data.JsonSpec.Elm.SumDef left, Data.JsonSpec.Elm.SumDef right) => Data.JsonSpec.Elm.SumDef ('Data.JsonSpec.Spec.JsonEither left right)
+ Data.JsonSpec.Elm: instance (GHC.TypeLits.KnownSymbol name, Data.JsonSpec.Elm.SumDef ('Data.JsonSpec.Spec.JsonEither left right)) => Data.JsonSpec.Elm.HasDef '(name, 'Data.JsonSpec.Spec.JsonEither left right)
+ Data.JsonSpec.Elm: instance Data.JsonSpec.Elm.HasType a => Data.JsonSpec.Elm.SumDef a
+ Data.JsonSpec.Elm: instance Data.JsonSpec.Elm.Record fields => Data.JsonSpec.Elm.HasType ('Data.JsonSpec.Spec.JsonObject fields)
+ Data.JsonSpec.Elm: type Named name def = JsonLet '[ '(name, def)] (JsonRef name)

Files

README.md view
@@ -5,3 +5,235 @@  See `/test/test.hs` for an example. +# Example++First let's define an example spec.++A couple of things to note:++1. Only things that are named using `JsonLet` will get elm types. (Also+   note, `Named` is just an alias for `JsonLet`.) So you will probably+   want to name the top-level of your spec at least.+2. `JsonEither` spec types _must_ be named. Elm can support anonymous+   record types but not anonymous sum types, so there is no way to embed+   an anonymous `JsonEither`. You have to give it a name.+3. Naming the constructors for sum types is a little tricky. If a branch+   of `JsonEither` is given a name (using `Named`), then we interpret+   the name as the name of the data constructor, not the name of the+   type contained within the branch.  To name both the data constructor+   and the type, you must use nested `Named`s.++```+type ExampleSpec =+  Named "ExampleType"+    ( JsonObject+        '[ '("stringField", JsonString)+         , '( "anonymousObject"+            , JsonObject+                '[ '("floatField", JsonNum)+                 , '("dateField", JsonDateTime)+                 , '( "sumType1"+                    , Named "SumTypeWithCustomConstructorNames"+                        ( JsonEither+                            ( JsonEither+                                (Named "IntConstructor" JsonInt)+                                (Named "StringConstructor" JsonString)+                            )+                            (Named "FloatConstructor" JsonNum)+                        )+                    )+                 , '( "sumType2"+                    , Named "SumTypeWithAutomaticConstructorNames"+                        ( JsonEither+                            ( JsonEither+                                JsonInt+                                JsonString+                            )+                            JsonNum+                        )+                    )+                 ]+            )+         , '( "namedObject"+            , Named "NamedElmRecord"+                ( JsonObject+                    '[ '("stringField", JsonString)+                     , '( "listOfStrings"+                        , JsonArray JsonString+                        )+                     ]+                )+            )+         ]+    )+```++This spec will produce the following code (after running it through+`elm-format`):++```+module Api.Data exposing+  ( ExampleType+  , NamedElmRecord+  , SumTypeWithAutomaticConstructorNames(..)+  , SumTypeWithCustomConstructorNames(..)+  , exampleTypeDecoder+  , exampleTypeEncoder+  , namedElmRecordDecoder+  , namedElmRecordEncoder+  , sumTypeWithAutomaticConstructorNamesDecoder+  , sumTypeWithAutomaticConstructorNamesEncoder+  , sumTypeWithCustomConstructorNamesDecoder+  , sumTypeWithCustomConstructorNamesEncoder+  )++import Iso8601+import Json.Decode+import Json.Encode+import Time+++exampleTypeDecoder : Json.Decode.Decoder ExampleType+exampleTypeDecoder =+  Json.Decode.succeed+    (\a b c ->+      { stringField = a+      , anonymousObject = b+      , namedObject = c+      }+    )+    |> Json.Decode.andThen (\a -> Json.Decode.map a Json.Decode.string)+    |> Json.Decode.andThen+        (\a ->+          Json.Decode.map a+            (Json.Decode.succeed+              (\b c d e ->+                { floatField = b+                , dateField = c+                , sumType1 = d+                , sumType2 = e+                }+              )+              |> Json.Decode.andThen (\b -> Json.Decode.map b Json.Decode.float)+              |> Json.Decode.andThen (\b -> Json.Decode.map b Iso8601.decoder)+              |> Json.Decode.andThen (\b -> Json.Decode.map b sumTypeWithCustomConstructorNamesDecoder)+              |> Json.Decode.andThen (\b -> Json.Decode.map b sumTypeWithAutomaticConstructorNamesDecoder)+            )+        )+    |> Json.Decode.andThen (\a -> Json.Decode.map a namedElmRecordDecoder)+++exampleTypeEncoder : ExampleType -> Json.Encode.Value+exampleTypeEncoder a =+  Json.Encode.object+    [ ( "stringField", Json.Encode.string a.stringField )+    , ( "anonymousObject"+      , (\b ->+          Json.Encode.object+            [ ( "floatField", Json.Encode.float b.floatField )+            , ( "dateField", Iso8601.encode b.dateField )+            , ( "sumType1", sumTypeWithCustomConstructorNamesEncoder b.sumType1 )+            , ( "sumType2", sumTypeWithAutomaticConstructorNamesEncoder b.sumType2 )+            ]+        )+          a.anonymousObject+      )+    , ( "namedObject", namedElmRecordEncoder a.namedObject )+    ]+++namedElmRecordDecoder : Json.Decode.Decoder NamedElmRecord+namedElmRecordDecoder =+  Json.Decode.succeed (\a b -> { stringField = a, listOfStrings = b })+    |> Json.Decode.andThen (\a -> Json.Decode.map a Json.Decode.string)+    |> Json.Decode.andThen (\a -> Json.Decode.map a (Json.Decode.list Json.Decode.string))+++namedElmRecordEncoder : NamedElmRecord -> Json.Encode.Value+namedElmRecordEncoder a =+  Json.Encode.object+    [ ( "stringField", Json.Encode.string a.stringField )+    , ( "listOfStrings", Json.Encode.list Json.Encode.string a.listOfStrings )+    ]+++sumTypeWithAutomaticConstructorNamesDecoder : Json.Decode.Decoder SumTypeWithAutomaticConstructorNames+sumTypeWithAutomaticConstructorNamesDecoder =+  Json.Decode.oneOf+    [ Json.Decode.map SumTypeWithAutomaticConstructorNames_1 Json.Decode.int+    , Json.Decode.map SumTypeWithAutomaticConstructorNames_2 Json.Decode.string+    , Json.Decode.map SumTypeWithAutomaticConstructorNames_3 Json.Decode.float+    ]+++sumTypeWithAutomaticConstructorNamesEncoder : SumTypeWithAutomaticConstructorNames -> Json.Encode.Value+sumTypeWithAutomaticConstructorNamesEncoder a =+  case a of+    SumTypeWithAutomaticConstructorNames_1 b ->+      Json.Encode.int b++    SumTypeWithAutomaticConstructorNames_2 b ->+      Json.Encode.string b++    SumTypeWithAutomaticConstructorNames_3 b ->+      Json.Encode.float b+++sumTypeWithCustomConstructorNamesDecoder : Json.Decode.Decoder SumTypeWithCustomConstructorNames+sumTypeWithCustomConstructorNamesDecoder =+  Json.Decode.oneOf+    [ Json.Decode.map IntConstructor Json.Decode.int+    , Json.Decode.map StringConstructor Json.Decode.string+    , Json.Decode.map FloatConstructor Json.Decode.float+    ]+++sumTypeWithCustomConstructorNamesEncoder : SumTypeWithCustomConstructorNames -> Json.Encode.Value+sumTypeWithCustomConstructorNamesEncoder a =+  case a of+    IntConstructor b ->+      Json.Encode.int b++    StringConstructor b ->+      Json.Encode.string b++    FloatConstructor b ->+      Json.Encode.float b+++type SumTypeWithAutomaticConstructorNames+  = SumTypeWithAutomaticConstructorNames_1 Int+  | SumTypeWithAutomaticConstructorNames_2 String+  | SumTypeWithAutomaticConstructorNames_3 Float+++type SumTypeWithCustomConstructorNames+  = IntConstructor Int+  | StringConstructor String+  | FloatConstructor Float+++type alias ExampleType =+  { stringField : String+  , anonymousObject :+      { floatField : Float+      , dateField : Time.Posix+      , sumType1 : SumTypeWithCustomConstructorNames+      , sumType2 : SumTypeWithAutomaticConstructorNames+      }+  , namedObject : NamedElmRecord+  }+++type alias NamedElmRecord =+  { stringField : String, listOfStrings : List String }+```++## Generating code++The main function exposed by this module is `elmDefs`, which returns+a `Set Definition` (where `Definition` is from the `elm-syntax`+package). For examples on how to transform a `Definition` into some+files on disk, see `/test/test.hs`.++
json-spec-elm.cabal view
@@ -1,6 +1,6 @@ cabal-version:       3.0 name:                json-spec-elm-version:             0.1.0.1+version:             0.2.0.0 synopsis:            Elm code generate for `json-spec`. description:                               Produce elm types, encoders, and decoders from a@@ -56,5 +56,7 @@   default-language: Haskell2010   build-depends:     , json-spec-elm-    , hspec >= 2.11.1 && < 2.12+    , directory >= 1.3.7.1  && < 1.4+    , hspec     >= 2.11.1   && < 2.12+    , process   >= 1.6.16.0 && < 1.7 
src/Data/JsonSpec/Elm.hs view
@@ -2,6 +2,7 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PolyKinds #-}@@ -15,10 +16,11 @@   elmDefs,   Definitions,   HasType(..),+  Named, ) where  -import Bound (Scope(Scope), Var(B, F), toScope)+import Bound (Scope(Scope), Var(B), abstract1, closed, toScope) import Control.Monad.Writer (MonadWriter(tell), Writer, execWriter) import Data.JsonSpec (Specification(JsonArray, JsonBool, JsonDateTime,   JsonEither, JsonInt, JsonLet, JsonNullable, JsonNum, JsonObject,@@ -31,12 +33,12 @@ import GHC.TypeLits (ErrorMessage((:$$:), (:<>:)), KnownSymbol, Symbol,   TypeError, symbolVal) import Language.Elm.Definition (Definition)-import Language.Elm.Expression ((|>), Expression, bind, if_)+import Language.Elm.Expression ((|>), Expression, if_) import Language.Elm.Name (Constructor, Qualified) import Language.Elm.Type (Type)-import Prelude (Applicative(pure), Foldable(foldl), Functor(fmap),+import Prelude (Applicative(pure), Foldable(foldl, foldr), Functor(fmap),   Maybe(Just, Nothing), Monad((>>)), Semigroup((<>)), Show(show), ($),-  (++), (.), (<$>), Int, error, reverse, zip)+  (++), (.), (<$>), Int, error, fst, snd, zip) import qualified Data.Char as Char import qualified Data.Set as Set import qualified Data.Text as Text@@ -59,9 +61,11 @@ class Record (spec :: [(Symbol, Specification)]) where   recordDefs :: forall v. Definitions [(Name.Field, Type v)]   recordEncoders :: Definitions [(Text, Name.Field, Expression Void)]+  recordDecoders :: Definitions [(Text, Expression Void)] instance Record '[] where   recordDefs = pure []   recordEncoders = pure []+  recordDecoders = pure [] instance     ( HasType spec     , KnownSymbol name@@ -73,11 +77,15 @@     recordDefs = do       type_ <- typeOf @spec       moreFields <- recordDefs @more-      pure $ (fieldName @name, type_) : moreFields+      pure $ (fieldName (sym @name), type_) : moreFields     recordEncoders = do       encoder <- encoderOf @spec       moreFields <- recordEncoders @more-      pure $ (sym @name, fieldName @name, encoder) : moreFields+      pure $ (sym @name, fieldName (sym @name), encoder) : moreFields+    recordDecoders = do+      dec <- decoderOf @spec+      more <- recordDecoders @more+      pure $ ( sym @name , dec) : more   class HasType (spec :: Specification) where@@ -96,65 +104,47 @@   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-                ))-              )+instance (Record fields) => HasType (JsonObject fields) where+  typeOf = Type.Record <$> recordDefs @fields+  decoderOf = do+    decoders <- recordDecoders @fields+    pure $+      foldl+        (\expr decoder ->+          expr |>+            (+              "Json.Decode.andThen" `a`+                lam (\var -> "Json.Decode.map" `a` var `a` (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`+        )+        ("Json.Decode.succeed" `a` recordConstructor (fst <$> decoders))+        (snd <$> decoders)+  encoderOf = do+      fields <- recordEncoders @fields+      pure $+        Expr.Lam . toScope $+          "Json.Encode.object" `a`             Expr.List               [ Expr.apps "Basics.," [                 Expr.String jsonField,-                Expr.bind Expr.Global absurd encoder `Expr.App`-                  (Expr.Proj elmField `Expr.App` Expr.Var var)+                Expr.bind Expr.Global absurd encoder `a`+                  (Expr.Proj elmField `a` Expr.Var var)                 ]               | (jsonField, elmField, encoder) <- fields               ]-      where-        var :: Bound.Var () a-        var = B ()-+    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+    pure $ "Basics.List" `ta` elemType   decoderOf = do     dec <- decoderOf @spec-    pure $ Expr.App "Json.Decode.list" dec+    pure $ "Json.Decode.list" `a` dec   encoderOf = do     encoder <- encoderOf @spec-    pure $ "Json.Encode.list" `Expr.App` encoder+    pure $ "Json.Encode.list" `a` encoder instance HasType JsonBool where   typeOf = pure "Basics.Bool"   decoderOf = pure "Json.Decode.bool"@@ -163,10 +153,10 @@ instance (HasType spec) => HasType (JsonNullable spec) where   typeOf = do     type_ <- typeOf @spec-    pure $ Type.App "Maybe.Maybe" type_+    pure $ "Maybe.Maybe" `ta` type_   decoderOf = do     dec <- decoderOf @spec-    pure $ Expr.App "Json.Decode.nullable" dec+    pure $ a "Json.Decode.nullable" dec   encoderOf = do     encoder <- encoderOf @spec     pure $@@ -195,13 +185,13 @@                     , Expr.String (sym @const)                     ]                 )-                (Expr.App "Json.Decode.succeed" "Basics.()")-                (Expr.App "Json.Decode.fail" (Expr.String "Tag mismatch"))+                (a "Json.Decode.succeed" "Basics.()")+                (a "Json.Decode.fail" (Expr.String "Tag mismatch"))           ]   encoderOf =     pure $-      "Basics.always" `Expr.App`-        ("Json.Encode.string" `Expr.App` Expr.String (sym @const))+      "Basics.always" `a`+        ("Json.Encode.string" `a` Expr.String (sym @const)) instance HasType JsonDateTime where   typeOf = pure "Time.Posix"   decoderOf = pure "Iso8601.decoder"@@ -221,7 +211,7 @@   decoderOf = decoderOf @spec   encoderOf = encoderOf @spec instance {- HasType (JsonLet ( def : more ) spec) -}-    ( ElmDef def+    ( HasDef def     , HasType (JsonLet more spec)     )   =>@@ -282,32 +272,6 @@     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]@@ -319,39 +283,24 @@     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+class HasDef (def :: (Symbol, Specification)) where   defs :: Definitions ()-instance {-# OVERLAPS #-}+instance {-# OVERLAPS #-} {- HasDef '(name, JsonEither left right) -}     ( KnownSymbol name     , SumDef (JsonEither left right)     )   =>-    ElmDef '(name, JsonEither left right)+    HasDef '(name, JsonEither left right)   where     defs = do         branches <- sumDef @(JsonEither left right)         let           constructors :: [(Constructor, [Scope Int Type Void])]           constructors =-            [ ( Name.Constructor (constructorName n)+            [ ( Name.Constructor (constructorName conName n)               , [Scope type_]               )-            | (n, type_) <- zip [(1 :: Int) ..] branches+            | (n, (conName, type_)) <- zip [1..] branches             ]         decoders <- sumDecoders @(JsonEither left right)         encoders <- sumEncoders @(JsonEither left right)@@ -360,20 +309,15 @@           , Def.Constant               (decoderName @name)               0-              ( Scope-                  ( "Json.Decode.Decoder"-                    `Type.App`-                    Type.Global (localName name)-                  )-              )+              (Scope ("Json.Decode.Decoder" `ta` Type.Global (localName name)))               (                 "Json.Decode.oneOf"-                `Expr.App`+                `a`                 Expr.List                   [ "Json.Decode.map"-                    `Expr.App` Expr.Global (localName (constructorName n))-                    `Expr.App` dec-                  | (n, dec) <-  zip [(1 :: Int) ..] decoders+                    `a` Expr.Global (localName (constructorName conName n))+                    `a` dec+                  | (n, (conName, dec)) <-  zip [1..] decoders                   ]               )           , Def.Constant@@ -389,22 +333,24 @@                 Expr.Lam . toScope $                   Expr.Case                     (Expr.Var (B ()))-                    [ ( Pat.Con (localName (constructorName n)) [Pat.Var 0]+                    [ ( Pat.Con (localName (constructorName conName n)) [Pat.Var 0]                       , toScope $-                        fmap absurd encoder `Expr.App`+                        fmap absurd encoder `a`                           Expr.Var (B (0 :: Int))                       )-                    | (n, encoder) <- zip [1..] encoders+                    | (n, (conName, encoder)) <- zip [1..] encoders                     ]               )           ]       where-        constructorName :: Int -> Text-        constructorName n = name <> "_" <> showt n+        constructorName :: Maybe Text -> Int -> Text+        constructorName = \cases+          Nothing n -> name <> "_" <> showt n+          (Just consName) _ -> consName          name :: Text         name = sym @name-instance (HasType spec, KnownSymbol name) => ElmDef '(name, spec) where+instance (HasType spec, KnownSymbol name) => HasDef '(name, spec) where   defs = do     type_ <- typeOf @spec     dec <- decoderOf @spec@@ -418,9 +364,9 @@           (decoderName @name)           0           ( Scope-              ( Type.App-                  "Json.Decode.Decoder"-                  (Type.Global $ localName (sym @name))+              (+                "Json.Decode.Decoder" `ta`+                  Type.Global (localName (sym @name))               )           )           dec@@ -438,77 +384,52 @@   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 :: forall v. Definitions [(Maybe Text, Type v)]+  sumDecoders :: Definitions [(Maybe Text, Expression Void)]+  sumEncoders :: Definitions [(Maybe Text, Expression Void)]+instance+    (SumDef left, SumDef right)   =>-    SumDef (JsonEither (JsonEither a b) (JsonEither c d))+    SumDef (JsonEither left right)   where     sumDef = do-      left <- sumDef @(JsonEither a b)-      right <- sumDef @(JsonEither c d)+      left <- sumDef @left+      right <- sumDef @right       pure $ left ++ right     sumDecoders = do-      left <- sumDecoders @(JsonEither a b)-      right <- sumDecoders @(JsonEither c d)+      left <- sumDecoders @left+      right <- sumDecoders @right       pure (left ++ right)     sumEncoders = do-      left <- sumEncoders @(JsonEither a b)-      right <- sumEncoders @(JsonEither c d)+      left <- sumEncoders @left+      right <- sumEncoders @right       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)+    ( HasType def+    , KnownSymbol name+    )   =>-    SumDef (JsonEither left right)+    SumDef (JsonLet '[ '(name, def) ] (JsonRef name))   where     sumDef = do-      left <- typeOf @left-      right <- typeOf @right-      pure [left, right]+      typ <- typeOf @def+      pure [(Just (sym @name), typ)]     sumDecoders = do-      left <- decoderOf @left-      right <- decoderOf @right-      pure [left, right]+      dec <- decoderOf @def+      pure [(Just (sym @name), dec)]     sumEncoders = do-      left <- encoderOf @left-      right <- encoderOf @right-      pure [left, right]+      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@@ -541,11 +462,55 @@ encoderName = localName (lower (sym @name) <> "Encoder")  -fieldName :: forall name. (KnownSymbol name) => Name.Field-fieldName =+fieldName :: Text -> Name.Field+fieldName specName =   Name.Field $-    case sym @name of+    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+        ]+++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)  
test/test.hs view
@@ -4,19 +4,23 @@  module Main (main) where +import Data.Foldable (traverse_) import Data.HashMap.Strict (HashMap) import Data.JsonSpec (Specification(JsonArray, JsonDateTime, JsonEither,-  JsonInt, JsonLet, JsonObject, JsonRef, JsonString, JsonTag))-import Data.JsonSpec.Elm (elmDefs)+  JsonInt, JsonLet, JsonNum, JsonObject, JsonRef, JsonString, JsonTag))+import Data.JsonSpec.Elm (Named, elmDefs) import Data.Maybe (fromMaybe) import Data.Proxy (Proxy(Proxy)) import Data.Text (Text) import Language.Elm.Name (Module) import Language.Elm.Pretty (modules)-import Prelude (Functor(fmap), Semigroup((<>)), ($), (.), IO)+import Prelude (Bool(True), Functor(fmap), Semigroup((<>)), ($), (.),+  FilePath, IO, init) import Prettyprinter (defaultLayoutOptions, layoutPretty) import Prettyprinter.Render.Text (renderStrict)+import System.Directory (createDirectoryIfMissing) import System.IO (stderr)+import System.Process (callCommand) import Test.Hspec (describe, hspec, it, shouldBe) import qualified Data.HashMap.Strict as HM import qualified Data.Set as Set@@ -26,8 +30,8 @@ main :: IO () main =   hspec $ do-    describe "thing" $ do-      it "works" $+    describe "Code generation" $ do+      it "works with a complicated schema" $         let           actual :: HashMap Module Text           actual =@@ -107,7 +111,7 @@                   , ""                   , "inviteDecoder : Json.Decode.Decoder Invite"                   , "inviteDecoder ="-                  , "    Json.Decode.oneOf [ Json.Decode.map Invite_1 (Json.Decode.succeed (\\a b -> { type_ = a"+                  , "    Json.Decode.oneOf [ Json.Decode.map InviteUser (Json.Decode.succeed (\\a b -> { type_ = a"                   , "    , username = b }) |>"                   , "    Json.Decode.andThen (\\a -> Json.Decode.map a (Json.Decode.string |>"                   , "    Json.Decode.andThen (\\b -> if b == \"discord-user\" then"@@ -116,7 +120,7 @@                   , "    else"                   , "        Json.Decode.fail \"Tag mismatch\"))) |>"                   , "    Json.Decode.andThen (\\a -> Json.Decode.map a Json.Decode.string))"-                  , "    , Json.Decode.map Invite_2 (Json.Decode.succeed (\\a b -> { type_ = a"+                  , "    , Json.Decode.map InviteGuild (Json.Decode.succeed (\\a b -> { type_ = a"                   , "    , guild = b }) |>"                   , "    Json.Decode.andThen (\\a -> Json.Decode.map a (Json.Decode.string |>"                   , "    Json.Decode.andThen (\\b -> if b == \"discord-server\" then"@@ -133,19 +137,19 @@                   , "inviteEncoder : Invite -> Json.Encode.Value"                   , "inviteEncoder a ="                   , "    case a of"-                  , "        Invite_1 b ->"+                  , "        InviteUser b ->"                   , "            (\\c -> Json.Encode.object [ (\"type\" , always (Json.Encode.string \"discord-user\") c.type_)"                   , "            , (\"username\" , Json.Encode.string c.username) ]) b"                   , ""-                  , "        Invite_2 b ->"+                  , "        InviteGuild b ->"                   , "            (\\c -> Json.Encode.object [ (\"type\" , always (Json.Encode.string \"discord-server\") c.type_)"                   , "            , (\"guild\" , (\\d -> Json.Encode.object [ (\"id\" , Json.Encode.string d.id)"                   , "            , (\"name\" , Json.Encode.string d.name) ]) c.guild) ]) b"                   , ""                   , ""                   , "type Invite "-                  , "    = Invite_1 { type_ : (), username : String }"-                  , "    | Invite_2 { type_ : (), guild : { id : String, name : String } }"+                  , "    = InviteUser { type_ : (), username : String }"+                  , "    | InviteGuild { type_ : (), guild : { id : String, name : String } }"                   , ""                   , ""                   , "type alias Dashboard  ="@@ -169,8 +173,34 @@           TIO.hPutStrLn stderr (fromMaybe "" (HM.lookup ["Api", "Data"] actual))           TIO.hPutStrLn stderr "\n\n==========================================\n\n"           actual `shouldBe` expected+      it "works with the example schema" $+        let+          actual :: HashMap Module Text+          actual =+            fmap ((<> "\n") . renderStrict . layoutPretty defaultLayoutOptions)+            . modules+            . Set.toList+            $ elmDefs (Proxy @ExampleSpec) +        in do+          traverse_ writeModule (HM.toList actual)+          callCommand "(cd elm-test; elm-format src/ --yes)"+          callCommand+            "(\+              \cd elm-test; \+              \yes Y | (\+                \elm init; \+                \elm install rtfeldman/elm-iso8601-date-strings; \+                \elm install elm/json; \+                \elm install elm/url; \+                \elm install elm/time; \+                \elm install elm/http\+              \); \+              \elm make src/Api/Data.elm\+            \)"+          callCommand "rm -rf elm-test" + {-   This spec is copied from an as-yet uncompleted personal project. I   just used it because it is fairly complex. Probably something known@@ -201,17 +231,17 @@                 JsonLet '[                   '("Invite",                     JsonEither-                      (JsonObject '[+                      (Named "InviteUser" (JsonObject '[                         '("type", JsonTag "discord-user"),                         '("username", JsonString)-                      ])-                      (JsonObject '[+                      ]))+                      (Named "InviteGuild" (JsonObject '[                         '("type", JsonTag "discord-server"),                         '("guild", JsonObject '[                           '("id", JsonString),                           '("name", JsonString)                          ])-                      ])+                      ]))                   )                 ]                 (JsonArray (JsonRef "Invite")))),@@ -224,4 +254,64 @@         '("user", JsonString)        ])     ] ( JsonRef "Dashboard")+++type ExampleSpec =+  Named "ExampleType"+    ( JsonObject+        '[ '("stringField", JsonString)+         , '( "anonymousObject"+            , JsonObject+                '[ '("floatField", JsonNum)+                 , '("dateField", JsonDateTime)+                 , '( "sumType1"+                    , Named "SumTypeWithCustomConstructorNames"+                        ( JsonEither+                            ( JsonEither+                                (Named "IntConstructor" JsonInt)+                                (Named "StringConstructor" JsonString)+                            )+                            (Named "FloatConstructor" JsonNum)+                        )+                    )+                 , '( "sumType2"+                    , Named "SumTypeWithAutomaticConstructorNames"+                        ( JsonEither+                            ( JsonEither+                                JsonInt+                                JsonString+                            )+                            JsonNum+                        )+                    )+                 ]+            )+         , '( "namedObject"+            , Named "NamedElmRecord"+                ( JsonObject+                    '[ '("stringField", JsonString)+                     , '( "listOfStrings"+                        , JsonArray JsonString+                        )+                     ]+                )+            )+         ]+    )+++writeModule :: (Module, Text) -> IO ()+writeModule (module_, content) = do+    createDirectoryIfMissing True dirname+    TIO.writeFile filename content+  where+    pathName :: [Text] -> FilePath+    pathName = ("elm-test/src/" <>) . Text.unpack . Text.intercalate "/"++    filename :: FilePath+    filename = pathName module_ <> ".elm"++    dirname :: FilePath+    dirname = pathName (init module_)+