From b230b285a3b1f90a46d3404ab9d4fd26dd0fdc94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Hr=C4=8Dek?= Date: Mon, 20 Mar 2023 13:58:49 +0100 Subject: [PATCH 01/11] Allow overriding custom code generation settings --- src/Elm/Aeson.hs | 41 ++++++++++++++++++++++++------------- src/Elm/Generic.hs | 51 ++++++++++++++++++++++++++++++++-------------- 2 files changed, 63 insertions(+), 29 deletions(-) diff --git a/src/Elm/Aeson.hs b/src/Elm/Aeson.hs index 074bf20..ff260bd 100644 --- a/src/Elm/Aeson.hs +++ b/src/Elm/Aeson.hs @@ -19,12 +19,12 @@ module Elm.Aeson import Data.Aeson (FromJSON (..), GFromJSON, GToJSON, Options (..), ToJSON (..), Value, Zero, defaultOptions, genericParseJSON, genericToJSON) import Data.Aeson.Types (Parser) +import Data.Proxy (Proxy(Proxy)) import GHC.Generics (Generic, Rep) -import Type.Reflection (Typeable, typeRep) +import Type.Reflection (Typeable) -import Elm.Ast (TypeName (..)) -import Elm.Generic (Elm (..), GenericElmDefinition (..), HasLessThanEightUnnamedFields, - HasNoNamedSum, HasNoTypeVars, stripTypeNamePrefix) +import Elm.Generic (Elm (..), CodeGenSettings (..), GenericElmDefinition (..), HasLessThanEightUnnamedFields, + HasNoNamedSum, HasNoTypeVars, defaultCodeGenSettings) import qualified Data.Text as T import qualified GHC.Generics as Generic (from) @@ -88,7 +88,15 @@ elmStreetParseJson (Typeable a, Generic a, GFromJSON Zero (Rep a)) => Value -> Parser a -elmStreetParseJson = genericParseJSON (elmStreetJsonOptions @a) +elmStreetParseJson = elmStreetParseJsonSettings (defaultCodeGenSettings (Proxy :: Proxy a)) + +elmStreetParseJsonSettings + :: forall a . + (Generic a, GFromJSON Zero (Rep a)) + => CodeGenSettings + -> Value + -> Parser a +elmStreetParseJsonSettings settings = genericParseJSON (elmStreetJsonOptions settings) {- | Allows to create 'Data.Aeson.ToJSON' instance that strips the supported by @elm-street@ data type name prefix from every field. @@ -113,7 +121,15 @@ elmStreetToJson (Typeable a, Generic a, GToJSON Zero (Rep a)) => a -> Value -elmStreetToJson = genericToJSON (elmStreetJsonOptions @a) +elmStreetToJson = elmStreetToJsonSettings (defaultCodeGenSettings (Proxy :: Proxy a)) + +elmStreetToJsonSettings + :: forall a . + (Generic a, GToJSON Zero (Rep a)) + => CodeGenSettings + -> a + -> Value +elmStreetToJsonSettings settings = genericToJSON (elmStreetJsonOptions settings) {- | Options to strip type name from the field names. @@ -130,15 +146,11 @@ elmStreetToJson = genericToJSON (elmStreetJsonOptions @a) +----------------+----------------+---------------------+ -} -elmStreetJsonOptions :: forall a . Typeable a => Options -elmStreetJsonOptions = defaultOptions - { fieldLabelModifier = T.unpack . stripTypeNamePrefix typeName . T.pack +elmStreetJsonOptions :: CodeGenSettings -> Options +elmStreetJsonOptions settings = defaultOptions + { fieldLabelModifier = T.unpack . cgsFieldLabelModifier settings . T.pack , tagSingleConstructors = True } - where - typeName :: TypeName - typeName = TypeName $ T.pack $ show $ typeRep @a - {- | Newtype for reusing in @DerivingVia@. @@ -157,8 +169,9 @@ instance ( HasNoTypeVars a , HasNoNamedSum a , Generic a , GenericElmDefinition (Rep a) + , Typeable a ) => Elm (ElmStreet a) where - toElmDefinition _ = genericToElmDefinition + toElmDefinition _ = genericToElmDefinition (defaultCodeGenSettings (Proxy :: Proxy a)) $ Generic.from (error "Proxy for generic elm was evaluated" :: a) instance (Typeable a, Generic a, GToJSON Zero (Rep a)) => ToJSON (ElmStreet a) where diff --git a/src/Elm/Generic.hs b/src/Elm/Generic.hs index 224c577..2b19381 100644 --- a/src/Elm/Generic.hs +++ b/src/Elm/Generic.hs @@ -28,6 +28,9 @@ module Elm.Generic , GenericConstructor (..) , toElmConstructor + -- * Customizing generated elm code and JSON instances + , CodeGenSettings (..) + , defaultCodeGenSettings -- * Type families for compile-time checks , HasNoTypeVars @@ -54,6 +57,7 @@ import Data.Kind (Constraint, Type) import Data.List.NonEmpty (NonEmpty (..)) import Data.Proxy (Proxy (..)) import Data.Text (Text) +import Type.Reflection (Typeable, typeRep) import Data.Time.Clock (UTCTime) import Data.Type.Bool (If, type (||)) import Data.Void (Void) @@ -82,10 +86,11 @@ class Elm a where , HasNoNamedSum a , Generic a , GenericElmDefinition (Rep a) + , Typeable a ) => Proxy a -> ElmDefinition - toElmDefinition _ = genericToElmDefinition + toElmDefinition _ = genericToElmDefinition (defaultCodeGenSettings (Proxy :: Proxy a)) $ Generic.from (error "Proxy for generic elm was evaluated" :: a) {- | Returns 'TypeRef' for the existing type. This function always returns the @@ -182,10 +187,10 @@ data type like @data type name@. Then it collects all constructors of the data type and decides what to generate. -} class GenericElmDefinition (f :: k -> Type) where - genericToElmDefinition :: f a -> ElmDefinition + genericToElmDefinition :: CodeGenSettings -> f a -> ElmDefinition instance (Datatype d, GenericElmConstructors f) => GenericElmDefinition (D1 d f) where - genericToElmDefinition datatype = case genericToElmConstructors (TypeName typeName) (unM1 datatype) of + genericToElmDefinition settings datatype = case genericToElmConstructors settings (unM1 datatype) of c :| [] -> case toElmConstructor c of Left fields -> DefRecord $ ElmRecord typeName fields elmIsNewtype Right ctor -> DefType $ ElmType typeName [] elmIsNewtype (ctor :| []) @@ -232,34 +237,34 @@ toElmConstructor GenericConstructor{..} = case genericConstructorFields of {- | Typeclass to collect all constructors of the Haskell data type generically. -} class GenericElmConstructors (f :: k -> Type) where genericToElmConstructors - :: TypeName -- ^ Name of the data type; to be stripped + :: CodeGenSettings -> f a -- ^ Generic value -> NonEmpty GenericConstructor -- ^ List of the data type constructors -- | If it's a sum type then just combine constructors instance (GenericElmConstructors f, GenericElmConstructors g) => GenericElmConstructors (f :+: g) where - genericToElmConstructors name _ = - genericToElmConstructors name (error "'f :+:' is evaluated" :: f p) - <> genericToElmConstructors name (error "':+: g' is evaluated" :: g p) + genericToElmConstructors settings _ = + genericToElmConstructors settings (error "'f :+:' is evaluated" :: f p) + <> genericToElmConstructors settings (error "':+: g' is evaluated" :: g p) -- | Create singleton list for case of a one constructor. instance (Constructor c, GenericElmFields f) => GenericElmConstructors (C1 c f) where - genericToElmConstructors name constructor = pure $ GenericConstructor + genericToElmConstructors settings constructor = pure $ GenericConstructor (T.pack $ conName constructor) - (genericToElmFields name $ unM1 constructor) + (genericToElmFields settings $ unM1 constructor) -- | Collect all fields when inside constructor. class GenericElmFields (f :: k -> Type) where genericToElmFields - :: TypeName -- ^ Name of the data type; to be stripped + :: CodeGenSettings -> f a -- ^ Generic value -> [(TypeRef, Maybe Text)] -- | If multiple fields then just combine all results. instance (GenericElmFields f, GenericElmFields g) => GenericElmFields (f :*: g) where - genericToElmFields name _ = - genericToElmFields name (error "'f :*:' is evaluated" :: f p) - <> genericToElmFields name (error "':*: g' is evaluated" :: g p) + genericToElmFields settings _ = + genericToElmFields settings (error "'f :*:' is evaluated" :: f p) + <> genericToElmFields settings (error "':*: g' is evaluated" :: g p) -- | Constructor without fields. instance GenericElmFields U1 where @@ -267,9 +272,9 @@ instance GenericElmFields U1 where -- | Single constructor field. instance (Selector s, Elm a) => GenericElmFields (S1 s (Rec0 a)) where - genericToElmFields typeName selector = case selName selector of + genericToElmFields settings selector = case selName selector of "" -> [(elmRef @a, Nothing)] - name -> [(elmRef @a, Just $ stripTypeNamePrefix typeName $ T.pack name)] + name -> [(elmRef @a, Just $ cgsFieldLabelModifier settings $ T.pack name)] {- | Strips name of the type name from field name prefix. @@ -303,6 +308,22 @@ stripTypeNamePrefix (TypeName typeName) fieldName = leaveIfEmpty :: Text -> Text leaveIfEmpty rest = if T.null rest then fieldName else headToLower rest +-- | Settings allow for customizing generated Elm code as well as +-- ToJSON and FromJSON instances derived generically. +-- +-- Note that for Generated Elm encoders / decoders to be compatible +-- with ToJSON / FromJSON instances for given type, the same +-- CodeGenSettings should be used to generate Elm / ToJSON / FromJSON. +data CodeGenSettings = CodeGenSettings + { cgsFieldLabelModifier :: Text -> Text + } + +defaultCodeGenSettings :: forall a. Typeable a => Proxy a -> CodeGenSettings +defaultCodeGenSettings _ = CodeGenSettings (stripTypeNamePrefix typeName) + where + typeName :: TypeName + typeName = TypeName $ T.pack $ show $ typeRep @a + ---------------------------------------------------------------------------- -- ~Magic~ ---------------------------------------------------------------------------- From 71997a4afbd139bd1dd2f1caa15952af86f1da69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Hr=C4=8Dek?= Date: Mon, 20 Mar 2023 15:50:44 +0100 Subject: [PATCH 02/11] Add test and simplify constraints a bit --- src/Elm/Aeson.hs | 13 ++++-------- src/Elm/Generic.hs | 19 +++++++++++------- test/Test/Golden.hs | 33 +++++++++++++++++------------- types/Types.hs | 49 ++++++++++++++++++++++++++++++++++++++------- 4 files changed, 77 insertions(+), 37 deletions(-) diff --git a/src/Elm/Aeson.hs b/src/Elm/Aeson.hs index ff260bd..949e593 100644 --- a/src/Elm/Aeson.hs +++ b/src/Elm/Aeson.hs @@ -10,7 +10,9 @@ comply to @elm-street@ rules regarding names. module Elm.Aeson ( elmStreetParseJson + , elmStreetParseJsonSettings , elmStreetToJson + , elmStreetToJsonSettings , elmStreetJsonOptions , ElmStreet (..) @@ -23,8 +25,7 @@ import Data.Proxy (Proxy(Proxy)) import GHC.Generics (Generic, Rep) import Type.Reflection (Typeable) -import Elm.Generic (Elm (..), CodeGenSettings (..), GenericElmDefinition (..), HasLessThanEightUnnamedFields, - HasNoNamedSum, HasNoTypeVars, defaultCodeGenSettings) +import Elm.Generic (Elm (..), CodeGenSettings (..), GenericElmDefinition (..), ElmStreetGenericConstraints, defaultCodeGenSettings) import qualified Data.Text as T import qualified GHC.Generics as Generic (from) @@ -164,13 +165,7 @@ newtype ElmStreet a = ElmStreet { unElmStreet :: a } -instance ( HasNoTypeVars a - , HasLessThanEightUnnamedFields a - , HasNoNamedSum a - , Generic a - , GenericElmDefinition (Rep a) - , Typeable a - ) => Elm (ElmStreet a) where +instance (ElmStreetGenericConstraints a, Typeable a) => Elm (ElmStreet a) where toElmDefinition _ = genericToElmDefinition (defaultCodeGenSettings (Proxy :: Proxy a)) $ Generic.from (error "Proxy for generic elm was evaluated" :: a) diff --git a/src/Elm/Generic.hs b/src/Elm/Generic.hs index 2b19381..625f696 100644 --- a/src/Elm/Generic.hs +++ b/src/Elm/Generic.hs @@ -45,6 +45,7 @@ module Elm.Generic , NamedSumError , CheckNamedSum , CheckConst + , ElmStreetGenericConstraints -- * Internals , stripTypeNamePrefix @@ -81,13 +82,7 @@ class Elm a where toElmDefinition :: Proxy a -> ElmDefinition default toElmDefinition - :: ( HasNoTypeVars a - , HasLessThanEightUnnamedFields a - , HasNoNamedSum a - , Generic a - , GenericElmDefinition (Rep a) - , Typeable a - ) + :: (ElmStreetGenericConstraints a, Typeable a) => Proxy a -> ElmDefinition toElmDefinition _ = genericToElmDefinition (defaultCodeGenSettings (Proxy :: Proxy a)) @@ -405,3 +400,13 @@ type family NamedSumError (t :: k) :: ErrorMessage where NamedSumError t = 'Text "'elm-street' doesn't support Sum types with records." ':$$: 'Text "But '" ':<>: 'ShowType t ':<>: 'Text "' has records." + +-- | Convenience grouping of constraints that type has to satisfy +-- in order to be eligible for automatic derivation of Elm via generics +type ElmStreetGenericConstraints a = + (HasNoTypeVars a + , HasLessThanEightUnnamedFields a + , HasNoNamedSum a + , Generic a + , GenericElmDefinition (Rep a) + ) \ No newline at end of file diff --git a/test/Test/Golden.hs b/test/Test/Golden.hs index f3f9401..4c2ed66 100644 --- a/test/Test/Golden.hs +++ b/test/Test/Golden.hs @@ -1,20 +1,25 @@ -module Test.Golden - ( goldenSpec - ) where +module Test.Golden (goldenSpec) where -import Test.Hspec (Spec, describe, it, runIO, shouldBe) +import Test.Hspec (Spec, describe, it, shouldBe, shouldReturn) -import Types (OneType, defaultOneType) - -import Data.Aeson as A -import Data.ByteString.Lazy as LBS +import Types (CustomCodeGen, OneType, defaultCustomCodeGen, defaultOneType) +import qualified Data.Aeson as A +import qualified Data.ByteString.Lazy as LBS goldenSpec :: Spec goldenSpec = describe "golden tests" $ do - golden <- runIO $ LBS.readFile "test/golden/oneType.json" - - it "Golden JSON -> Haskell == default" $ - A.eitherDecode @OneType golden `shouldBe` Right defaultOneType - it "default -> JSON -> Haskell == default" $ - (A.eitherDecode @OneType $ A.encode defaultOneType) `shouldBe` Right defaultOneType + describe "Default CodeGenSettings" $ do + it "Golden JSON -> Haskell == default" $ + A.eitherDecode @OneType <$> LBS.readFile "test/golden/oneType.json" + `shouldReturn` Right defaultOneType + it "default -> JSON -> Haskell == default" $ + A.eitherDecode @OneType (A.encode defaultOneType) + `shouldBe` Right defaultOneType + describe "Custom CodeGenSettings" $ do + it "should decode type with custom CodeGenSettings" $ + A.eitherDecode @CustomCodeGen "{\"customFunTestInt\": 78,\"customFunTestString\": \"Hello\",\"tag\": \"CustomCodeGen\"}" + `shouldBe` Right defaultCustomCodeGen + it "should encode type with custom CodeGen" $ + A.eitherDecode @CustomCodeGen (A.encode defaultCustomCodeGen) + `shouldBe` Right defaultCustomCodeGen diff --git a/types/Types.hs b/types/Types.hs index 45057c5..ec5c344 100644 --- a/types/Types.hs +++ b/types/Types.hs @@ -1,7 +1,8 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveAnyClass #-} -{-# LANGUAGE DerivingVia #-} -{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DerivingVia #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE UndecidableInstances #-} {- | Haskell types used for testing `elm-street` generated Elm types. -} @@ -10,6 +11,7 @@ module Types ( Types , OneType (..) , defaultOneType + , defaultCustomCodeGen -- * All test types , Prims (..) @@ -22,17 +24,22 @@ module Types , User (..) , Guest (..) , UserRequest (..) + , CustomCodeGen (..) ) where -import Data.Aeson (FromJSON (..), ToJSON (..), Value(..), object, (.=)) +import Data.Aeson (FromJSON(..), ToJSON(..), Value(..), object, (.=), GFromJSON, GToJSON, Zero) import Data.List.NonEmpty (NonEmpty(..)) import Data.Text (Text) import Data.Time.Calendar (fromGregorian) import Data.Time.Clock (UTCTime (..)) import Data.Word (Word32) import Elm (Elm (..), ElmStreet (..), elmNewtype, elmStreetParseJson, elmStreetToJson) -import GHC.Generics (Generic) - +import Elm.Generic (CodeGenSettings (..), ElmStreetGenericConstraints, GenericElmDefinition(..)) +import Elm.Aeson (elmStreetParseJsonSettings, elmStreetToJsonSettings) +import GHC.Generics (Generic, Rep) +import Type.Reflection (Typeable) +import qualified GHC.Generics as Generic (from) +import qualified Data.Text as Text data Prims = Prims { primsUnit :: !() @@ -161,6 +168,28 @@ data OneType = OneType instance ToJSON OneType where toJSON = elmStreetToJson instance FromJSON OneType where parseJSON = elmStreetParseJson +data CustomCodeGen = CustomCodeGen + { customCodeGenString :: String + , customCodeGenInt :: Int + } deriving (Generic, Eq, Show) + deriving (Elm, FromJSON, ToJSON) via MyElm CustomCodeGen + +-- Settings which do some custom modifications of record filed names +customCodeGenSettings :: CodeGenSettings +customCodeGenSettings = CodeGenSettings (Text.replace "CodeGen" "FunTest") + +newtype MyElm a = MyElm {unMyElm :: a} + +instance ElmStreetGenericConstraints a => Elm (MyElm a) where + toElmDefinition _ = genericToElmDefinition customCodeGenSettings + $ Generic.from (error "Proxy for generic elm was evaluated" :: a) + +instance (Typeable a, Generic a, GToJSON Zero (Rep a)) => ToJSON (MyElm a) where + toJSON = elmStreetToJsonSettings customCodeGenSettings . unMyElm + +instance (Typeable a, Generic a, GFromJSON Zero (Rep a)) => FromJSON (MyElm a) where + parseJSON = fmap MyElm . elmStreetParseJsonSettings customCodeGenSettings + -- | Type level list of all test types. type Types = '[ Prims @@ -233,3 +262,9 @@ defaultOneType = OneType , userRequestLimit = 123 , userRequestExample = Just (Right Blocked) } + +defaultCustomCodeGen :: CustomCodeGen +defaultCustomCodeGen = CustomCodeGen + { customCodeGenString = "Hello" + , customCodeGenInt = 78 + } From c1eed478b939de95052d761a55223e81d20b3738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Hr=C4=8Dek?= Date: Mon, 20 Mar 2023 16:04:02 +0100 Subject: [PATCH 03/11] Remove redundant constraints, don't disable warnings --- src/Elm/Aeson.hs | 2 -- src/Elm/Generate.hs | 2 -- src/Elm/Generic.hs | 14 +++++++------- types/Types.hs | 8 ++++---- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/Elm/Aeson.hs b/src/Elm/Aeson.hs index 949e593..69d61c6 100644 --- a/src/Elm/Aeson.hs +++ b/src/Elm/Aeson.hs @@ -1,5 +1,3 @@ -{-# OPTIONS_GHC -fno-warn-redundant-constraints #-} - {-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE UndecidableInstances #-} diff --git a/src/Elm/Generate.hs b/src/Elm/Generate.hs index b645967..76272a4 100644 --- a/src/Elm/Generate.hs +++ b/src/Elm/Generate.hs @@ -1,5 +1,3 @@ -{-# OPTIONS_GHC -fno-warn-orphans #-} - {-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE GADTs #-} diff --git a/src/Elm/Generic.hs b/src/Elm/Generic.hs index 625f696..4e6e400 100644 --- a/src/Elm/Generic.hs +++ b/src/Elm/Generic.hs @@ -1,5 +1,3 @@ -{-# OPTIONS_GHC -fno-warn-redundant-constraints #-} - {-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DefaultSignatures #-} @@ -303,16 +301,18 @@ stripTypeNamePrefix (TypeName typeName) fieldName = leaveIfEmpty :: Text -> Text leaveIfEmpty rest = if T.null rest then fieldName else headToLower rest --- | Settings allow for customizing generated Elm code as well as +-- | CodeGenSettings allow for customizing generated Elm code as well as -- ToJSON and FromJSON instances derived generically. -- -- Note that for Generated Elm encoders / decoders to be compatible -- with ToJSON / FromJSON instances for given type, the same -- CodeGenSettings should be used to generate Elm / ToJSON / FromJSON. -data CodeGenSettings = CodeGenSettings +newtype CodeGenSettings = CodeGenSettings { cgsFieldLabelModifier :: Text -> Text } +-- | Default settings modify record field names by stripping type name prefix +-- (if present) defaultCodeGenSettings :: forall a. Typeable a => Proxy a -> CodeGenSettings defaultCodeGenSettings _ = CodeGenSettings (stripTypeNamePrefix typeName) where @@ -402,11 +402,11 @@ type family NamedSumError (t :: k) :: ErrorMessage where ':$$: 'Text "But '" ':<>: 'ShowType t ':<>: 'Text "' has records." -- | Convenience grouping of constraints that type has to satisfy --- in order to be eligible for automatic derivation of Elm via generics +-- in order to be eligible for automatic derivation of Elm instance via generics type ElmStreetGenericConstraints a = - (HasNoTypeVars a + ( HasNoTypeVars a , HasLessThanEightUnnamedFields a , HasNoNamedSum a , Generic a , GenericElmDefinition (Rep a) - ) \ No newline at end of file + ) diff --git a/types/Types.hs b/types/Types.hs index ec5c344..7fc4034 100644 --- a/types/Types.hs +++ b/types/Types.hs @@ -37,7 +37,7 @@ import Elm (Elm (..), ElmStreet (..), elmNewtype, elmStreetParseJson, elmStreetT import Elm.Generic (CodeGenSettings (..), ElmStreetGenericConstraints, GenericElmDefinition(..)) import Elm.Aeson (elmStreetParseJsonSettings, elmStreetToJsonSettings) import GHC.Generics (Generic, Rep) -import Type.Reflection (Typeable) + import qualified GHC.Generics as Generic (from) import qualified Data.Text as Text @@ -171,7 +171,7 @@ instance FromJSON OneType where parseJSON = elmStreetParseJson data CustomCodeGen = CustomCodeGen { customCodeGenString :: String , customCodeGenInt :: Int - } deriving (Generic, Eq, Show) + } deriving stock (Generic, Eq, Show) deriving (Elm, FromJSON, ToJSON) via MyElm CustomCodeGen -- Settings which do some custom modifications of record filed names @@ -184,10 +184,10 @@ instance ElmStreetGenericConstraints a => Elm (MyElm a) where toElmDefinition _ = genericToElmDefinition customCodeGenSettings $ Generic.from (error "Proxy for generic elm was evaluated" :: a) -instance (Typeable a, Generic a, GToJSON Zero (Rep a)) => ToJSON (MyElm a) where +instance (Generic a, GToJSON Zero (Rep a)) => ToJSON (MyElm a) where toJSON = elmStreetToJsonSettings customCodeGenSettings . unMyElm -instance (Typeable a, Generic a, GFromJSON Zero (Rep a)) => FromJSON (MyElm a) where +instance (Generic a, GFromJSON Zero (Rep a)) => FromJSON (MyElm a) where parseJSON = fmap MyElm . elmStreetParseJsonSettings customCodeGenSettings -- | Type level list of all test types. From 7b7f00894e7f028de6635879bb59184b1ff447fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Hr=C4=8Dek?= Date: Mon, 20 Mar 2023 16:19:46 +0100 Subject: [PATCH 04/11] Use TypeApplications intead of Proxy --- src/Elm/Aeson.hs | 7 +++---- src/Elm/Generic.hs | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Elm/Aeson.hs b/src/Elm/Aeson.hs index 69d61c6..9322836 100644 --- a/src/Elm/Aeson.hs +++ b/src/Elm/Aeson.hs @@ -19,7 +19,6 @@ module Elm.Aeson import Data.Aeson (FromJSON (..), GFromJSON, GToJSON, Options (..), ToJSON (..), Value, Zero, defaultOptions, genericParseJSON, genericToJSON) import Data.Aeson.Types (Parser) -import Data.Proxy (Proxy(Proxy)) import GHC.Generics (Generic, Rep) import Type.Reflection (Typeable) @@ -87,7 +86,7 @@ elmStreetParseJson (Typeable a, Generic a, GFromJSON Zero (Rep a)) => Value -> Parser a -elmStreetParseJson = elmStreetParseJsonSettings (defaultCodeGenSettings (Proxy :: Proxy a)) +elmStreetParseJson = elmStreetParseJsonSettings (defaultCodeGenSettings @a) elmStreetParseJsonSettings :: forall a . @@ -120,7 +119,7 @@ elmStreetToJson (Typeable a, Generic a, GToJSON Zero (Rep a)) => a -> Value -elmStreetToJson = elmStreetToJsonSettings (defaultCodeGenSettings (Proxy :: Proxy a)) +elmStreetToJson = elmStreetToJsonSettings (defaultCodeGenSettings @a) elmStreetToJsonSettings :: forall a . @@ -164,7 +163,7 @@ newtype ElmStreet a = ElmStreet } instance (ElmStreetGenericConstraints a, Typeable a) => Elm (ElmStreet a) where - toElmDefinition _ = genericToElmDefinition (defaultCodeGenSettings (Proxy :: Proxy a)) + toElmDefinition _ = genericToElmDefinition (defaultCodeGenSettings @a) $ Generic.from (error "Proxy for generic elm was evaluated" :: a) instance (Typeable a, Generic a, GToJSON Zero (Rep a)) => ToJSON (ElmStreet a) where diff --git a/src/Elm/Generic.hs b/src/Elm/Generic.hs index 4e6e400..f12c960 100644 --- a/src/Elm/Generic.hs +++ b/src/Elm/Generic.hs @@ -83,7 +83,7 @@ class Elm a where :: (ElmStreetGenericConstraints a, Typeable a) => Proxy a -> ElmDefinition - toElmDefinition _ = genericToElmDefinition (defaultCodeGenSettings (Proxy :: Proxy a)) + toElmDefinition _ = genericToElmDefinition (defaultCodeGenSettings @a) $ Generic.from (error "Proxy for generic elm was evaluated" :: a) {- | Returns 'TypeRef' for the existing type. This function always returns the @@ -313,8 +313,8 @@ newtype CodeGenSettings = CodeGenSettings -- | Default settings modify record field names by stripping type name prefix -- (if present) -defaultCodeGenSettings :: forall a. Typeable a => Proxy a -> CodeGenSettings -defaultCodeGenSettings _ = CodeGenSettings (stripTypeNamePrefix typeName) +defaultCodeGenSettings :: forall a. Typeable a => CodeGenSettings +defaultCodeGenSettings = CodeGenSettings (stripTypeNamePrefix typeName) where typeName :: TypeName typeName = TypeName $ T.pack $ show $ typeRep @a From 16f880474b7894179642eba159ff301d16a946ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Hr=C4=8Dek?= Date: Sat, 25 Mar 2023 09:16:17 +0100 Subject: [PATCH 05/11] Rename Settings to options, update docs --- README.md | 11 ++--- src/Elm/Aeson.hs | 78 +++++++++++++---------------- src/Elm/Generic.hs | 116 +++++++++++++++++++++++++++++++++----------- test/Test/Golden.hs | 6 +-- types/Types.hs | 26 +++++----- 5 files changed, 143 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 03693af..eb9a91e 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,8 @@ Crossing the road between Haskell and Elm. ## What is this library about? -`Elm-street` allows you to generate automatically derived from Haskell types -definitions of Elm data types, JSON encoders and decoders. This helps to avoid -writing and maintaining huge chunk of boilerplate code when developing full-stack +`elm-street` allows you to automatically generate definitions of Elm data types and compatible JSON encoders and decoders + from Haskell types. This helps to avoid writing and maintaining huge chunk of boilerplate code when developing full-stack applications. ## Getting started @@ -56,13 +55,13 @@ In order to use `elm-street` features, you need to perform the following steps: > **NOTE:** This requires extension `-XDataKinds`. 4. Use `generateElm` function to output definitions to specified directory under specified module prefix. - ``` + ```haskell main :: IO () main = generateElm @Types $ defaultSettings "frontend/src" ["Core", "Generated"] ``` > **NOTE:** This requires extension `-XTypeApplications`. - The above command when called generates the following files: + When executed, the above program generates the following files: + `frontend/src/Core/Generated/Types.elm`: `Core.Generated.Types` module with the definitions of all types + `frontend/src/Core/Generated/Encoder.elm`: `Core.Generated.Encoder` module with the JSON encoders for the types @@ -102,7 +101,7 @@ limitations, specifically: } ``` 2. Data types with type variables are not supported (see [issue #45](https://github.com/Holmusk/elm-street/issues/45) for more details). - Though, if type variables are phantom, you still can implement `Elm` instance which + Though, if type variables are phantom, you can still implement `Elm` instance which will generate valid Elm defintions. Here is how you can create `Elm` instance for `newtype`s with phantom type variables: ```haskell diff --git a/src/Elm/Aeson.hs b/src/Elm/Aeson.hs index 9322836..2e668b4 100644 --- a/src/Elm/Aeson.hs +++ b/src/Elm/Aeson.hs @@ -8,9 +8,9 @@ comply to @elm-street@ rules regarding names. module Elm.Aeson ( elmStreetParseJson - , elmStreetParseJsonSettings + , elmStreetParseJsonWith , elmStreetToJson - , elmStreetToJsonSettings + , elmStreetToJsonWith , elmStreetJsonOptions , ElmStreet (..) @@ -22,18 +22,18 @@ import Data.Aeson.Types (Parser) import GHC.Generics (Generic, Rep) import Type.Reflection (Typeable) -import Elm.Generic (Elm (..), CodeGenSettings (..), GenericElmDefinition (..), ElmStreetGenericConstraints, defaultCodeGenSettings) +import Elm.Generic (Elm (..), CodeGenOptions (..), GenericElmDefinition (..), ElmStreetGenericConstraints, defaultCodeGenOptions) import qualified Data.Text as T import qualified GHC.Generics as Generic (from) -{- | Allows to create 'Data.Aeson.FromJSON' instance that strips the supported -by @elm-street@ data type name prefix from every field.. +{- | Allows to create 'Data.Aeson.FromJSON' instance for data types supported by +@elm-street@. Strips data type name prefix from every field. __Example:__ -With the following @JSON@ +The following @JSON@ @ { \"name\": \"John\" @@ -41,16 +41,16 @@ With the following @JSON@ } @ -it is decoded it the following way for each of the specified types: +is decoded in the following way for each of the specified types: +-------------------------------+--------------------------+ | Haskell data type | Parsed type | +===============================+==========================+ | @ | @ | | data User = User | User | -| \ { userName :: String | { userName = \"John\" | -| \ , userAge :: Int | , userAge = 42 | -| \ } | } | +| { userName :: String | { userName = \"John\" | +| , userAge :: Int | , userAge = 42 | +| } | } | | @ | @ | +-------------------------------+--------------------------+ | | | @@ -71,13 +71,13 @@ it is decoded it the following way for each of the specified types: >>> data User = User { userName :: String, userAge :: Int } deriving (Generic, Show) >>> instance FromJSON User where parseJSON = elmStreetParseJson ->>> decode @User "{ \"name\": \"John\", \"age\": 42 }" +>>> decode @User "{\"age\":42,\"name\":\"John\",\"tag\":\"User\"}" Just (User {userName = "John", userAge = 42}) >>> data VeryLongType = VeryLongType { vltName :: String, vltAge :: Int } deriving (Generic, Show) >>> instance FromJSON VeryLongType where parseJSON = elmStreetParseJson ->>> decode @VeryLongType "{ \"name\": \"John\", \"age\": 42 }" +>>> decode @VeryLongType "{\"age\":42,\"name\":\"John\",\"tag\":\"VeryLongType\"}" Just (VeryLongType {vltName = "John", vltAge = 42}) -} @@ -86,67 +86,57 @@ elmStreetParseJson (Typeable a, Generic a, GFromJSON Zero (Rep a)) => Value -> Parser a -elmStreetParseJson = elmStreetParseJsonSettings (defaultCodeGenSettings @a) +elmStreetParseJson = elmStreetParseJsonWith (defaultCodeGenOptions @a) -elmStreetParseJsonSettings +{- | Use custom 'CodeGenOptions' to customize the behavior of derived FromJSON instance. +-} +elmStreetParseJsonWith :: forall a . (Generic a, GFromJSON Zero (Rep a)) - => CodeGenSettings + => CodeGenOptions -> Value -> Parser a -elmStreetParseJsonSettings settings = genericParseJSON (elmStreetJsonOptions settings) +elmStreetParseJsonWith options = genericParseJSON (elmStreetJsonOptions options) -{- | Allows to create 'Data.Aeson.ToJSON' instance that strips the supported by -@elm-street@ data type name prefix from every field. +{- | Allows to create 'Data.Aeson.ToJSON' instance for types supported by @elm-street@. +Strips type name prefix from every record field. >>> data User = User { userName :: String, userAge :: Int } deriving (Generic, Show) >>> instance ToJSON User where toJSON = elmStreetToJson >>> encode $ User { userName = "John", userAge = 42 } -"{\"age\":42,\"name\":\"John\"}" +"{\"age\":42,\"name\":\"John\",\"tag\":\"User\"}" >>> data VeryLongType = VeryLongType { vltName :: String, vltAge :: Int } deriving (Generic, Show) >>> instance ToJSON VeryLongType where toJSON = elmStreetToJson >>> encode $ VeryLongType {vltName = "John", vltAge = 42} -"{\"age\":42,\"name\":\"John\"}" +"{\"age\":42,\"name\":\"John\",\"tag\":\"VeryLongType\"}" >>> data User = User { name :: String, age :: Int } deriving (Generic, Show) >>> instance ToJSON User where toJSON = elmStreetToJson >>> encode $ User { name = "John", age = 42 } -"{\"age\":42,\"name\":\"John\"}" +"{\"age\":42,\"name\":\"John\",\"tag\":\"User\"}" -} elmStreetToJson :: forall a . (Typeable a, Generic a, GToJSON Zero (Rep a)) => a -> Value -elmStreetToJson = elmStreetToJsonSettings (defaultCodeGenSettings @a) +elmStreetToJson = elmStreetToJsonWith (defaultCodeGenOptions @a) -elmStreetToJsonSettings +{- | Use custom 'CodeGenOptions' to customize the behavior of derived ToJSON instance. +-} +elmStreetToJsonWith :: forall a . (Generic a, GToJSON Zero (Rep a)) - => CodeGenSettings + => CodeGenOptions -> a -> Value -elmStreetToJsonSettings settings = genericToJSON (elmStreetJsonOptions settings) - -{- | Options to strip type name from the field names. - -+----------------+----------------+---------------------+ -| Data type name | Field name | Stripped field name | -+================+================+=====================+ -| @User@ | @userName@ | @name@ | -+----------------+----------------+---------------------+ -| @AaaBbbCcc@ | @abcFieldName@ | @fieldName@ | -+----------------+----------------+---------------------+ -| @Foo@ | @field@ | @field@ | -+----------------+----------------+---------------------+ -| @Field@ | @field@ | @field@ | -+----------------+----------------+---------------------+ +elmStreetToJsonWith settings = genericToJSON (elmStreetJsonOptions settings) --} -elmStreetJsonOptions :: CodeGenSettings -> Options -elmStreetJsonOptions settings = defaultOptions - { fieldLabelModifier = T.unpack . cgsFieldLabelModifier settings . T.pack +-- | Build @elm-street@ compatible 'Data.Aeson.Options' from 'CodeGenOptions'. +elmStreetJsonOptions :: CodeGenOptions -> Options +elmStreetJsonOptions options = defaultOptions + { fieldLabelModifier = T.unpack . cgoFieldLabelModifier options . T.pack , tagSingleConstructors = True } @@ -163,7 +153,7 @@ newtype ElmStreet a = ElmStreet } instance (ElmStreetGenericConstraints a, Typeable a) => Elm (ElmStreet a) where - toElmDefinition _ = genericToElmDefinition (defaultCodeGenSettings @a) + toElmDefinition _ = genericToElmDefinition (defaultCodeGenOptions @a) $ Generic.from (error "Proxy for generic elm was evaluated" :: a) instance (Typeable a, Generic a, GToJSON Zero (Rep a)) => ToJSON (ElmStreet a) where diff --git a/src/Elm/Generic.hs b/src/Elm/Generic.hs index f12c960..1f274e5 100644 --- a/src/Elm/Generic.hs +++ b/src/Elm/Generic.hs @@ -27,8 +27,8 @@ module Elm.Generic , GenericConstructor (..) , toElmConstructor -- * Customizing generated elm code and JSON instances - , CodeGenSettings (..) - , defaultCodeGenSettings + , CodeGenOptions (..) + , defaultCodeGenOptions -- * Type families for compile-time checks , HasNoTypeVars @@ -83,7 +83,7 @@ class Elm a where :: (ElmStreetGenericConstraints a, Typeable a) => Proxy a -> ElmDefinition - toElmDefinition _ = genericToElmDefinition (defaultCodeGenSettings @a) + toElmDefinition _ = genericToElmDefinition (defaultCodeGenOptions @a) $ Generic.from (error "Proxy for generic elm was evaluated" :: a) {- | Returns 'TypeRef' for the existing type. This function always returns the @@ -180,10 +180,10 @@ data type like @data type name@. Then it collects all constructors of the data type and decides what to generate. -} class GenericElmDefinition (f :: k -> Type) where - genericToElmDefinition :: CodeGenSettings -> f a -> ElmDefinition + genericToElmDefinition :: CodeGenOptions -> f a -> ElmDefinition instance (Datatype d, GenericElmConstructors f) => GenericElmDefinition (D1 d f) where - genericToElmDefinition settings datatype = case genericToElmConstructors settings (unM1 datatype) of + genericToElmDefinition options datatype = case genericToElmConstructors options (unM1 datatype) of c :| [] -> case toElmConstructor c of Left fields -> DefRecord $ ElmRecord typeName fields elmIsNewtype Right ctor -> DefType $ ElmType typeName [] elmIsNewtype (ctor :| []) @@ -230,34 +230,34 @@ toElmConstructor GenericConstructor{..} = case genericConstructorFields of {- | Typeclass to collect all constructors of the Haskell data type generically. -} class GenericElmConstructors (f :: k -> Type) where genericToElmConstructors - :: CodeGenSettings + :: CodeGenOptions -> f a -- ^ Generic value -> NonEmpty GenericConstructor -- ^ List of the data type constructors -- | If it's a sum type then just combine constructors instance (GenericElmConstructors f, GenericElmConstructors g) => GenericElmConstructors (f :+: g) where - genericToElmConstructors settings _ = - genericToElmConstructors settings (error "'f :+:' is evaluated" :: f p) - <> genericToElmConstructors settings (error "':+: g' is evaluated" :: g p) + genericToElmConstructors options _ = + genericToElmConstructors options (error "'f :+:' is evaluated" :: f p) + <> genericToElmConstructors options (error "':+: g' is evaluated" :: g p) -- | Create singleton list for case of a one constructor. instance (Constructor c, GenericElmFields f) => GenericElmConstructors (C1 c f) where - genericToElmConstructors settings constructor = pure $ GenericConstructor + genericToElmConstructors options constructor = pure $ GenericConstructor (T.pack $ conName constructor) - (genericToElmFields settings $ unM1 constructor) + (genericToElmFields options $ unM1 constructor) -- | Collect all fields when inside constructor. class GenericElmFields (f :: k -> Type) where genericToElmFields - :: CodeGenSettings + :: CodeGenOptions -> f a -- ^ Generic value -> [(TypeRef, Maybe Text)] -- | If multiple fields then just combine all results. instance (GenericElmFields f, GenericElmFields g) => GenericElmFields (f :*: g) where - genericToElmFields settings _ = - genericToElmFields settings (error "'f :*:' is evaluated" :: f p) - <> genericToElmFields settings (error "':*: g' is evaluated" :: g p) + genericToElmFields options _ = + genericToElmFields options (error "'f :*:' is evaluated" :: f p) + <> genericToElmFields options (error "':*: g' is evaluated" :: g p) -- | Constructor without fields. instance GenericElmFields U1 where @@ -265,9 +265,9 @@ instance GenericElmFields U1 where -- | Single constructor field. instance (Selector s, Elm a) => GenericElmFields (S1 s (Rec0 a)) where - genericToElmFields settings selector = case selName selector of + genericToElmFields options selector = case selName selector of "" -> [(elmRef @a, Nothing)] - name -> [(elmRef @a, Just $ cgsFieldLabelModifier settings $ T.pack name)] + name -> [(elmRef @a, Just $ cgoFieldLabelModifier options $ T.pack name)] {- | Strips name of the type name from field name prefix. @@ -301,20 +301,78 @@ stripTypeNamePrefix (TypeName typeName) fieldName = leaveIfEmpty :: Text -> Text leaveIfEmpty rest = if T.null rest then fieldName else headToLower rest --- | CodeGenSettings allow for customizing generated Elm code as well as --- ToJSON and FromJSON instances derived generically. --- --- Note that for Generated Elm encoders / decoders to be compatible --- with ToJSON / FromJSON instances for given type, the same --- CodeGenSettings should be used to generate Elm / ToJSON / FromJSON. -newtype CodeGenSettings = CodeGenSettings - { cgsFieldLabelModifier :: Text -> Text +{- | CodeGenOptions allow for customizing some aspects of generated Elm code as well as + ToJSON and FromJSON instances derived generically. + +They can be passed to 'elmStreetParseJsonWith', 'elmStreetToJsonWith' and 'genericToElmDefinition' +to influence the behavior of FromJSON \/ ToJSON and Elm instances respectively. + +Note that for Generated Elm encoders \/ decoders to be compatible +with ToJSON \/ FromJSON instances for given type, the same +CodeGenOptions must be used in Elm \/ ToJSON \/ FromJSON instance declarations. + +Example: Say you don't like the default behavior (stripping type name prefix from all record fields) +and you would like to keep all record field names unmodified instead. +You can achieve that by declaring custom options: + +@ +myCodeGenOptions :: CodeGenOptions +myCodeGenOptions = CodeGenOptions { cgoFieldLabelModifier = id } +@ + +And then pass these options when defining Elm \/ ToJSON \/ FromJSON instances. +It is recommended to use DerivingVia to reduce the amount of boilerplate needed. +First declare a newtype whose Elm \/ ToJSON \/ FromJSON instances use your custom CodeGenOptions: + +@ +newtype CustomElm a = CustomElm {unCustomElm :: a} + +instance ElmStreetGenericConstraints a => Elm (CustomElm a) where + toElmDefinition _ = genericToElmDefinition myCodeGenOptions $ + GHC.Generics.from (error "Proxy for generic elm was evaluated" :: a) + +instance (Generic a, GToJSON Zero (Rep a)) => ToJSON (CustomElm a) where + toJSON = elmStreetToJsonWith myCodeGenOptions . unCustomElm + +instance (Generic a, GFromJSON Zero (Rep a)) => FromJSON (CustomElm a) where + parseJSON = fmap CustomElm . elmStreetParseJsonWith myCodeGenOptions +@ + +Then derive Elm \/ ToJSON \/ FromJSON instance via that newtype: + +@ +data MyType = MyType + { myTypeFieldOne :: String + , myTypeFieldTwo :: Int + } deriving stock (Show, Generic) + deriving (Elm, ToJSON, FromJSON) via CustomElm MyType +@ + +We can check that type name prefix is no longer stripped from record field names: +>>> encode (MyType "Hello" 10) +"{\"myTypeFieldOne\":\"Hello\",\"myTypeFieldTwo\":10,\"tag\":\"MyType\"}" +-} +newtype CodeGenOptions = CodeGenOptions + { cgoFieldLabelModifier :: Text -> Text -- ^ Function that modifies record field names (e.g. by dropping type name prefix) } --- | Default settings modify record field names by stripping type name prefix --- (if present) -defaultCodeGenSettings :: forall a. Typeable a => CodeGenSettings -defaultCodeGenSettings = CodeGenSettings (stripTypeNamePrefix typeName) +{- | Options to strip type name from the field names. + ++----------------+----------------+---------------------+ +| Data type name | Field name | Stripped field name | ++================+================+=====================+ +| @User@ | @userName@ | @name@ | ++----------------+----------------+---------------------+ +| @AaaBbbCcc@ | @abcFieldName@ | @fieldName@ | ++----------------+----------------+---------------------+ +| @Foo@ | @field@ | @field@ | ++----------------+----------------+---------------------+ +| @Field@ | @field@ | @field@ | ++----------------+----------------+---------------------+ + +-} +defaultCodeGenOptions :: forall a. Typeable a => CodeGenOptions +defaultCodeGenOptions = CodeGenOptions (stripTypeNamePrefix typeName) where typeName :: TypeName typeName = TypeName $ T.pack $ show $ typeRep @a diff --git a/test/Test/Golden.hs b/test/Test/Golden.hs index 4c2ed66..be9f225 100644 --- a/test/Test/Golden.hs +++ b/test/Test/Golden.hs @@ -9,15 +9,15 @@ import qualified Data.ByteString.Lazy as LBS goldenSpec :: Spec goldenSpec = describe "golden tests" $ do - describe "Default CodeGenSettings" $ do + describe "Default CodeGenOptions" $ do it "Golden JSON -> Haskell == default" $ A.eitherDecode @OneType <$> LBS.readFile "test/golden/oneType.json" `shouldReturn` Right defaultOneType it "default -> JSON -> Haskell == default" $ A.eitherDecode @OneType (A.encode defaultOneType) `shouldBe` Right defaultOneType - describe "Custom CodeGenSettings" $ do - it "should decode type with custom CodeGenSettings" $ + describe "Custom CodeGenOptions" $ do + it "should decode type with custom CodeGenOptions" $ A.eitherDecode @CustomCodeGen "{\"customFunTestInt\": 78,\"customFunTestString\": \"Hello\",\"tag\": \"CustomCodeGen\"}" `shouldBe` Right defaultCustomCodeGen it "should encode type with custom CodeGen" $ diff --git a/types/Types.hs b/types/Types.hs index 7fc4034..890e4a1 100644 --- a/types/Types.hs +++ b/types/Types.hs @@ -34,8 +34,8 @@ import Data.Time.Calendar (fromGregorian) import Data.Time.Clock (UTCTime (..)) import Data.Word (Word32) import Elm (Elm (..), ElmStreet (..), elmNewtype, elmStreetParseJson, elmStreetToJson) -import Elm.Generic (CodeGenSettings (..), ElmStreetGenericConstraints, GenericElmDefinition(..)) -import Elm.Aeson (elmStreetParseJsonSettings, elmStreetToJsonSettings) +import Elm.Generic (CodeGenOptions (..), ElmStreetGenericConstraints, GenericElmDefinition(..)) +import Elm.Aeson (elmStreetParseJsonWith, elmStreetToJsonWith) import GHC.Generics (Generic, Rep) import qualified GHC.Generics as Generic (from) @@ -172,23 +172,25 @@ data CustomCodeGen = CustomCodeGen { customCodeGenString :: String , customCodeGenInt :: Int } deriving stock (Generic, Eq, Show) - deriving (Elm, FromJSON, ToJSON) via MyElm CustomCodeGen + deriving (Elm, FromJSON, ToJSON) via CustomElm CustomCodeGen -- Settings which do some custom modifications of record filed names -customCodeGenSettings :: CodeGenSettings -customCodeGenSettings = CodeGenSettings (Text.replace "CodeGen" "FunTest") +customCodeGenOptions :: CodeGenOptions +customCodeGenOptions = CodeGenOptions (Text.replace "CodeGen" "FunTest") -newtype MyElm a = MyElm {unMyElm :: a} -instance ElmStreetGenericConstraints a => Elm (MyElm a) where - toElmDefinition _ = genericToElmDefinition customCodeGenSettings +-- Newtype whose Elm/ToJSON/FromJSON instance use custom CodeGenOptions +newtype CustomElm a = CustomElm {unCustomElm :: a} + +instance ElmStreetGenericConstraints a => Elm (CustomElm a) where + toElmDefinition _ = genericToElmDefinition customCodeGenOptions $ Generic.from (error "Proxy for generic elm was evaluated" :: a) -instance (Generic a, GToJSON Zero (Rep a)) => ToJSON (MyElm a) where - toJSON = elmStreetToJsonSettings customCodeGenSettings . unMyElm +instance (Generic a, GToJSON Zero (Rep a)) => ToJSON (CustomElm a) where + toJSON = elmStreetToJsonWith customCodeGenOptions . unCustomElm -instance (Generic a, GFromJSON Zero (Rep a)) => FromJSON (MyElm a) where - parseJSON = fmap MyElm . elmStreetParseJsonSettings customCodeGenSettings +instance (Generic a, GFromJSON Zero (Rep a)) => FromJSON (CustomElm a) where + parseJSON = fmap CustomElm . elmStreetParseJsonWith customCodeGenOptions -- | Type level list of all test types. type Types = From 29005c513cc892072b7964c560d16c32d53ed4e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Hr=C4=8Dek?= Date: Sat, 25 Mar 2023 09:24:31 +0100 Subject: [PATCH 06/11] Fix elm-tests --- frontend/src/Core/Decoder.elm | 1 + frontend/src/Core/Encoder.elm | 1 + frontend/src/Core/Types.elm | 1 + frontend/tests/Tests.elm | 1 + frontend/tests/Tests/Golden.elm | 2 +- 5 files changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/Core/Decoder.elm b/frontend/src/Core/Decoder.elm index 828db7d..ca8bdbe 100644 --- a/frontend/src/Core/Decoder.elm +++ b/frontend/src/Core/Decoder.elm @@ -16,6 +16,7 @@ decodePrims = D.succeed T.Prims |> required "int" D.int |> required "float" D.float |> required "text" D.string + |> required "string" D.string |> required "time" Iso.decoder |> required "value" D.value |> required "maybe" (nullable D.int) diff --git a/frontend/src/Core/Encoder.elm b/frontend/src/Core/Encoder.elm index 78a161f..ace7983 100644 --- a/frontend/src/Core/Encoder.elm +++ b/frontend/src/Core/Encoder.elm @@ -16,6 +16,7 @@ encodePrims x = E.object , ("int", E.int x.int) , ("float", E.float x.float) , ("text", E.string x.text) + , ("string", E.string x.string) , ("time", Iso.encode x.time) , ("value", Basics.identity x.value) , ("maybe", (elmStreetEncodeMaybe E.int) x.maybe) diff --git a/frontend/src/Core/Types.elm b/frontend/src/Core/Types.elm index 322805d..5ba8f5c 100644 --- a/frontend/src/Core/Types.elm +++ b/frontend/src/Core/Types.elm @@ -11,6 +11,7 @@ type alias Prims = , int : Int , float : Float , text : String + , string : String , time : Posix , value : Value , maybe : Maybe Int diff --git a/frontend/tests/Tests.elm b/frontend/tests/Tests.elm index bd1dcfa..ac4e0d6 100644 --- a/frontend/tests/Tests.elm +++ b/frontend/tests/Tests.elm @@ -47,6 +47,7 @@ defaultOneType = , int = 42 , float = 36.6 , text = "heh" + , string = "bye" , value = E.object [ ("nullField", E.null) , ("boolField", E.bool True) diff --git a/frontend/tests/Tests/Golden.elm b/frontend/tests/Tests/Golden.elm index 2267b0d..2abed68 100644 --- a/frontend/tests/Tests/Golden.elm +++ b/frontend/tests/Tests/Golden.elm @@ -43,7 +43,7 @@ goldenOneTypeJson = "objectField": {}, "arrayField": [1,2,3], "nullField": null - } + } }, "myUnit": { "tag": "MyUnit", From 617ef8a91b64254bae3e76c24c0cfd6d127ca3dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Hr=C4=8Dek?= Date: Sat, 25 Mar 2023 09:28:33 +0100 Subject: [PATCH 07/11] rename settings -> options, fix haddocks --- src/Elm/Aeson.hs | 2 +- src/Elm/Generic.hs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Elm/Aeson.hs b/src/Elm/Aeson.hs index 2e668b4..ecc377f 100644 --- a/src/Elm/Aeson.hs +++ b/src/Elm/Aeson.hs @@ -131,7 +131,7 @@ elmStreetToJsonWith => CodeGenOptions -> a -> Value -elmStreetToJsonWith settings = genericToJSON (elmStreetJsonOptions settings) +elmStreetToJsonWith options = genericToJSON (elmStreetJsonOptions options) -- | Build @elm-street@ compatible 'Data.Aeson.Options' from 'CodeGenOptions'. elmStreetJsonOptions :: CodeGenOptions -> Options diff --git a/src/Elm/Generic.hs b/src/Elm/Generic.hs index 1f274e5..298131c 100644 --- a/src/Elm/Generic.hs +++ b/src/Elm/Generic.hs @@ -349,6 +349,7 @@ data MyType = MyType @ We can check that type name prefix is no longer stripped from record field names: + >>> encode (MyType "Hello" 10) "{\"myTypeFieldOne\":\"Hello\",\"myTypeFieldTwo\":10,\"tag\":\"MyType\"}" -} From 2d125306a6019f8bf02aba4b5893646c91c58c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Hr=C4=8Dek?= Date: Sat, 25 Mar 2023 09:43:27 +0100 Subject: [PATCH 08/11] Update changelog --- CHANGELOG.md | 1 + src/Elm/Generic.hs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dec896..b1296e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The changelog is available [on GitHub][2]. ## Unreleased * Add GHC 9.4.5 and 9.6.2 to CI / tested-with +* Introduce CodeGenOptions that allow customizing how record field names are modified. ## 0.2.0.0 - Mar 29, 2022 diff --git a/src/Elm/Generic.hs b/src/Elm/Generic.hs index 298131c..69aebdd 100644 --- a/src/Elm/Generic.hs +++ b/src/Elm/Generic.hs @@ -308,8 +308,8 @@ They can be passed to 'elmStreetParseJsonWith', 'elmStreetToJsonWith' and 'gener to influence the behavior of FromJSON \/ ToJSON and Elm instances respectively. Note that for Generated Elm encoders \/ decoders to be compatible -with ToJSON \/ FromJSON instances for given type, the same -CodeGenOptions must be used in Elm \/ ToJSON \/ FromJSON instance declarations. +with ToJSON \/ FromJSON instances for given type, +__the same CodeGenOptions must be used in Elm \/ ToJSON \/ FromJSON instance declarations__. Example: Say you don't like the default behavior (stripping type name prefix from all record fields) and you would like to keep all record field names unmodified instead. From c88c9f08fcac01e815d0bf2142c197ef131c4bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Hr=C4=8Dek?= Date: Sat, 25 Mar 2023 09:46:19 +0100 Subject: [PATCH 09/11] Formatting fixes --- frontend/tests/Tests/Golden.elm | 2 +- test/golden/oneType.json | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/tests/Tests/Golden.elm b/frontend/tests/Tests/Golden.elm index 2abed68..2267b0d 100644 --- a/frontend/tests/Tests/Golden.elm +++ b/frontend/tests/Tests/Golden.elm @@ -43,7 +43,7 @@ goldenOneTypeJson = "objectField": {}, "arrayField": [1,2,3], "nullField": null - } + } }, "myUnit": { "tag": "MyUnit", diff --git a/test/golden/oneType.json b/test/golden/oneType.json index be54246..a9eba4e 100644 --- a/test/golden/oneType.json +++ b/test/golden/oneType.json @@ -31,14 +31,14 @@ "bool": true, "unit": [], "nonEmpty": [1], - "value" : { + "value": { "boolField": true, "numberField": 1, "stringField": "hi", "objectField": {}, "arrayField": [1,2,3], "nullField": null - } + } }, "myUnit": { "tag": "MyUnit", @@ -62,9 +62,9 @@ "limit": 123 }, "age": 18, - "newtype": 666, + "newtype": 666, "newtypeList": [123], - "oneConstructor": "OneConstructor", + "oneConstructor": "OneConstructor", "user": { "status": "Approved", "tag": "User", @@ -100,4 +100,4 @@ "contents": [] } ] -} +} \ No newline at end of file From ac820c556f5fef610e62990d36d23eb79ccbff28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Hr=C4=8Dek?= Date: Sun, 26 Mar 2023 08:22:07 +0200 Subject: [PATCH 10/11] Yes newline --- test/golden/oneType.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/golden/oneType.json b/test/golden/oneType.json index a9eba4e..f516274 100644 --- a/test/golden/oneType.json +++ b/test/golden/oneType.json @@ -100,4 +100,5 @@ "contents": [] } ] -} \ No newline at end of file +} + From 8b79edc79af360d4cb2a8428521f53e392f1f917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Hr=C4=8Dek?= Date: Mon, 27 Mar 2023 11:45:49 +0200 Subject: [PATCH 11/11] Export to elm, simplify Types.hs --- frontend/src/Core/Decoder.elm | 5 +++++ frontend/src/Core/Encoder.elm | 7 ++++++ frontend/src/Core/Types.elm | 5 +++++ frontend/tests/Tests/Golden.elm | 6 ++--- types/Types.hs | 39 ++++++++------------------------- 5 files changed, 29 insertions(+), 33 deletions(-) diff --git a/frontend/src/Core/Decoder.elm b/frontend/src/Core/Decoder.elm index ca8bdbe..8b269ba 100644 --- a/frontend/src/Core/Decoder.elm +++ b/frontend/src/Core/Decoder.elm @@ -100,3 +100,8 @@ decodeOneType = D.succeed T.OneType |> required "guests" (D.list decodeGuest) |> required "userRequest" decodeUserRequest |> required "nonEmpty" (elmStreetDecodeNonEmpty decodeMyUnit) + +decodeCustomCodeGen : Decoder T.CustomCodeGen +decodeCustomCodeGen = D.succeed T.CustomCodeGen + |> required "customFunTestString" D.string + |> required "customFunTestInt" D.int diff --git a/frontend/src/Core/Encoder.elm b/frontend/src/Core/Encoder.elm index ace7983..dddf3a0 100644 --- a/frontend/src/Core/Encoder.elm +++ b/frontend/src/Core/Encoder.elm @@ -95,3 +95,10 @@ encodeOneType x = E.object , ("userRequest", encodeUserRequest x.userRequest) , ("nonEmpty", (elmStreetEncodeNonEmpty encodeMyUnit) x.nonEmpty) ] + +encodeCustomCodeGen : T.CustomCodeGen -> Value +encodeCustomCodeGen x = E.object + [ ("tag", E.string "CustomCodeGen") + , ("customFunTestString", E.string x.customFunTestString) + , ("customFunTestInt", E.int x.customFunTestInt) + ] diff --git a/frontend/src/Core/Types.elm b/frontend/src/Core/Types.elm index 5ba8f5c..54373cc 100644 --- a/frontend/src/Core/Types.elm +++ b/frontend/src/Core/Types.elm @@ -119,3 +119,8 @@ type alias OneType = , userRequest : UserRequest , nonEmpty : (MyUnit, List MyUnit) } + +type alias CustomCodeGen = + { customFunTestString : String + , customFunTestInt : Int + } diff --git a/frontend/tests/Tests/Golden.elm b/frontend/tests/Tests/Golden.elm index 2267b0d..4ee1d53 100644 --- a/frontend/tests/Tests/Golden.elm +++ b/frontend/tests/Tests/Golden.elm @@ -36,7 +36,7 @@ goldenOneTypeJson = "bool": true, "unit": [], "nonEmpty": [1], - "value" : { + "value": { "boolField": true, "numberField": 1, "stringField": "hi", @@ -67,9 +67,9 @@ goldenOneTypeJson = "limit": 123 }, "age": 18, - "newtype": 666, + "newtype": 666, "newtypeList": [123], - "oneConstructor": "OneConstructor", + "oneConstructor": "OneConstructor", "user": { "status": "Approved", "tag": "User", diff --git a/types/Types.hs b/types/Types.hs index 890e4a1..ebf33dc 100644 --- a/types/Types.hs +++ b/types/Types.hs @@ -33,7 +33,7 @@ import Data.Text (Text) import Data.Time.Calendar (fromGregorian) import Data.Time.Clock (UTCTime (..)) import Data.Word (Word32) -import Elm (Elm (..), ElmStreet (..), elmNewtype, elmStreetParseJson, elmStreetToJson) +import Elm (Elm (..), ElmStreet (..), elmNewtype) import Elm.Generic (CodeGenOptions (..), ElmStreetGenericConstraints, GenericElmDefinition(..)) import Elm.Aeson (elmStreetParseJsonWith, elmStreetToJsonWith) import GHC.Generics (Generic, Rep) @@ -86,20 +86,14 @@ newtype NewtypeList = NewtypeList [Int] data OneConstructor = OneConstructor deriving stock (Generic, Eq, Show) - deriving anyclass (Elm) - -instance ToJSON OneConstructor where toJSON = elmStreetToJson -instance FromJSON OneConstructor where parseJSON = elmStreetParseJson + deriving (Elm, FromJSON, ToJSON) via ElmStreet OneConstructor data RequestStatus = Approved | Rejected | Reviewing deriving (Generic, Eq, Show) - deriving anyclass (Elm) - -instance ToJSON RequestStatus where toJSON = elmStreetToJson -instance FromJSON RequestStatus where parseJSON = elmStreetParseJson + deriving (Elm, FromJSON, ToJSON) via ElmStreet RequestStatus data User = User { userId :: !(Id User) @@ -107,10 +101,7 @@ data User = User , userAge :: !Age , userStatus :: !RequestStatus } deriving (Generic, Eq, Show) - deriving anyclass (Elm) - -instance ToJSON User where toJSON = elmStreetToJson -instance FromJSON User where parseJSON = elmStreetParseJson + deriving (Elm, FromJSON, ToJSON) via ElmStreet User data Guest = Regular Text Int @@ -118,20 +109,14 @@ data Guest | Special (Maybe [Int]) | Blocked deriving (Generic, Eq, Show) - deriving anyclass (Elm) - -instance ToJSON Guest where toJSON = elmStreetToJson -instance FromJSON Guest where parseJSON = elmStreetParseJson + deriving (Elm, FromJSON, ToJSON) via ElmStreet Guest data UserRequest = UserRequest { userRequestIds :: ![Id User] , userRequestLimit :: !Word32 , userRequestExample :: !(Maybe (Either User Guest)) } deriving (Generic, Eq, Show) - deriving anyclass (Elm) - -instance ToJSON UserRequest where toJSON = elmStreetToJson -instance FromJSON UserRequest where parseJSON = elmStreetParseJson + deriving (Elm, FromJSON, ToJSON) via ElmStreet UserRequest data MyUnit = MyUnit () deriving stock (Show, Eq, Ord, Generic) @@ -142,10 +127,7 @@ data MyResult = Ok | Err Text deriving (Generic, Eq, Show) - deriving anyclass (Elm) - -instance ToJSON MyResult where toJSON = elmStreetToJson -instance FromJSON MyResult where parseJSON = elmStreetParseJson + deriving (Elm, FromJSON, ToJSON) via ElmStreet MyResult -- | All test types together in one type to play with. data OneType = OneType @@ -163,10 +145,7 @@ data OneType = OneType , oneTypeUserRequest :: !UserRequest , oneTypeNonEmpty :: !(NonEmpty MyUnit) } deriving (Generic, Eq, Show) - deriving anyclass (Elm) - -instance ToJSON OneType where toJSON = elmStreetToJson -instance FromJSON OneType where parseJSON = elmStreetParseJson + deriving (Elm, FromJSON, ToJSON) via ElmStreet OneType data CustomCodeGen = CustomCodeGen { customCodeGenString :: String @@ -178,7 +157,6 @@ data CustomCodeGen = CustomCodeGen customCodeGenOptions :: CodeGenOptions customCodeGenOptions = CodeGenOptions (Text.replace "CodeGen" "FunTest") - -- Newtype whose Elm/ToJSON/FromJSON instance use custom CodeGenOptions newtype CustomElm a = CustomElm {unCustomElm :: a} @@ -207,6 +185,7 @@ type Types = , Guest , UserRequest , OneType + , CustomCodeGen ]