Hello!
TL;DR: Here are two new modules:
Url.SimpleParseras a more featureful and simpler to understand replacement toUrl.ParserUrl.Codecfor defining both URL parser and builder at the same time (at the cost of some boilerplate)
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.SimpleParseras a more featureful and simpler to understand replacement toUrl.ParserUrl.Codecfor 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/urlsupports, 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 Routewhen 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 ![]()