Skip to content

Paths

A parser is a pipeline read left to right, in the order of the URL. It starts from a constructor with Parser.succeed; each step consumes one path segment and feeds the constructor one argument.

Literal segments

Parser.segment matches a fixed segment and produces no value:

open Fable.UrlParser

type Route = About

let routes = [ Parser.succeed About |> Parser.segment "about" ]

Parser.tryParsePath routes "/about" |> printfn "%A" // Ok About
Parser.tryParsePath routes "/abut" |> printfn "%A" // Error "Expected segment 'about' but got 'abut'."

Typed segments

Parser.string takes the next segment as text. Parser.int insists it is an integer. Values reach the constructor in pipeline order:

open Fable.UrlParser

type Route = Doc of folder: string * page: int

let routes =
    [
        Parser.succeed (fun folder page -> Doc(folder, page))
        |> Parser.segment "doc"
        |> Parser.string
        |> Parser.int
    ]

Parser.tryParsePath routes "/doc/guides/2" |> printfn "%A" // Ok (Doc ("guides", 2))
Parser.tryParsePath routes "/doc/guides/two" |> printfn "%A" // Error "Could not convert 'two' to int."

Custom segments

Parser.custom takes your own conversion: a tryParse that returns None for anything it does not recognise, and a type name for the error message:

open Fable.UrlParser

type Level =
    | Low
    | High

let tryParseLevel =
    function
    | "low" -> Some Low
    | "high" -> Some High
    | _ -> None

type Route = Task of Level

let routes =
    [
        Parser.succeed Task
        |> Parser.segment "task"
        |> Parser.custom "Level" tryParseLevel
    ]

Parser.tryParsePath routes "/task/high" |> printfn "%A" // Ok (Task High)
Parser.tryParsePath routes "/task/extreme" |> printfn "%A" // Error "Could not convert 'extreme' to Level."

The root

Parser.succeed on its own consumes nothing, so it matches /:

Parser.succeed Home

The whole path, nothing more

Every segment must be consumed. A parser that matches a prefix but leaves segments behind fails:

open Fable.UrlParser

type Route = Users

let routes = [ Parser.succeed Users |> Parser.segment "users" ]

Parser.tryParsePath routes "/users/42/details" |> printfn "%A"
// Error "URL matched but had leftover segments: '42/details'."

Which error you get

Parsers are tried in order and the first success wins. When all fail, the reported error comes from the parser that consumed the most segments - the one closest to a match:

open Fable.UrlParser

type Route =
    | Home
    | Blog of int

let routes =
    [
        Parser.succeed Home
        Parser.succeed Blog |> Parser.segment "blog" |> Parser.int
    ]

Parser.tryParsePath routes "/blog/first-post" |> printfn "%A"
// Error "Could not convert 'first-post' to int."

The error names the integer conversion, not the root mismatch: the blog parser got two segments deep before failing.

Decoding

Segments are percent-decoded before your parser sees them. /user/ada%20lovelace arrives as the segment ada lovelace.

Edit this page