Hello!
TL;DR: Here are two new modules:
-
Url.SimpleParser
as a more featureful and simpler to understand replacement toUrl.Parser
-
Url.Codec
for defining both URL parser and builder at the same time (at the cost of some boilerplate)
https://package.elm-lang.org/packages/Janiczek/elm-url-codec/latest/
Backstory
Recently two events came together and made me publish a package
The first one was a few people asking about the elm/url
's Parser
type on the Elm Slack â more specifically, why is it confusing and how does it work, how to think about the type arguments etc.
The second one was that I had to write a Route
module with its typical toString
and fromUrl
functions for a toy application yet again, and after implementing the parser I just couldnât be bothered duplicating that logic for the Route -> String
function. Feeling that this can get out of sync over time, I started wanting some different solution. âSurely there must be a better way!â
So I took a look. Could we get clearer type signatures and single source of truth in this problem domain?
In my mind an applicative API was a nice goal to strive for:
parser : Parser Route
parser =
succeed UserRoute
|> s "user"
|> string
alongside a single-argument Parser
type (compared to the Parser (a -> b) b
state of the art).
In my first attempt I tried constructing an AST out of that applicative call tree, and then interpreting it in two ways (URL parsing and URL building), but for various reasons that didnât pan out.
But when I gave up on an AST and went for the all-powerful âjust compose functions straight awayâ approach, Iâve succeeded!
I present to you two modules:
-
Url.SimpleParser
as a more featureful and simpler to understand replacement toUrl.Parser
-
Url.Codec
for defining both URL parser and builder at the same time (at the cost of some boilerplate)
In the end the usage looks like:
blogParser : Parser Route
blogParser =
Parser.succeed (\id page tags -> Blog id { page = page, tags = tags })
|> Parser.s "blog"
|> Parser.int
|> Parser.queryInt "page"
|> Parser.queryStrings "tags"
Url.SimpleParser.parsePath [blogParser] "/blog/123?page=3&tags=foo&tags=elm"
--> Ok (Blog 123 { page = Just 3, tags = ["foo", "elm"] })
and in case of Url.Codec:
blogCodec : Codec Route
blogCodec =
Codec.succeed (\id page tags -> Blog id { page = page, tags = tags }) isBlogRoute
|> Codec.s "blog"
|> Codec.int getBlogId
|> Codec.queryInt "page" getBlogPage
|> Codec.queryStrings "tags" getBlogTags
Url.Codec.parsePath [blogCodec] "/blog/123?page=3&tags=foo&tags=elm"
--> Ok (Blog 123 { page = Just 3, tags = ["foo", "elm"] })
Url.Codec.toString [blogCodec] (Blog 999 { page = Nothing, tags = ["hello"] })
--> Just "/blog/999?tags=hello"
FAQ
How are these more featureful?
- In addition to what
elm/url
supports, they support query parameters of the type/hello?admin&no-exports
â something I call âquery flagsâ; parameters that donât have an=
sign.
How is Url.SimpleParser
simpler?
- The parser will have a type signature
Parser Route
when finished and eg.Parser (String -> Route)
when not finished. Compare toelm/url
'sParser (String -> a) a
.
What is the boilerplate cost of Url.Codec
?
- Youâll need to define predicates like
isUserRoute : Route -> Bool
isUserRoute route =
case route of
User _ ->
True
_ ->
False
and getters like
getUserId : Route -> Maybe String
getUserId route =
case route of
User id ->
Just id
_ ->
Nothing
I believe it would be possible to create an elm-review codegen rule that would create these on demand, following the example of lue-bird/elm-review-missing-record-field-lens
. Until then, to work with Url.Codec
youâll need to write these yourself.
Links
For more fleshed-out examples, take a look at:
And hereâs the package itself
https://package.elm-lang.org/packages/Janiczek/elm-url-codec/latest/