Elm-app-url: A simpler way of parsing URLs

elm-app-url is an attempt at making URL handling simpler than elm/url!

It’s based around this AppUrl type:

type alias AppUrl =
    { path : List String
    , queryParameters : QueryParameters
    , fragment : Maybe String
    }


type alias QueryParameters =
    Dict String (List String)

Which works really nicely with pattern matching!

import AppUrl exposing (AppUrl)
import Dict


parse : AppUrl -> Maybe Page
parse url =
    case url.path of
        [] ->
            Just Home

        [ "product", productId ] ->
            String.toInt productId |> Maybe.map (Product << ProductId)

        [ "products" ] ->
            Just
                (ListProducts
                    { color = Dict.get "color" url.queryParameters |> Maybe.andThen List.head
                    , size = Dict.get "size" url.queryParameters |> Maybe.andThen List.head
                    }
                )

        _ ->
            Nothing


type Page
    = Home
    | Product ProductId
    | ListProducts Filters


type ProductId
    = ProductId Int


type alias Filters =
    { color : Maybe String
    , size : Maybe String
    }

Some design stuff:

  • Parse: Avoid the complex types Url.Parser has, and use simple list pattern matching instead. Decode away all percentage escapes so you never need to think about them.
  • Stringify: No separate API like Url.Builder. Escape the minimum needed with percentage escapes.
  • Specification: Follow the WHATWG URL Standard.
  • Keep it simple: URLs shouldn’t be that complicated. List pattern matching, Dict and Maybe is the bread and butter of AppUrl, rather than parsers, tricky type annotations and the </> and <?> operators.

Here’s a little video about it:

27 Likes

I like this! Very simple (in a good way).

I’m guessing you could provide some optional helpers for “get the first/only query param under this key” etc.?

This looks very nice! Thank you

I used to have a helper for that, but when using it in real projects the code ended up better without it, I think!

2 Likes

Any downsides like are the parse errors cleaner with elm/url?

This is very nice and sane! I ran into a corner case a couple of times when trying to parse only the query params, which would fail when there is a non-root path. This makes much more intuitive sense!

2 Likes

I’ve always found it a bit odd that elm/url has this Parser type with the potential of storing lots of metadata, but the only way of running it is:

parse : Parser (a -> a) a -> Url -> Maybe a

So on parse error, you only get Nothing.

On the other hand, I’ve never had the need for parse errors anyway. Either a URL matches, or it does not, and it’s usually easy to figure out why. The only reasonable UX is to show “not found”.

So having an AppUrl -> Maybe a function is no worse.

2 Likes

I love this! :raised_hands: In my mind routing should always have been a simple pattern match :clap:
I don’t know if you drew inspiration from the way ReasonReact does it, but very nicely done! :wink:

2 Likes

Oh, I didn’t know they had the same approach! Thanks for that link!

2 Likes

Nice library, the only problem I have with it is with that query params being List inside the Dict. It makes things for inserting and retrieving one parameter (which is like 99,99% of cases) unnecessarily complex to my taste.

We are also having separate Routers, one for each page/subpage, and we are compounding them together from the higher routers, so we need the way for joining resulting Url structure together, which again is a pain with those Dict of Lists. In the end we plan to wrap AppUrl to our own module and provide those functions ourselves.

But apart from that, I really like how the parsing of the Url is now just a pattern matching.

1 Like

Yeah, it requires a little bit more code:

-- nope:
Dict.get "my-param" url.queryParameters

-- yep:
Dict.get "my-param" url.queryParameters |> Maybe.andThen List.head
-- nope:
Dict.insert "my-param" "value" url.queryParameters

-- yep:
Dict.insert "my-param" ["value"] url.queryParameters

But it’s about taste, like you say.

See also elm-app-url/examples.md at main · lydell/elm-app-url · GitHub for more about this. TL;DR if Add `unionWith` and `unionWithKey` by SiriusStarr · Pull Request #29 · elm-community/dict-extra · GitHub is merged then it’s:

Dict.Extra.unionWith (++) queryParametersA queryParametersB

However, dict-extra has no maintainer so it won’t be merged until someone steps up and takes that role.

1 Like

From that example.md

I’m not sure yet if AppUrl should include a helper function for this.

I would definitely love that :slight_smile: but I can navigate my way around it for sure.