packages feed

haskell-to-elm (empty) → 0.1.0.0

raw patch · 8 files changed

+1170/−0 lines, 8 filesdep +aesondep +basedep +boundsetup-changed

Dependencies added: aeson, base, bound, elm-syntax, generics-sop, haskell-to-elm, protolude, text, time, unordered-containers

Files

+ CHANGELOG.md view
@@ -0,0 +1,7 @@+# Changelog for haskell-to-elm++## Unreleased changes++## 0.1.0.0++- Initial release
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Olle Fredriksson (c) 2019++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Olle Fredriksson nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,161 @@+# haskell-to-elm++`haskell-to-elm` is a library that takes Haskell type definitions as input and+generates matching Elm type definitions and JSON encoders and decoders that+match Aeson's format.++## The problem++Let's say we're building a web page with a Haskell backend and an Elm frontend.++We might have a Haskell type like this, that we pass to the frontend encoded as+JSON. The JSON encoder is derived using the Aeson library.++```haskell+data User = User+  { name :: Text+  , age :: Int+  } deriving (Generic, ToJSON)+```++We mirror the type on the Elm side and add a JSON decoder as follows:++```elm+type alias User =+    { name : String+    , age : Int+    }++decoder : Decoder User+decoder =+    Decode.map2 User+        (Decode.field "name" Decode.string)+        (Decode.field "age" Decode.int)+```++Now, let's say we want to change a field in the backend:++```haskell+-- Haskell+data User = User+  { name :: Text+--, age :: Int+  , birthday :: Date -- <---- new!+  } deriving (Generic, ToJSON)+```++If we now run the application again, but forget to update the Elm code, the+`User` decoder will fail at runtime in Elm.++## The solution++`haskell-to-elm` solves this problem by letting us _generate_ the Elm `User`+type and `decoder` from the Haskell `User` type.++With `haskell-to-elm` as part of your build pipeline you can make sure that the+frontend is always in sync with your backend, and get type errors in your+frontend code when you change your backend types.++The companion library [servant-to-elm](https://github.com/folq/servant-to-elm) also+lets you generate Elm client libraries for your Servant APIs.++## Basic usage++To generate code for the `User` type above, we first need to derive a bunch of class instances:++```haskell+data User = User+  { name :: Text+  , age :: Int+  } deriving+    ( Generic+    , Aeson.ToJSON+    , SOP.Generic+    , SOP.HasDatatypeInfo+    , HasElmDecoder Aeson.Value+    , HasElmEncoder Aeson.Value+    , HasElmType+    )++instance HasElmDefinition User where+  elmDefinition =+    deriveElmTypeDefinition @User defaultOptions "Api.User.User"++instance HasElmDecoderDefinition Aeson.Value User where+  elmDecoderDefinition =+    deriveElmJSONDecoder @User defaultOptions Aeson.defaultOptions "Api.User.decoder"++instance HasElmEncoderDefinition Aeson.Value User where+  elmEncoderDefinition =+    deriveElmJSONEncoder @User defaultOptions Aeson.defaultOptions "Api.User.encoder"+```++Then we can print the generated Elm code using the following code:++```haskell+main :: IO ()+main = do+  let+    definitions =+      jsonDefinitions @User++    modules =+      Pretty.modules definitions++  forM_ (HashMap.toList modules) $ \(_moduleName, contents) ->+    print contents+```++Running `main` will print the following Elm code:++```elm+module Api.User exposing (..)++import Json.Decode+import Json.Decode.Pipeline+import Json.Encode+++type alias User =+    { name : String, age : Int }+++encoder : User -> Json.Encode.Value+encoder a =+    Json.Encode.object [ ("name" , Json.Encode.string a.name)+    , ("age" , Json.Encode.int a.age) ]+++decoder : Json.Decode.Decoder User+decoder =+    Json.Decode.succeed User |>+    Json.Decode.Pipeline.required "name" Json.Decode.string |>+    Json.Decode.Pipeline.required "age" Json.Decode.int+```++In an actual project we would be writing the code to disk instead of printing it.++See [this file](examples/User.hs) for the full code with imports.++## Roadmap++- [x] Derive JSON encoders and generically+  - [ ] Support all Aeson options+- [x] Pretty-print the Elm AST+  - [x] Separate pretty printing from code generation: [elm-syntax](https://github.com/folq/elm-syntax)+- [x] Generate Elm modules+- [x] Servant client library generation: [servant-to-elm](https://github.com/folq/servant-to-elm)+- [x] Test that encoding and decoding round-trip: [elm-to-haskell-test](https://github.com/folq/elm-to-haskell-test)++## Related projects++Libraries that use or are used by haskell-to-elm:+- [elm-syntax](https://github.com/folq/elm-syntax) defines Haskell ASTs for Elm's syntax, and lets us pretty-print it.+- [servant-to-elm](https://github.com/folq/servant-to-elm) can be used to generate Elm client libraries from Servant APIs.+- [haskell-to-elm-test](https://github.com/folq/haskell-to-elm-test) does end-to-end testing of this library.++Others:+- [elm-export](http://hackage.haskell.org/package/elm-export)+- [elm-bridge](http://hackage.haskell.org/package/elm-bridge)+- [elm-street](http://hackage.haskell.org/package/elm-street)+- [elminator](https://github.com/sras/elminator)
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ examples/User.hs view
@@ -0,0 +1,53 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}+module User where++import Protolude++import qualified Data.Aeson as Aeson+import qualified Data.HashMap.Lazy as HashMap+import qualified Generics.SOP as SOP++import qualified Language.Elm.Pretty as Pretty+import Language.Haskell.To.Elm++data User = User+  { name :: Text+  , age :: Int+  } deriving+    ( Generic+    , Aeson.ToJSON+    , SOP.Generic+    , SOP.HasDatatypeInfo+    , HasElmDecoder Aeson.Value+    , HasElmEncoder Aeson.Value+    , HasElmType+    )++instance HasElmDefinition User where+  elmDefinition =+    deriveElmTypeDefinition @User defaultOptions "Api.User.User"++instance HasElmDecoderDefinition Aeson.Value User where+  elmDecoderDefinition =+    deriveElmJSONDecoder @User defaultOptions Aeson.defaultOptions "Api.User.decoder"++instance HasElmEncoderDefinition Aeson.Value User where+  elmEncoderDefinition =+    deriveElmJSONEncoder @User defaultOptions Aeson.defaultOptions "Api.User.encoder"++main :: IO ()+main = do+  let+    definitions =+      jsonDefinitions @User++    modules =+      Pretty.modules definitions++  forM_ (HashMap.toList modules) $ \(_moduleName, contents) ->+    print contents
+ haskell-to-elm.cabal view
@@ -0,0 +1,96 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.31.2.+--+-- see: https://github.com/sol/hpack+--+-- hash: ae480eeef791a5ae2e0dd89113815afda032b3f664b4e92f9241893aff5a8626++name:           haskell-to-elm+version:        0.1.0.0+synopsis:       Generate Elm types and JSON encoders and decoders from Haskell types+description:    Please see the README on GitHub at <https://github.com/folq/haskell-to-elm#readme>+category:       Elm, Compiler, Language+homepage:       https://github.com/folq/haskell-to-elm#readme+bug-reports:    https://github.com/folq/haskell-to-elm/issues+author:         Olle Fredriksson+maintainer:     fredriksson.olle@gmail.com+copyright:      2019 Olle Fredriksson+license:        BSD3+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    README.md+    CHANGELOG.md++source-repository head+  type: git+  location: https://github.com/folq/haskell-to-elm++flag examples+  description: Build examples+  manual: True+  default: False++library+  exposed-modules:+      Language.Haskell.To.Elm+  other-modules:+      Paths_haskell_to_elm+  hs-source-dirs:+      src+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -Wtabs -funbox-strict-fields+  build-depends:+      aeson >=1.4.0+    , base >=4.7 && <5+    , bound >=2.0.0+    , elm-syntax >=0.1.0 && <0.1.1+    , generics-sop >=0.4.0+    , protolude >=0.2.3+    , text >=1.2.0+    , time >=1.8.0+    , unordered-containers >=0.2.8+  default-language: Haskell2010++executable user-example+  main-is: User.hs+  other-modules:+      Paths_haskell_to_elm+  hs-source-dirs:+      examples+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -Wtabs -funbox-strict-fields+  build-depends:+      aeson >=1.4.0+    , base >=4.7 && <5+    , bound >=2.0.0+    , elm-syntax >=0.1.0 && <0.1.1+    , generics-sop >=0.4.0+    , haskell-to-elm+    , protolude >=0.2.3+    , text >=1.2.0+    , time >=1.8.0+    , unordered-containers >=0.2.8+  if !flag(examples)+    buildable: False+  default-language: Haskell2010++test-suite haskell-to-elm-test+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      Paths_haskell_to_elm+  hs-source-dirs:+      test+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -Wtabs -funbox-strict-fields -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      aeson >=1.4.0+    , base >=4.7 && <5+    , bound >=2.0.0+    , elm-syntax >=0.1.0 && <0.1.1+    , generics-sop >=0.4.0+    , haskell-to-elm+    , protolude >=0.2.3+    , text >=1.2.0+    , time >=1.8.0+    , unordered-containers >=0.2.8+  default-language: Haskell2010
+ src/Language/Haskell/To/Elm.hs view
@@ -0,0 +1,819 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DefaultSignatures #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PartialTypeSignatures #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeOperators #-}+{-# OPTIONS_GHC -Wno-partial-type-signatures #-}+module Language.Haskell.To.Elm where++import Protolude hiding (All, Infix, Type)++import qualified Bound+import qualified Data.Aeson as Aeson+import Data.HashMap.Lazy (HashMap)+import qualified Data.HashMap.Lazy as HashMap+import Data.String+import Data.Text (Text)+import Data.Time+import Generics.SOP as SOP++import Language.Elm.Definition (Definition)+import qualified Language.Elm.Definition as Definition+import Language.Elm.Expression (Expression)+import qualified Language.Elm.Expression as Expression+import qualified Language.Elm.Name as Name+import qualified Language.Elm.Pattern as Pattern+import Language.Elm.Type (Type)+import qualified Language.Elm.Type as Type++-------------------------------------------------------------------------------+-- * Classes++-- | Represents that the corresponding Elm type for the Haskell type @a@ is @'elmType' \@a@.+--+-- This class has a default instance for types that satisfy 'HasElmDefinition',+-- which refers to the name of that definition.+class HasElmType a where+  elmType :: Type v+  default elmType :: HasElmDefinition a => Type v+  elmType = Type.Global $ Definition.name $ elmDefinition @a++-- | Represents that we can generate the definition for the Elm type that+-- corresponds to @a@ using @'elmDefinition' \@a@.+--+-- See 'deriveElmTypeDefinition' for a way to automatically derive 'elmDefinition'.+class HasElmDefinition a where+  elmDefinition :: Definition++-- | Represents that the Elm type that corresponds to @a@ has a decoder from+-- @value@, namely @'elmDecoder' \@value \@a@.+--+-- This class has a default instance for types that satisfy+-- 'HasElmDecoderDefinition', which refers to the name of that definition.+class HasElmType a => HasElmDecoder value a where+  elmDecoder :: Expression v+  default elmDecoder :: HasElmDecoderDefinition value a => Expression v+  elmDecoder = Expression.Global $ Definition.name $ elmDecoderDefinition @value @a++-- | Represents that the Elm type that corresponds to @a@ has an encoder into+-- @value@, namely @'elmEncoder' \@value \@a@.+--+-- This class has a default instance for types that satisfy+-- 'HasElmEncoderDefinition', which refers to the name of that definition.+class HasElmType a => HasElmEncoder value a where+  elmEncoder :: Expression v+  default elmEncoder :: HasElmEncoderDefinition value a => Expression v+  elmEncoder = Expression.Global $ Definition.name $ elmEncoderDefinition @value @a++-- | Represents that we can generate the Elm decoder definition from @value@+-- for the Elm type that corresponds to @a@.+--+-- See 'deriveElmJSONDecoder' for a way to automatically derive+-- 'elmDecoderDefinition' when @value = 'Aeson.Value'@.+class HasElmDecoderDefinition value a where+  elmDecoderDefinition :: Definition++-- | Represents that we can generate the Elm encoder definition into @value@+-- for the Elm type that corresponds to @a@.+--+-- See 'deriveElmJSONEncoder' for a way to automatically derive+-- 'elmEncoderDefinition' when @value = 'Aeson.Value'@.+class HasElmEncoderDefinition value a where+  elmEncoderDefinition :: Definition++-------------------------------------------------------------------------------+-- * Derivers++-- | Elm code generation options+newtype Options = Options+  { fieldLabelModifier :: String -> String -- ^ Use this function to go from Haskell record field name to Elm record field name.+  }++defaultOptions :: Options+defaultOptions =+  Options+    { fieldLabelModifier = identity+    }++-- | Automatically create an Elm definition given a Haskell type.+--+-- This is suitable for use as a 'HasElmDefinition' instance:+--+-- @+-- instance 'HasElmDefinition' MyType where+--   'elmDefinition' =+--     'deriveElmTypeDefinition' \@MyType 'defaultOptions' \"Api.MyType.MyType\"+-- @+deriveElmTypeDefinition :: forall a. (HasDatatypeInfo a, All2 HasElmType (Code a)) => Options -> Name.Qualified -> Definition+deriveElmTypeDefinition options name =+  case datatypeInfo (Proxy @a) of+    ADT _mname _tname (Record _cname fields :* Nil) ->+      Definition.Alias name (Type.Record (recordFields fields))++    ADT _mname _tname cs ->+      Definition.Type name (constructors cs)++    Newtype _mname _tname (Record _cname fields) ->+      Definition.Alias name (Type.Record (recordFields fields))++    Newtype _mname _tname c ->+      Definition.Type name (constructors (c :* Nil))+  where+    recordFields :: All HasElmType xs => NP FieldInfo xs -> [(Name.Field, Type v)]+    recordFields Nil = []+    recordFields (f :* fs) = field f : recordFields fs++    field :: forall x v. HasElmType x => FieldInfo x -> (Name.Field, Type v)+    field (FieldInfo fname) =+      (fromString $ fieldLabelModifier options fname, elmType @x)++    constructors :: All2 HasElmType xss => NP ConstructorInfo xss -> [(Name.Constructor, [Type v])]+    constructors Nil = []+    constructors (c :* cs) = constructor c : constructors cs++    constructor :: forall xs v. All HasElmType xs => ConstructorInfo xs -> (Name.Constructor, [Type v])+    constructor (Constructor cname) = (fromString cname, constructorFields $ shape @_ @xs)+    constructor (Infix _ _ _) = panic "Infix constructors are not supported"+    constructor (Record cname fs) = (fromString cname, [Type.Record $ recordFields fs])++    constructorFields :: All HasElmType xs => Shape xs -> [Type v]+    constructorFields ShapeNil = []+    constructorFields s@(ShapeCons _) = go s+      where+        go :: forall x xs v. (HasElmType x, All HasElmType xs) => Shape (x ': xs) -> [Type v]+        go (ShapeCons s') = elmType @x : constructorFields s'++-- | Automatically create an Elm JSON decoder definition given a Haskell type.+--+-- This is suitable for use as a @'HasElmDecoderDefinition' 'Aeson.Value'@ instance:+--+-- @+-- instance 'HasElmDecoderDefinition' 'Aeson.Value' MyType where+--   'elmDecoderDefinition' =+--     'deriveElmJSONDecoder' \@MyType 'defaultOptions' 'Aeson.defaultOptions' "Api.MyType.decoder"+-- @+--+-- Uses the given 'Aeson.Options' to match the JSON format of derived+-- 'Aeson.FromJSON' and 'Aeson.ToJSON' instances.+deriveElmJSONDecoder+  :: forall a+  . (HasDatatypeInfo a, HasElmType a, All2 (HasElmDecoder Aeson.Value) (Code a))+  => Options+  -> Aeson.Options+  -> Name.Qualified+  -> Definition+deriveElmJSONDecoder options aesonOptions decoderName =+  Definition.Constant decoderName (Type.App "Json.Decode.Decoder" $ elmType @a) $+  case datatypeInfo (Proxy @a) of+    ADT _mname _tname (Record _cname fields :* Nil) ->+      decodeRecord fields $+      Expression.App "Json.Decode.succeed" $+      case Type.appsView (elmType @a) of+        (Type.Record fieldTypes, _) ->+          explicitRecordConstructor $ fst <$> fieldTypes++        _ ->+          Expression.Global typeName++    ADT _mname _tname cs ->+      decodeConstructors $ constructors cs++    Newtype _mname _tname (Record _cname fields) ->+      decodeRecord fields $+      Expression.App "Json.Decode.succeed" $+      case Type.appsView (elmType @a) of+        (Type.Record fieldTypes, _) ->+          explicitRecordConstructor $ fst <$> fieldTypes++        _ ->+          Expression.Global typeName++    Newtype _mname _tname c ->+      decodeConstructors $ constructors (c :* Nil)+  where+    typeName@(Name.Qualified moduleName_ _) =+      case Type.appsView (elmType @a) of+        (Type.Global tname, _) -> tname++        _ ->+          panic "Can't automatically derive JSON decoder for an anonymous Elm type"++    constructors+      :: All2 (HasElmDecoder Aeson.Value) xss+      => NP ConstructorInfo xss+      -> [(String, [Expression v])]+    constructors Nil = []+    constructors (c :* cs) = constructor c : constructors cs++    constructor+      :: forall xs v+      . All (HasElmDecoder Aeson.Value) xs+      => ConstructorInfo xs+      -> (String, [Expression v])+    constructor (Constructor cname) =+      (cname, constructorFields $ shape @_ @xs)+    constructor (Infix _ _ _) =+      panic "Infix constructors are not supported"+    constructor (Record cname fs) =+      (cname, [decodeRecord fs $ explicitRecordConstructor $ recordFieldNames fs])++    constructorFields+      :: All (HasElmDecoder Aeson.Value) xs+      => Shape xs+      -> [Expression v]+    constructorFields ShapeNil = []+    constructorFields s@(ShapeCons _) = go s+      where+        go+          :: forall x xs v+          . (HasElmDecoder Aeson.Value x, All (HasElmDecoder Aeson.Value) xs)+          => Shape (x ': xs)+          -> [Expression v]+        go (ShapeCons s') = elmDecoder @Aeson.Value @x : constructorFields s'++    explicitRecordConstructor+      :: [Name.Field]+      -> Expression v+    explicitRecordConstructor names =+      go mempty names+      where+        go :: HashMap Name.Field v -> [Name.Field] -> Expression v+        go locals fnames =+          case fnames of+            [] ->+              Expression.Record [(name, Expression.Var $ locals HashMap.! name) | name <- names]++            fname:fnames' ->+              Expression.Lam $ Bound.toScope $ go (HashMap.insert fname (Bound.B ()) $ Bound.F <$> locals) fnames'++    decodeRecord+      :: All (HasElmDecoder Aeson.Value) xs+      => NP FieldInfo xs+      -> Expression v+      -> Expression v+    decodeRecord (f :* Nil)+      | Aeson.unwrapUnaryRecords aesonOptions =+        unwrappedRecordField f+    decodeRecord fs =+      recordFields fs++    recordFields+      :: All (HasElmDecoder Aeson.Value) xs+      => NP FieldInfo xs+      -> Expression v+      -> Expression v+    recordFields Nil e = e+    recordFields (f :* fs) e =+      recordFields fs $ recordField f e++    isMaybe :: forall t. HasElmType t => Bool+    isMaybe =+      case Type.appsView $ elmType @t of+        (Type.Global "Maybe.Maybe", _) -> True+        _ -> False++    recordField+      :: forall x v+      . HasElmDecoder Aeson.Value x+      => FieldInfo x+      -> Expression v+      -> Expression v+    recordField (FieldInfo fname) e+      | Aeson.omitNothingFields aesonOptions && isMaybe @x =+        e Expression.|>+          Expression.apps+            "Json.Decode.Pipeline.optional"+            [ jsonFieldName fname+            , elmDecoder @Aeson.Value @x+            , "Maybe.Nothing"+            ]++      | otherwise =+        e Expression.|>+          Expression.apps+            "Json.Decode.Pipeline.required"+            [ jsonFieldName fname+            , elmDecoder @Aeson.Value @x+            ]++    unwrappedRecordField+      :: forall x v+      . HasElmDecoder Aeson.Value x+      => FieldInfo x+      -> Expression v+      -> Expression v+    unwrappedRecordField (FieldInfo _) e =+      e Expression.|> elmDecoder @Aeson.Value @x++    recordFieldNames+      :: All (HasElmDecoder Aeson.Value) xs+      => NP FieldInfo xs+      -> [Name.Field]+    recordFieldNames Nil = []+    recordFieldNames (FieldInfo fname :* fs) =+      elmField fname : recordFieldNames fs++    constructorJSONName :: String -> Text+    constructorJSONName = toS . Aeson.constructorTagModifier aesonOptions++    jsonFieldName :: String -> Expression v+    jsonFieldName = Expression.String . toS . Aeson.fieldLabelModifier aesonOptions++    elmField :: String -> Name.Field+    elmField = fromString . fieldLabelModifier options++    decodeConstructor :: String -> Expression v -> [Expression v] -> Expression v+    decodeConstructor _ constr [] =+      Expression.App "Json.Decode.succeed" constr++    decodeConstructor contentsName constr [constrField] =+      Expression.App "Json.Decode.succeed" constr Expression.|>+        Expression.apps "Json.Decode.Pipeline.required" [Expression.String (toS contentsName), constrField]++    decodeConstructor contentsName constr constrFields =+      Expression.apps+        "Json.Decode.field"+        [ Expression.String (toS contentsName)+        , foldl'+          (Expression.|>)+          (Expression.App "Json.Decode.succeed" constr)+          [ Expression.App+            "Json.Decode.Pipeline.custom"+            (Expression.apps "Json.Decode.index" [Expression.Int index, field])+          | (index, field) <- zip [0..] constrFields+          ]+        ]++    decodeConstructors :: [(String, [Expression v])] -> Expression v+    decodeConstructors [(constr, constrFields)]+      | not $ Aeson.tagSingleConstructors aesonOptions =+        let+          qualifiedConstr =+            Expression.Global $ Name.Qualified moduleName_ $ toS constr+        in+        case constrFields of+          [constrField] ->+            Expression.apps "Json.Decode.map" [qualifiedConstr, constrField]++          _ ->+            foldl'+              (Expression.|>)+              (Expression.App "Json.Decode.succeed" qualifiedConstr)+              [Expression.App+                "Json.Decode.Pipeline.custom"+                (Expression.apps "Json.Decode.index" [Expression.Int index, field])+              | (index, field) <- zip [0..] constrFields+              ]++    decodeConstructors constrs+      | Aeson.allNullaryToStringTag aesonOptions && allNullary constrs =+        "Json.Decode.string" Expression.|> Expression.App "Json.Decode.andThen" (Expression.Lam+          (Bound.toScope $ Expression.Case (pure $ Bound.B ()) $+            [ ( Pattern.String $ constructorJSONName constr+              , Bound.toScope $ Expression.App "Json.Decode.succeed" qualifiedConstr+              )+            | (constr, _) <- constrs+            , let+                qualifiedConstr =+                  Expression.Global $ Name.Qualified moduleName_ $ toS constr+            ]+            +++            [ ( Pattern.Wildcard+              , Bound.toScope $ Expression.App "Json.Decode.fail" $ Expression.String "No matching constructor"+              )+            ]+          ))++    decodeConstructors constrs =+      case Aeson.sumEncoding aesonOptions of+        Aeson.TaggedObject tagName contentsName ->+          Expression.apps "Json.Decode.field" [Expression.String $ toS tagName, "Json.Decode.string"] Expression.|>+            Expression.App "Json.Decode.andThen" (Expression.Lam+              (Bound.toScope $ Expression.Case (pure $ Bound.B ()) $+                [ ( Pattern.String $ constructorJSONName constr+                  , Bound.toScope $ decodeConstructor contentsName qualifiedConstr (fmap (Bound.F . Bound.F) <$> fields)+                  )+              | (constr, fields) <- constrs+              , let+                  qualifiedConstr =+                    Expression.Global $ Name.Qualified moduleName_ $ toS constr+              ]+              +++              [ ( Pattern.Wildcard+                , Bound.toScope $ Expression.App "Json.Decode.fail" $ Expression.String "No matching constructor"+                )+              ]+            ))++        _ -> panic "Only the DataAeson.TaggedObject sumEncoding is currently supported"++    allNullary :: forall c f. [(c, [f])] -> Bool+    allNullary = all (null . snd)++-- | Automatically create an Elm JSON encoder definition given a Haskell type.+--+-- This is suitable for use as a @'HasElmEncoderDefinition' 'Aeson.Value'@ instance:+--+-- @+-- instance 'HasElmEncoderDefinition' 'Aeson.Value' MyType where+--   'elmEncoderDefinition' =+--     'deriveElmJSONEncoder' \@MyType 'defaultOptions' 'Aeson.defaultOptions' "Api.MyType.encoder"+-- @+--+-- Uses the given 'Aeson.Options' to match the JSON format of derived+-- 'Aeson.FromJSON' and 'Aeson.ToJSON' instances.+deriveElmJSONEncoder+  :: forall a+  . (HasDatatypeInfo a, HasElmType a, All2 (HasElmEncoder Aeson.Value) (Code a))+  => Options+  -> Aeson.Options+  -> Name.Qualified+  -> Definition+deriveElmJSONEncoder options aesonOptions encoderName =+  Definition.Constant encoderName (Type.Fun (elmType @a) "Json.Encode.Value") $+  Expression.Lam $ Bound.toScope $+    case datatypeInfo (Proxy @a) of+      ADT _mname _tname (Record _cname fields :* Nil) ->+        encodeRecord fields $ pure $ Bound.B ()++      ADT _mname _tname cs ->+        encodeConstructors (constructors cs) (pure $ Bound.B ())++      Newtype _mname _tname (Record _cname fields) ->+        encodeRecord fields $ pure $ Bound.B ()++      Newtype _mname _tname c ->+        encodeConstructors (constructors (c :* Nil)) (pure $ Bound.B ())+  where+    (Name.Qualified moduleName_ _) =+      case Type.appsView (elmType @a) of+        (Type.Global tname, _) -> tname++        _ ->+          panic "Can't automatically derive JSON encoder for an anonymous Elm type"++    constructors+      :: All2 (HasElmEncoder Aeson.Value) xss+      => NP ConstructorInfo xss+      -> [(String, [Expression v])]+    constructors Nil = []+    constructors (c :* cs) = constructor c : constructors cs++    constructor+      :: forall xs v+      . All (HasElmEncoder Aeson.Value) xs+      => ConstructorInfo xs+      -> (String, [Expression v])+    constructor (Constructor cname) =+      (cname, constructorFields $ shape @_ @xs)+    constructor Infix {} =+      panic "Infix constructors are not supported"+    constructor (Record cname fs) =+      (cname, [Expression.Lam $ Bound.toScope $ encodeRecord fs (pure $ Bound.B ())])++    constructorFields+      :: All (HasElmEncoder Aeson.Value) xs+      => Shape xs+      -> [Expression v]+    constructorFields ShapeNil = []+    constructorFields s@(ShapeCons _) = go s+      where+        go+          :: forall x xs v+          . (HasElmEncoder Aeson.Value x, All (HasElmEncoder Aeson.Value) xs)+          => Shape (x ': xs)+          -> [Expression v]+        go (ShapeCons s') = elmEncoder @Aeson.Value @x : constructorFields s'++    encodeRecord+      :: All (HasElmEncoder Aeson.Value) xs+      => NP FieldInfo xs+      -> Expression v+      -> Expression v+    encodeRecord (f :* Nil) e+      | Aeson.unwrapUnaryRecords aesonOptions =+        unwrappedRecordField f e+    encodeRecord fs e =+      Expression.App "Json.Encode.object" $+        case recordFields fs e of+          (nonNullable, []) ->+            Expression.List nonNullable++          ([], nullable) ->+            Expression.App "List.concat" $ Expression.List nullable++          (nonNullable, nullable) ->+            Expression.apps "Basics.++"+              [ Expression.List nonNullable+              , Expression.App "List.concat" $ Expression.List nullable+              ]++    recordFields+      :: All (HasElmEncoder Aeson.Value) xs+      => NP FieldInfo xs+      -> Expression v+      -> ([Expression v], [Expression v])+    recordFields Nil _ = mempty+    recordFields (f :* fs) e =+      recordField f e <> recordFields fs e++    isMaybe :: forall t. HasElmType t => Bool+    isMaybe =+      case Type.appsView $ elmType @t of+        (Type.Global "Maybe.Maybe", _) -> True+        _ -> False++    recordField+      :: forall x v+      . HasElmEncoder Aeson.Value x+      => FieldInfo x+      -> Expression v+      -> ([Expression v], [Expression v])+    recordField (FieldInfo fname) e+      | Aeson.omitNothingFields aesonOptions && isMaybe @x =+        ( []+        , [ Expression.Case (Expression.App (Expression.Proj $ elmField fname) e)+            [ ( Pattern.Con "Maybe.Nothing" []+              , Bound.toScope $ Expression.List []+              )+            , ( Pattern.Con "Maybe.Just" [Pattern.Var 0]+              , Bound.toScope $+                Expression.App+                  (elmEncoder @Aeson.Value @x)+                  (Expression.App (Expression.Proj $ elmField fname) (Bound.F <$> e))+              )+            ]+          ]+        )++      | otherwise =+        ( [ Expression.tuple+            (jsonFieldName fname)+            (Expression.App (elmEncoder @Aeson.Value @x) (Expression.App (Expression.Proj $ elmField fname) e))+          ]+        , []+        )++    unwrappedRecordField+      :: forall x v+      . HasElmEncoder Aeson.Value x+      => FieldInfo x+      -> Expression v+      -> Expression v+    unwrappedRecordField (FieldInfo _) =+      Expression.App (elmEncoder @Aeson.Value @x)++    constructorJSONName :: String -> Text+    constructorJSONName = toS . Aeson.constructorTagModifier aesonOptions++    jsonFieldName :: String -> Expression v+    jsonFieldName = Expression.String . toS . Aeson.fieldLabelModifier aesonOptions++    elmField :: String -> Name.Field+    elmField = fromString . fieldLabelModifier options++    elmConstr :: String -> Name.Qualified+    elmConstr = Name.Qualified moduleName_ . fromString++    encodeConstructorFields :: [Expression v] -> Expression (Bound.Var Int v)+    encodeConstructorFields [constrField] =+      Expression.App (Bound.F <$> constrField) (pure $ Bound.B 0)++    encodeConstructorFields constrFields =+      Expression.apps+        "Json.Encode.list"+        [ "Basics.identity"+        , Expression.List+          [ Expression.App (Bound.F <$> field) (pure $ Bound.B index)+          | (index, field) <- zip [0..] constrFields+          ]+        ]++    encodeConstructors :: [(String, [Expression v])] -> Expression v -> Expression v+    encodeConstructors [(constr, constrFields)] expr+      | not $ Aeson.tagSingleConstructors aesonOptions =+        Expression.Case expr+          [ ( Pattern.Con (elmConstr constr) (Pattern.Var . fst <$> zip [0..] constrFields)+            , Bound.toScope $ encodeConstructorFields constrFields+            )+          ]++    encodeConstructors constrs expr+      | Aeson.allNullaryToStringTag aesonOptions && allNullary constrs =+        Expression.Case expr+          [ ( Pattern.Con (elmConstr constr) []+            , Bound.toScope $+              Expression.App "Json.Encode.string" $ Expression.String $ constructorJSONName constr+            )+          | (constr, _) <- constrs+          ]++    encodeConstructors constrs expr =+      case Aeson.sumEncoding aesonOptions of+        Aeson.TaggedObject tagName contentsName ->+          Expression.Case expr+            [ ( Pattern.Con (elmConstr constr) (Pattern.Var . fst <$> zip [0..] constrFields)+              , Bound.toScope $+                Expression.App "Json.Encode.object" $+                Expression.List $+                  Expression.tuple+                    (Expression.String (toS tagName))+                    (Expression.App "Json.Encode.string" $ Expression.String $ constructorJSONName constr)+                  :+                  [ Expression.tuple+                    (Expression.String (toS contentsName))+                    (encodeConstructorFields constrFields)+                  | not $ null constrFields+                  ]+              )+            | (constr, constrFields) <- constrs+            ]++        _ -> panic "Only the DataAeson.TaggedObject sumEncoding is currently supported"++    allNullary :: forall c f. [(c, [f])] -> Bool+    allNullary = all (null . snd)++-------------++instance HasElmType Int where+  elmType =+    "Basics.Int"++instance HasElmEncoder Aeson.Value Int where+  elmEncoder =+    "Json.Encode.int"++instance HasElmDecoder Aeson.Value Int where+  elmDecoder =+    "Json.Decode.int"++instance HasElmType Double where+  elmType =+    "Basics.Float"++instance HasElmEncoder Aeson.Value Double where+  elmEncoder =+    "Json.Encode.float"++instance HasElmDecoder Aeson.Value Double where+  elmDecoder =+    "Json.Decode.float"++instance HasElmType Bool where+  elmType =+    "Basics.Bool"++instance HasElmEncoder Aeson.Value Bool where+  elmEncoder =+    "Json.Encode.bool"++instance HasElmDecoder Aeson.Value Bool where+  elmDecoder =+    "Json.Decode.bool"++instance HasElmType Text where+  elmType =+    "String.String"++instance HasElmEncoder Text Text where+  elmEncoder =+    "Basics.identity"++instance HasElmDecoder Text Text where+  elmDecoder =+    "Basics.identity"++instance HasElmEncoder Text Char where+  elmEncoder =+    "String.fromChar"++instance HasElmEncoder Text Int where+  elmEncoder =+    "String.fromInt"++instance HasElmEncoder Text Double where+  elmEncoder =+    "String.fromFloat"++instance HasElmEncoder Aeson.Value Text where+  elmEncoder =+    "Json.Encode.string"++instance HasElmDecoder Aeson.Value Text where+  elmDecoder =+    "Json.Decode.string"++instance HasElmType Char where+  elmType =+    "Char.Char"++instance HasElmEncoder Aeson.Value Char where+  elmEncoder =+    "Json.Encode.string" Expression.<< "String.fromChar"++instance HasElmDecoder Aeson.Value Char where+  elmDecoder =+    "Json.Decode.string" Expression.|>+      Expression.App "Json.Decode.andThen"+      (Expression.Lam $ Bound.toScope $+        Expression.Case+          (Expression.App "String.uncons" $ Expression.Var $ Bound.B ())+          [ ( Pattern.Con "Maybe.Just" [Pattern.tuple (Pattern.Var 0) (Pattern.String "")]+            , Bound.toScope $ Expression.App "Json.Decode.succeed" $ Expression.Var $ Bound.B 0+            )+          , ( Pattern.Wildcard+            , Bound.toScope $ Expression.App "Json.Decode.fail" $ Expression.String "Not a char"+            )+          ]+      )++instance HasElmType UTCTime where+  elmType =+    "Time.Posix"++instance HasElmEncoder Aeson.Value UTCTime where+  elmEncoder =+    "Iso8601.encode"++instance HasElmDecoder Aeson.Value UTCTime where+  elmDecoder =+    "Iso8601.decoder"++instance HasElmEncoder a b => HasElmEncoder (Maybe a) (Maybe b) where+  elmEncoder = Expression.App "Maybe.map" (elmEncoder @a @b)++instance HasElmType a => HasElmType (Maybe a) where+  elmType =+    Type.App "Maybe.Maybe" (elmType @a)++instance HasElmEncoder Aeson.Value a => HasElmEncoder Aeson.Value (Maybe a) where+  elmEncoder =+    Expression.apps "Maybe.Extra.unwrap" ["Json.Encode.null", elmEncoder @Aeson.Value @a]++instance HasElmDecoder Aeson.Value a => HasElmDecoder Aeson.Value (Maybe a) where+  elmDecoder =+    Expression.App "Json.Decode.nullable" (elmDecoder @Aeson.Value @a)++instance HasElmType a => HasElmType [a] where+  elmType =+    Type.App "List.List" (elmType @a)++instance HasElmEncoder Aeson.Value a => HasElmEncoder Aeson.Value [a] where+  elmEncoder =+    Expression.App "Json.Encode.list" (elmEncoder @Aeson.Value @a)++instance HasElmDecoder Aeson.Value a => HasElmDecoder Aeson.Value [a] where+  elmDecoder =+    Expression.App "Json.Decode.list" (elmDecoder @Aeson.Value @a)++instance (HasElmType a, HasElmType b) => HasElmType (a, b) where+  elmType =+    Type.apps "Basics.," [elmType @a, elmType @b]++instance (HasElmEncoder Aeson.Value a, HasElmEncoder Aeson.Value b) => HasElmEncoder Aeson.Value (a, b) where+  elmEncoder =+    Expression.Lam $ Bound.toScope $+      Expression.Case (pure $ Bound.B ())+        [ ( Pattern.tuple (Pattern.Var 0) (Pattern.Var 1)+          , Bound.toScope $+            Expression.apps+              "Json.Encode.list"+              [ "Basics.identity"+              , Expression.List+                  [ Expression.App (elmEncoder @Aeson.Value @a) $ pure $ Bound.B 0+                  , Expression.App (elmEncoder @Aeson.Value @b) $ pure $ Bound.B 1+                  ]+              ]+          )+        ]++instance (HasElmDecoder Aeson.Value a, HasElmDecoder Aeson.Value b) => HasElmDecoder Aeson.Value (a, b) where+  elmDecoder =+    Expression.apps+      "Json.Decode.map2"+      [ "Tuple.pair"+      , Expression.apps "Json.Decode.index" [Expression.Int 0, elmDecoder @Aeson.Value @a]+      , Expression.apps "Json.Decode.index" [Expression.Int 1, elmDecoder @Aeson.Value @b]+      ]++-- | A shorthand for a list of the type definitions for+-- @'jsonDefinitions' \@MyType@ is a shorthand for creating a list of its+-- 'elmDefinition', @'elmEncoderDefinition' \@'Aeson.Value'@, and+-- @'elmDecoderDefinition' \@'Aeson.Value'@.+jsonDefinitions :: forall t. (HasElmDefinition t, HasElmEncoderDefinition Aeson.Value t, HasElmDecoderDefinition Aeson.Value t) => [Definition]+jsonDefinitions =+  [ elmDefinition @t+  , elmEncoderDefinition @Aeson.Value @t+  , elmDecoderDefinition @Aeson.Value @t+  ]
+ test/Spec.hs view
@@ -0,0 +1,2 @@+main :: IO ()+main = putStrLn "Test suite not yet implemented"