Url.Parser

In the URI spec, Tim Berners-Lee says a URL looks like this:

  https://example.com:8042/over/there?name=ferret#nose
  \___/   \______________/\_________/ \_________/ \__/
    |            |            |            |        |
  scheme     authority       path        query   fragment

This module is primarily for parsing the path part.

Primitives

type Parser a b

Turn URLs like /blog/42/cat-herding-techniques into nice Gren data.

string : Parser (String -> a) a

Parse a segment of the path as a String.

-- /alice/  ==>  Just "alice"
-- /bob     ==>  Just "bob"
-- /42/     ==>  Just "42"
-- /        ==>  Nothing
int : Parser (Int -> a) a

Parse a segment of the path as an Int.

-- /alice/  ==>  Nothing
-- /bob     ==>  Nothing
-- /42/     ==>  Just 42
-- /        ==>  Nothing
s : String -> Parser a a

Parse a segment of the path if it matches a given string. It is almost always used with </> or oneOf. For example:

blog : Parser (Int -> a) a
blog =
  s "blog" </> int

-- /blog/42  ==>  Just 42
-- /tree/42  ==>  Nothing

The path segment must be an exact match!

Path

(</>) : Parser a b -> Parser b c -> Parser a c

Parse a path with multiple segments.

blog : Parser (Int -> a) a
blog =
  s "blog" </> int

-- /blog/35/  ==>  Just 35
-- /blog/42   ==>  Just 42
-- /blog/     ==>  Nothing
-- /42/       ==>  Nothing

search : Parser (String -> a) a
search =
  s "search" </> string

-- /search/wolf/  ==>  Just "wolf"
-- /search/frog   ==>  Just "frog"
-- /search/       ==>  Nothing
-- /wolf/         ==>  Nothing
map : a -> Parser a b -> Parser (b -> c) c

Transform a path parser.

type alias Comment = { user : String, id : Int }

userAndId : Parser (String -> Int -> a) a
userAndId =
  s "user" </> string </> s "comment" </> int

comment : Parser (Comment -> a) a
comment =
  map Comment userAndId

-- /user/bob/comment/42  ==>  Just { user = "bob", id = 42 }
-- /user/tom/comment/35  ==>  Just { user = "tom", id = 35 }
-- /user/sam/             ==>  Nothing
oneOf : Array (Parser a b) -> Parser a b

Try a bunch of different path parsers.

type Route
  = Topic String
  | Blog Int
  | User String
  | Comment String Int

route : Parser (Route -> a) a
route =
  oneOf
    [ map Topic   (s "topic" </> string)
    , map Blog    (s "blog" </> int)
    , map User    (s "user" </> string)
    , map Comment (s "user" </> string </> s "comment" </> int)
    ]

-- /topic/wolf           ==>  Just (Topic "wolf")
-- /topic/               ==>  Nothing

-- /blog/42               ==>  Just (Blog 42)
-- /blog/wolf             ==>  Nothing

-- /user/sam/             ==>  Just (User "sam")
-- /user/bob/comment/42  ==>  Just (Comment "bob" 42)
-- /user/tom/comment/35  ==>  Just (Comment "tom" 35)
-- /user/                 ==>  Nothing

If there are multiple parsers that could succeed, the first one wins.

top : Parser a a

A parser that does not consume any path segments.

type Route = Overview | Post Int

blog : Parser (BlogRoute -> a) a
blog =
  s "blog" </>
    oneOf
      [ map Overview top
      , map Post (s "post" </> int)
      ]

-- /blog/         ==>  Just Overview
-- /blog/post/42  ==>  Just (Post 42)
custom : String -> (String -> Maybe a) -> Parser (a -> b) b

Create a custom path segment parser. Here is how it is used to define the int parser:

int : Parser (Int -> a) a
int =
  custom "NUMBER" String.toInt

You can use it to define something like “only CSS files” like this:

css : Parser (String -> a) a
css =
  custom "CSS_FILE" <| \segment ->
    if String.endsWith ".css" segment then
      Just segment
    else
      Nothing

Query

(<?>) : Parser a (query -> b) -> Parser query -> Parser a b

The Url.Parser.Query module defines its own Parser type. This function helps you use those with normal parsers. For example, maybe you want to add a search feature to your blog website:

import Url.Parser.Query as Query

type Route
  = Overview (Maybe String)
  | Post Int

blog : Parser (Route -> a) a
blog =
  oneOf
    [ map Overview (s "blog" <?> Query.string "q")
    , map Post (s "blog" </> int)
    ]

-- /blog/           ==>  Just (Overview Nothing)
-- /blog/?q=wolf    ==>  Just (Overview (Just "wolf"))
-- /blog/wolf       ==>  Nothing
-- /blog/42         ==>  Just (Post 42)
-- /blog/42?q=wolf  ==>  Just (Post 42)
-- /blog/42/wolf    ==>  Nothing
query : Parser query -> Parser (query -> a) a

The Url.Parser.Query module defines its own Parser type. This function is a helper to convert those into normal parsers.

import Url.Parser.Query as Query

-- the following expressions are both the same!
--
-- s "blog" <?> Query.string "search"
-- s "blog" </> query (Query.string "search")

This may be handy if you need query parameters but are not parsing any path segments.

Fragment

fragment : (Maybe String -> fragment) -> Parser (fragment -> a) a

Create a parser for the URL fragment, the stuff after the #. This can be handy for handling links to DOM elements within a page. Pages like this one!

type alias Docs =
  (String, Maybe String)

docs : Parser (Docs -> a) a
docs =
  map Tuple.pair (string </> fragment identity)

-- /List/map   ==>  Nothing
-- /List/#map  ==>  Just ("List", Just "map")
-- /List#map   ==>  Just ("List", Just "map")
-- /List#      ==>  Just ("List", Just "")
-- /List       ==>  Just ("List", Nothing)
-- /           ==>  Nothing

Run Parsers

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

Actually run a parser! You provide some Url that represent a valid URL. From there parse runs your parser on the path, query parameters, and fragment.

import Url
import Url.Parser exposing (Parser, parse, int, map, oneOf, s, top)

type Route = Home | Blog Int | NotFound

route : Parser (Route -> a) a
route =
  oneOf
    [ map Home top
    , map Blog (s "blog" </> int)
    ]

toRoute : String -> Route
toRoute string =
  when Url.fromString string is
    Nothing ->
      NotFound

    Just url ->
      Maybe.withDefault NotFound (parse route url)

-- toRoute "/blog/42"                            ==  NotFound
-- toRoute "https://example.com/"                ==  Home
-- toRoute "https://example.com/blog"            ==  NotFound
-- toRoute "https://example.com/blog/42"         ==  Blog 42
-- toRoute "https://example.com/blog/42/"        ==  Blog 42
-- toRoute "https://example.com/blog/42#wolf"    ==  Blog 42
-- toRoute "https://example.com/blog/42?q=wolf"  ==  Blog 42
-- toRoute "https://example.com/settings"        ==  NotFound

Functions like toRoute are useful when creating single-page apps with Browser.application. I use them in init and onNavigation to handle the initial URL and any changes.