{"id":18319261,"url":"https://github.com/holmusk/elm-street","last_synced_at":"2025-04-04T08:05:07.898Z","repository":{"id":34185876,"uuid":"166807765","full_name":"Holmusk/elm-street","owner":"Holmusk","description":":deciduous_tree: Crossing the road between Haskell and Elm","archived":false,"fork":false,"pushed_at":"2024-12-18T07:29:23.000Z","size":154,"stargazers_count":89,"open_issues_count":16,"forks_count":6,"subscribers_count":12,"default_branch":"master","last_synced_at":"2025-04-02T21:51:54.044Z","etag":null,"topics":["backend","code-generation","elm","frontend","haskell","web"],"latest_commit_sha":null,"homepage":"","language":"Haskell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Holmusk.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-01-21T12:07:40.000Z","updated_at":"2025-01-11T03:50:12.000Z","dependencies_parsed_at":"2024-01-03T11:29:36.301Z","dependency_job_id":"2943eed1-5100-43d3-8a6c-1d3fe8dcf2a9","html_url":"https://github.com/Holmusk/elm-street","commit_stats":{"total_commits":66,"total_committers":8,"mean_commits":8.25,"dds":0.6515151515151515,"last_synced_commit":"2bbbbeb0c2ff6bf713c848595b2205db596bae85"},"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Holmusk%2Felm-street","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Holmusk%2Felm-street/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Holmusk%2Felm-street/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Holmusk%2Felm-street/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Holmusk","download_url":"https://codeload.github.com/Holmusk/elm-street/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247142062,"owners_count":20890652,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["backend","code-generation","elm","frontend","haskell","web"],"created_at":"2024-11-05T18:12:52.607Z","updated_at":"2025-04-04T08:05:07.867Z","avatar_url":"https://github.com/Holmusk.png","language":"Haskell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# elm-street\n\n![logo](https://holmusk.dev/images/projects/elm_street.png)\n\n[![Hackage](https://img.shields.io/hackage/v/elm-street.svg)](https://hackage.haskell.org/package/elm-street)\n[![MPL-2.0 license](https://img.shields.io/badge/license-MPL--2.0-blue.svg)](LICENSE)\n\nCrossing the road between Haskell and Elm.\n\n## What is this library about?\n\n`elm-street` allows you to automatically generate definitions of Elm data types and compatible JSON encoders and decoders\n from Haskell types. This helps to avoid writing and maintaining huge chunk of boilerplate code when developing full-stack\napplications.\n\n## Getting started\n\nIn order to use `elm-street` features, you need to perform the following steps:\n\n1. Add `elm-street` to the dependencies of your Haskell package.\n2. Derive the `Elm` typeclass for relevant data types. You also need to derive\n   JSON instances according to `elm-street` naming scheme.\n   This can be done like this:\n   ```haskell\n   import Elm (Elm, elmStreetParseJson, elmStreetToJson)\n\n   data User = User\n       { userName :: Text\n       , userAge  :: Int\n       } deriving (Generic)\n         deriving anyclass (Elm)\n\n   instance ToJSON   User where toJSON = elmStreetToJson\n   instance FromJSON User where parseJSON = elmStreetParseJson\n   ```\n   \u003e **NOTE:** This requires extensions `-XDerivingStrategies`, `-XDeriveGeneric`, `-XDeriveAnyClass`.\n\n   Alternatively you can use `-XDerivingVia` to remove some boilerplate (available since GHC 8.6.1):\n   ```haskell\n   import Elm (Elm, ElmStreet (..))\n\n   data User = User\n       { userName :: Text\n       , userAge  :: Int\n       } deriving (Generic)\n         deriving (Elm, ToJSON, FromJSON) via ElmStreet User\n   ```\n3. Create list of all types you want to expose to Elm:\n   ```haskell\n   type Types =\n      '[ User\n       , Status\n       ]\n   ```\n   \u003e **NOTE:** This requires extension `-XDataKinds`.\n4. Use `generateElm` function to output definitions to specified directory under\n   specified module prefix.\n   ```haskell\n   main :: IO ()\n   main = generateElm @Types $ defaultSettings \"frontend/src\" [\"Core\", \"Generated\"]\n   ```\n   \u003e **NOTE:** This requires extension `-XTypeApplications`.\n\n   When executed, the above program generates the following files:\n\n     + `frontend/src/Core/Generated/Types.elm`: `Core.Generated.Types` module with the definitions of all types\n     + `frontend/src/Core/Generated/Encoder.elm`: `Core.Generated.Encoder` module with the JSON encoders for the types\n     + `frontend/src/Core/Generated/Decoder.elm`: `Core.Generated.Decoder` module with the JSON decoders for the types\n     + `frontend/src/Core/Generated/ElmStreet.elm`: `Core.Generated.ElmStreet` module with bundled helper functions\n\n## Elm-side preparations\n\nIf you want to use capabilities provided by `elm-street` in your Elm\napplication, you need to have several Elm packages preinstalled in the project. You\ncan install them with the following commands:\n\n```shell\nelm install elm/time\nelm install elm/json\nelm install NoRedInk/elm-json-decode-pipeline\nelm install rtfeldman/elm-iso8601-date-strings\n```\n\n## Library restrictions\n\n`Elm-street` is **not** trying to be as general as possible and support every\nuse-case. The library is opinionated in some decisions and contains several\nlimitations, specifically:\n\n1. Record fields must be prefixed with the type name or its abbreviation.\n   ```haskell\n   data UserStatus = UserStatus\n       { userStatusId      :: Id\n       , userStatusRemarks :: Text\n       }\n\n   data HealthReading = HealthReading\n       { hrUser   :: User\n       , hrDate   :: UTCTime\n       , hrWeight :: Double\n       }\n   ```\n2. Data types with type variables are not supported (see [issue #45](https://github.com/Holmusk/elm-street/issues/45) for more details).\n   Though, if type variables are phantom, you can still implement `Elm` instance which\n   will generate valid Elm defintions. Here is how you can create `Elm` instance for\n   `newtype`s with phantom type variables:\n   ```haskell\n   newtype Id a = Id { unId :: Text }\n\n   instance Elm (Id a) where\n       toElmDefinition _ = elmNewtype @Text \"Id\" \"unId\"\n   ```\n3. Sum types with records are not supported (because it's a bad practice to have\n   records in sum types).\n   ```haskell\n   -- - Not supported\n   data Address\n       = Post { postCode :: Text }\n       | Full { fullStreet :: Text, fullHouse :: Int }\n\n   ```\n4. Sum types with more than 8 fields in at least one constructor are not\n   supported.\n   ```haskell\n   -- - Not supported\n   data Foo\n       = Bar Int Text\n       | Baz Int Int Text Text Double Double Bool Bool Char\n   ```\n5. Records with fields that reference the type itself are not supported. This\n   limitation is due to the fact that `elm-street` generates `type alias` for\n   record data type. So the generated Elm type for the following Haskell data\n   type won't compile in Elm:\n   ```haskell\n   data User = User\n       { userName      :: Text\n       , userFollowers :: [User]\n       }\n   ```\n6. Generated JSON encoders and decoders are consistent with default behavior of\n   derived `ToJSON/FromJSON` instances from the `aeson` library except you need\n   to strip record field prefixes. Fortunately, this also can be done\n   generically. You can use functions from `Elm.Aeson` module to derive `JSON`\n   instances from the `aeson` package.\n7. Only `UTCTime` Haskell data type is supported and it's translated to `Posix`\n   type in Elm.\n8. 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:\n   * `if`\n   * `then`\n   * `else`\n   * `case`\n   * `of`\n   * `let`\n   * `in`\n   * `type`\n   * `module`\n   * `where`\n   * `import`\n   * `exposing`\n   * `as`\n   * `port`\n   * `tag` (reserved for constructor name due to `aeson` options)\n9. For newtypes `FromJSON` and `ToJSON` instances should be derived using `newtype` strategy. And `Elm` should be derived using `anyclass` strategy:\n   ```haskell\n   newtype Newtype = Newtype Int\n       deriving newtype (FromJSON, ToJSON)\n       deriving anyclass (Elm)\n   ```\n\n## Play with frontend example\n\nThe `frontend` directory contains example of minimal Elm project that shows how\ngenerated types are used. To play with this project, do:\n\n1. Build and execute the `generate-elm` binary:\n   ```\n   cabal new-run generate-elm\n   ```\n2. Run Haskell backend:\n   ```\n   cabal new-run run-backend\n   ```\n3. In separate terminal tab go to the `frontend` folder:\n   ```\n   cd frontend\n   ```\n4. Run the frontend:\n   ```\n   elm-app start\n   ```\n\n## Generated examples\n\nBelow you can see some examples of how Haskell data types are converted to Elm\ntypes with JSON encoders and decoders using the `elm-street` library.\n\n### Records\n\n**Haskell**\n\n```haskell\ndata User = User\n    { userName :: Text\n    , userAge  :: Int\n    } deriving (Generic)\n      deriving (Elm, ToJSON, FromJSON) via ElmStreet User\n```\n\n**Elm**\n\n```elm\ntype alias User =\n    { name : String\n    , age : Int\n    }\n\nencodeUser : User -\u003e Value\nencodeUser x = E.object\n    [ (\"name\", E.string x.name)\n    , (\"age\", E.int x.age)\n    ]\n\ndecodeUser : Decoder User\ndecodeUser = D.succeed User\n    |\u003e required \"name\" D.string\n    |\u003e required \"age\" D.int\n```\n\n### Enums\n\n**Haskell**\n\n```haskell\ndata RequestStatus\n    = Approved\n    | Rejected\n    | Reviewing\n    deriving (Generic)\n    deriving (Elm, ToJSON, FromJSON) via ElmStreet RequestStatus\n```\n\n**Elm**\n\n```elm\ntype RequestStatus\n    = Approved\n    | Rejected\n    | Reviewing\n\nshowRequestStatus : RequestStatus -\u003e String\nshowRequestStatus x = case x of\n    Approved -\u003e \"Approved\"\n    Rejected -\u003e \"Rejected\"\n    Reviewing -\u003e \"Reviewing\"\n\nreadRequestStatus : String -\u003e Maybe RequestStatus\nreadRequestStatus x = case x of\n    \"Approved\" -\u003e Just Approved\n    \"Rejected\" -\u003e Just Rejected\n    \"Reviewing\" -\u003e Just Reviewing\n    _ -\u003e Nothing\n\nuniverseRequestStatus : List RequestStatus\nuniverseRequestStatus = [Approved, Rejected, Reviewing]\n\nencodeRequestStatus : RequestStatus -\u003e Value\nencodeRequestStatus = E.string \u003c\u003c showRequestStatus\n\ndecodeRequestStatus : Decoder RequestStatus\ndecodeRequestStatus = elmStreetDecodeEnum readRequestStatus\n```\n\n### Newtypes\n\n**Haskell**\n\n```haskell\nnewtype Age = Age\n    { unAge :: Int\n    } deriving (Generic)\n      deriving newtype (FromJSON, ToJSON)\n      deriving anyclass (Elm)\n```\n\n**Elm**\n\n```elm\ntype alias Age =\n    { age : Int\n    }\n\nencodeAge : Age -\u003e Value\nencodeAge x = E.int x.age\n\ndecodeAge : Decoder Age\ndecodeAge = D.map Age D.int\n```\n\n### Newtypes with phantom types\n\n**Haskell**\n\n```haskell\nnewtype Id a = Id\n    { unId :: Text\n    } deriving (Generic)\n      deriving newtype (FromJSON, ToJSON)\n\ninstance Elm (Id a) where\n    toElmDefinition _ = elmNewtype @Text \"Id\" \"unId\"\n```\n\n**Elm**\n\n```elm\ntype alias Id =\n    { unId : String\n    }\n\nencodeId : Id -\u003e Value\nencodeId x = E.string x.unId\n\ndecodeId : Decoder Id\ndecodeId = D.map Id D.string\n```\n\n### Sum types\n\n**Haskell**\n\n```haskell\ndata Guest\n    = Regular Text Int\n    | Visitor Text\n    | Blocked\n    deriving (Generic)\n    deriving (Elm, ToJSON, FromJSON) via ElmStreet Guest\n```\n\n**Elm**\n\n```elm\ntype Guest\n    = Regular String Int\n    | Visitor String\n    | Blocked\n\nencodeGuest : Guest -\u003e Value\nencodeGuest x = E.object \u003c| case x of\n    Regular x1 x2 -\u003e [(\"tag\", E.string \"Regular\"), (\"contents\", E.list identity [E.string x1, E.int x2])]\n    Visitor x1 -\u003e [(\"tag\", E.string \"Visitor\"), (\"contents\", E.string x1)]\n    Blocked  -\u003e [(\"tag\", E.string \"Blocked\"), (\"contents\", E.list identity [])]\n\ndecodeGuest : Decoder Guest\ndecodeGuest =\n    let decide : String -\u003e Decoder Guest\n        decide x = case x of\n            \"Regular\" -\u003e D.field \"contents\" \u003c| D.map2 Regular (D.index 0 D.string) (D.index 1 D.int)\n            \"Visitor\" -\u003e D.field \"contents\" \u003c| D.map Visitor D.string\n            \"Blocked\" -\u003e D.succeed Blocked\n            c -\u003e D.fail \u003c| \"Guest doesn't have such constructor: \" ++ c\n    in D.andThen decide (D.field \"tag\" D.string)\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fholmusk%2Felm-street","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fholmusk%2Felm-street","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fholmusk%2Felm-street/lists"}