elm-street
Crossing the road between Haskell and Elm
https://github.com/Holmusk/elm-street
LTS Haskell 23.1: | 0.2.2.1 |
Stackage Nightly 2024-12-09: | 0.2.2.1 |
Latest on Hackage: | 0.2.2.1 |
elm-street-0.2.2.1@sha256:89628bf309b20d2a04635842bfcffd57ed42b6cb29b2c7bf4e74088512f4bd9d,4565
Module documentation for 0.2.2.1
elm-street
Crossing the road between Haskell and Elm.
What is this library about?
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
In order to use elm-street
features, you need to perform the following steps:
-
Add
elm-street
to the dependencies of your Haskell package. -
Derive the
Elm
typeclass for relevant data types. You also need to derive JSON instances according toelm-street
naming scheme. This can be done like this:import Elm (Elm, elmStreetParseJson, elmStreetToJson) data User = User { userName :: Text , userAge :: Int } deriving (Generic) deriving anyclass (Elm) instance ToJSON User where toJSON = elmStreetToJson instance FromJSON User where parseJSON = elmStreetParseJson
NOTE: This requires extensions
-XDerivingStrategies
,-XDeriveGeneric
,-XDeriveAnyClass
.Alternatively you can use
-XDerivingVia
to remove some boilerplate (available since GHC 8.6.1):import Elm (Elm, ElmStreet (..)) data User = User { userName :: Text , userAge :: Int } deriving (Generic) deriving (Elm, ToJSON, FromJSON) via ElmStreet User
-
Create list of all types you want to expose to Elm:
type Types = '[ User , Status ]
NOTE: This requires extension
-XDataKinds
. -
Use
generateElm
function to output definitions to specified directory under specified module prefix.main :: IO () main = generateElm @Types $ defaultSettings "frontend/src" ["Core", "Generated"]
NOTE: This requires extension
-XTypeApplications
.When executed, the above program generates the following files:
frontend/src/Core/Generated/Types.elm
:Core.Generated.Types
module with the definitions of all typesfrontend/src/Core/Generated/Encoder.elm
:Core.Generated.Encoder
module with the JSON encoders for the typesfrontend/src/Core/Generated/Decoder.elm
:Core.Generated.Decoder
module with the JSON decoders for the typesfrontend/src/Core/Generated/ElmStreet.elm
:Core.Generated.ElmStreet
module with bundled helper functions
Elm-side preparations
If you want to use capabilities provided by elm-street
in your Elm
application, you need to have several Elm packages preinstalled in the project. You
can install them with the following commands:
elm install elm/time
elm install elm/json
elm install NoRedInk/elm-json-decode-pipeline
elm install rtfeldman/elm-iso8601-date-strings
Library restrictions
Elm-street
is not trying to be as general as possible and support every
use-case. The library is opinionated in some decisions and contains several
limitations, specifically:
- Record fields must be prefixed with the type name or its abbreviation.
data UserStatus = UserStatus { userStatusId :: Id , userStatusRemarks :: Text } data HealthReading = HealthReading { hrUser :: User , hrDate :: UTCTime , hrWeight :: Double }
- Data types with type variables are not supported (see issue #45 for more details).
Though, if type variables are phantom, you can still implement
Elm
instance which will generate valid Elm defintions. Here is how you can createElm
instance fornewtype
s with phantom type variables:newtype Id a = Id { unId :: Text } instance Elm (Id a) where toElmDefinition _ = elmNewtype @Text "Id" "unId"
- Sum types with records are not supported (because it’s a bad practice to have
records in sum types).
-- - Not supported data Address = Post { postCode :: Text } | Full { fullStreet :: Text, fullHouse :: Int }
- Sum types with more than 8 fields in at least one constructor are not
supported.
-- - Not supported data Foo = Bar Int Text | Baz Int Int Text Text Double Double Bool Bool Char
- Records with fields that reference the type itself are not supported. This
limitation is due to the fact that
elm-street
generatestype alias
for record data type. So the generated Elm type for the following Haskell data type won’t compile in Elm:data User = User { userName :: Text , userFollowers :: [User] }
- Generated JSON encoders and decoders are consistent with default behavior of
derived
ToJSON/FromJSON
instances from theaeson
library except you need to strip record field prefixes. Fortunately, this also can be done generically. You can use functions fromElm.Aeson
module to deriveJSON
instances from theaeson
package. - Only
UTCTime
Haskell data type is supported and it’s translated toPosix
type in Elm. - Some words in Elm are considered reserved and naming a record field with one of these words (prefixed with the type name, see 1) will result in the generated Elm files to not compile. So, the following words should not be used as field names:
if
then
else
case
of
let
in
type
module
where
import
exposing
as
port
tag
(reserved for constructor name due toaeson
options)
- For newtypes
FromJSON
andToJSON
instances should be derived usingnewtype
strategy. AndElm
should be derived usinganyclass
strategy:newtype Newtype = Newtype Int deriving newtype (FromJSON, ToJSON) deriving anyclass (Elm)
Play with frontend example
The frontend
directory contains example of minimal Elm project that shows how
generated types are used. To play with this project, do:
- Build and execute the
generate-elm
binary:cabal new-run generate-elm
- Run Haskell backend:
cabal new-run run-backend
- In separate terminal tab go to the
frontend
folder:cd frontend
- Run the frontend:
elm-app start
Generated examples
Below you can see some examples of how Haskell data types are converted to Elm
types with JSON encoders and decoders using the elm-street
library.
Records
Haskell
data User = User
{ userName :: Text
, userAge :: Int
} deriving (Generic)
deriving (Elm, ToJSON, FromJSON) via ElmStreet User
Elm
type alias User =
{ name : String
, age : Int
}
encodeUser : User -> Value
encodeUser x = E.object
[ ("name", E.string x.name)
, ("age", E.int x.age)
]
decodeUser : Decoder User
decodeUser = D.succeed User
|> required "name" D.string
|> required "age" D.int
Enums
Haskell
data RequestStatus
= Approved
| Rejected
| Reviewing
deriving (Generic)
deriving (Elm, ToJSON, FromJSON) via ElmStreet RequestStatus
Elm
type RequestStatus
= Approved
| Rejected
| Reviewing
showRequestStatus : RequestStatus -> String
showRequestStatus x = case x of
Approved -> "Approved"
Rejected -> "Rejected"
Reviewing -> "Reviewing"
readRequestStatus : String -> Maybe RequestStatus
readRequestStatus x = case x of
"Approved" -> Just Approved
"Rejected" -> Just Rejected
"Reviewing" -> Just Reviewing
_ -> Nothing
universeRequestStatus : List RequestStatus
universeRequestStatus = [Approved, Rejected, Reviewing]
encodeRequestStatus : RequestStatus -> Value
encodeRequestStatus = E.string << showRequestStatus
decodeRequestStatus : Decoder RequestStatus
decodeRequestStatus = elmStreetDecodeEnum readRequestStatus
Newtypes
Haskell
newtype Age = Age
{ unAge :: Int
} deriving (Generic)
deriving newtype (FromJSON, ToJSON)
deriving anyclass (Elm)
Elm
type alias Age =
{ age : Int
}
encodeAge : Age -> Value
encodeAge x = E.int x.age
decodeAge : Decoder Age
decodeAge = D.map Age D.int
Newtypes with phantom types
Haskell
newtype Id a = Id
{ unId :: Text
} deriving (Generic)
deriving newtype (FromJSON, ToJSON)
instance Elm (Id a) where
toElmDefinition _ = elmNewtype @Text "Id" "unId"
Elm
type alias Id =
{ unId : String
}
encodeId : Id -> Value
encodeId x = E.string x.unId
decodeId : Decoder Id
decodeId = D.map Id D.string
Sum types
Haskell
data Guest
= Regular Text Int
| Visitor Text
| Blocked
deriving (Generic)
deriving (Elm, ToJSON, FromJSON) via ElmStreet Guest
Elm
type Guest
= Regular String Int
| Visitor String
| Blocked
encodeGuest : Guest -> Value
encodeGuest x = E.object <| case x of
Regular x1 x2 -> [("tag", E.string "Regular"), ("contents", E.list identity [E.string x1, E.int x2])]
Visitor x1 -> [("tag", E.string "Visitor"), ("contents", E.string x1)]
Blocked -> [("tag", E.string "Blocked"), ("contents", E.list identity [])]
decodeGuest : Decoder Guest
decodeGuest =
let decide : String -> Decoder Guest
decide x = case x of
"Regular" -> D.field "contents" <| D.map2 Regular (D.index 0 D.string) (D.index 1 D.int)
"Visitor" -> D.field "contents" <| D.map Visitor D.string
"Blocked" -> D.succeed Blocked
c -> D.fail <| "Guest doesn't have such constructor: " ++ c
in D.andThen decide (D.field "tag" D.string)
Changes
Changelog
elm-street
uses PVP Versioning.
The changelog is available on GitHub.
0.2.2.1 - May 16, 2024
- Relax version bounds of warp, text and filepath
0.2.2.0 - Jan 4, 2024
- Version bumps to allow building with GHC 9.8.1
- Remove GHC 8.10.7 from CI
0.2.1.1 - Aug 3, 2023
- Add missing extra-source-files: test/golden/oneType.json to cabal
0.2.1.0 - Aug 3, 2023
- Add GHC 9.4.5 and 9.6.2 to CI / tested-with
- Introduce CodeGenOptions to allow customizing record field names.
0.2.0.0 - Mar 29, 2022
- Remove GHC 8.4.4 and 8.6.5 from CI / tested-with
- Add GHC 8.10.7, 9.0.2 and 9.2.2 to CI / tested-with
- Support Json.Decode.Value as primitive
- Add primitive to represent NonEmpty lists as (a, List a) on elm side
- Add overlapping instance for Elm String, to ensure that Haskell
String
s are represented asString
on Elm side (not asList Char
)
0.1.0.4 - Jan 28, 2020
- Bump prettyprinter upper bound to allow building with lts-17.0
0.1.0.3 - Jun 29, 2020
- Update to lts-16.2
0.1.0.2 — Sep 13, 2019
- #89: Regulate parenthesis on complicated types in encoder, decoder and type generation.
0.1.0.1 — Sep 6, 2019
- Fix newtype qualified import bug in
Types.elm
generated module. - Allow
pretty-printer-1.3
version.
0.1.0.0 — Sep 6, 2019
-
#80: Important: All encoders for constructors with fields now have
tag
due to aeson decoder on Haskell side.Migration guide 1: Rename fields that will have
tag
name on the Elm side.Migration guide 2: If you have manual
ToJSON
instances that communicate with Elm via generated decoders, you need to addtag
field with the constructor name:data User = User { ... } instance ToJSON User where toJSON = [ "tag" .= ("User" :: Text), ... ]
-
#71: Breaking change: Remove overlapping instance for
String
.Migration guide: Use
Text
instead ofString
. -
#70: Use qualified imports of generated types and function in Elm generated files.
-
#74: Fix unit type
typeRef
encoder and decoder printers. -
#72: Use consistent encoders and decoders for unary constructors.
-
#79: Implement cross-language golden tests.
-
#76: Support GHC-8.6.5. Use common stanzas.
-
#86: Refactor
Elm.Print
module and split into multiple smaller modules. -
#73: Clarify the restriction with reserved words in documentation.
-
#90 Support converting 3-tuples.
-
#6: Test generated Elm code on CI.
0.0.1 — Mar 29, 2019
- #64: Fix indentation for the generated enums.
- #66: Patch JSON encoders and decoders for sum types with a single field.
0.0.0
- Initially created.