elmish-browser


UrlParser

This port of the Elm library helps you turn URLs into nicely structured data. It is designed to be used with Navigation module to help folks create single-page applications (SPAs) where you manage browser navigation yourself.

1: 
module Elmish.UrlParser

Types

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
type State<'v> =
  { visited : string list
    unvisited : string list
    args : Map<string,string>
    value : 'v }

[<RequireQualifiedAccess>]
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module internal State =
  let mkState visited unvisited args value =
        { visited = visited
          unvisited = unvisited
          args = args
          value = value }

  let map f { visited = visited; unvisited = unvisited; args = args; value = value } =
        { visited = visited
          unvisited = unvisited
          args = args
          value = f value }


/// Turn URLs like `/blog/42/cat-herding-techniques` into nice data.
type Parser<'a,'b> = State<'a> -> State<'b> list

Parse segments

Create a custom path segment parser. You can use it to define something like “only CSS files” like this:

1: 
2: 
3: 
4: 
5: 
6: 
    let css =
      custom "CSS_FILE" <| fun segment ->
        if String.EndsWith ".css" then
          Ok segment
        else
          Error "Does not end with .css"
 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
let custom tipe (stringToSomething: string->Result<_,_>) : Parser<_,_> =
    let inner { visited = visited; unvisited = unvisited; args = args; value = value } =
        match unvisited with
        | [] -> []
        | next :: rest ->
            match stringToSomething next with
            | Ok nextValue ->
                [ State.mkState (next :: visited) rest args (value nextValue) ]

            | Error msg ->
                []
    inner

Parse a segment of the path as a string.

1: 
    parse str location
    /alice/  ==>  Some "alice"
    /bob     ==>  Some "bob"
    /42/     ==>  Some "42"
1: 
2: 
let str state =
    custom "string" Ok state

Parse a segment of the path as an int.

1: 
    parse i32 location
    /alice/  ==>  None
    /bob     ==>  None
    /42/     ==>  Some 42
1: 
2: 
let i32 state =
    custom "i32" (System.Int32.TryParse >> function true, value -> Ok value | _ -> Error "Can't parse int" ) state

Parse a segment of the path if it matches a given string.

1: 
2: 
    s "blog"  // can parse /blog/
              // but not /glob/ or /42/ or anything else
 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
let s str : Parser<_,_> =
    let inner { visited = visited; unvisited = unvisited; args = args; value = value } =
        match unvisited with
        | [] -> []
        | next :: rest ->
            if next = str then
                [ State.mkState (next :: visited) rest args value ]
            else
                []
    inner

Combining parsers

Parse a path with multiple segments.

1: 
    parse (s "blog" </> i32) location
    /blog/35/  ==>  Some 35
    /blog/42   ==>  Some 42
    /blog/     ==>  None
    /42/       ==>  None
1: 
    parse (s "search" </> str) location
    /search/cats/  ==>  Some "cats"
    /search/frog   ==>  Some "frog"
    /search/       ==>  None
    /cats/         ==>  None
1: 
2: 
3: 
let inline (</>) (parseBefore: Parser<_,_>) (parseAfter: Parser<_,_>) =
  fun state ->
    List.collect parseAfter (parseBefore state)

Transform a path parser.

1: 
2: 
3: 
4: 
5: 
6: 
    type Comment = { author : string; id : int }
    rawComment =
      s "user" </> str </> s "comments" </> i32
    comment =
      map (fun a id -> { author = a; id = id }) rawComment
    parse comment location
    /user/bob/comments/42  ==>  Some { author = "bob"; id = 42 }
    /user/tom/comments/35  ==>  Some { author = "tom"; id = 35 }
    /user/sam/             ==>  None
1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
let map (subValue: 'a) (parse: Parser<'a,'b>) : Parser<'b->'c,'c> =
    let inner { visited = visited; unvisited = unvisited; args = args; value = value } =
        List.map (State.map value)
        <| parse { visited = visited
                   unvisited = unvisited
                   args = args
                   value = subValue }
    inner

Try a bunch of different path parsers.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
    type Route
      = Search of string
      | Blog of int
      | User of string
      | Comment of string*int
    route =
      oneOf
        [ map Search  (s "search" </> str)
          map Blog    (s "blog" </> i32)
          map User    (s "user" </> str)
          map (fun u c -> Comment (u,c)) (s "user" </> str </> "comments" </> i32) ]
    parse route location
    /search/cats           ==>  Some (Search "cats")
    /search/               ==>  None
    /blog/42               ==>  Some (Blog 42)
    /blog/cats             ==>  None
    /user/sam/             ==>  Some (User "sam")
    /user/bob/comments/42  ==>  Some (Comment "bob" 42)
    /user/tom/comments/35  ==>  Some (Comment "tom" 35)
    /user/                 ==>  None
1: 
2: 
let oneOf parsers state =
    List.collect (fun parser -> parser state) parsers

A parser that does not consume any path segments.

1: 
2: 
3: 
4: 
5: 
6: 
    type BlogRoute = Overview | Post of int
    blogRoute =
      oneOf
        [ map Overview top
          map Post  (s "post" </> i32) ]
    parse (s "blog" </> blogRoute) location
    /blog/         ==>  Some Overview
    /blog/post/42  ==>  Some (Post 42)
1: 
2: 
let top state=
    [state]

Query parameters

Turn query parameters like ?name=tom&age=42 into nice data.

1: 
type QueryParser<'a,'b> = State<'a> -> State<'b> list

Parse some query parameters.

1: 
2: 
3: 
4: 
5: 
6: 
    type Route = BlogList (Option string) | BlogPost Int
    route =
      oneOf
        [ map BlogList (s "blog" <?> stringParam "search")
          map BlogPost (s "blog" </> i32) ]
    parse route location
    /blog/              ==>  Some (BlogList None)
    /blog/?search=cats  ==>  Some (BlogList (Some "cats"))
    /blog/42            ==>  Some (BlogPost 42)
1: 
2: 
3: 
let inline (<?>) (parser: Parser<_,_>) (queryParser:QueryParser<_,_>) : Parser<_,_> =
    fun state ->
        List.collect queryParser (parser state)

Create a custom query parser. You could create parsers like these:

1: 
2: 
    val jsonParam : string -> Decoder a -> QueryParser (Option a -> b) b
    val enumParam : string -> Map<string,a> -> QueryParser (Option a -> b) b
1: 
2: 
3: 
4: 
let customParam (key: string) (func: string option -> _) : QueryParser<_,_> =
    let inner { visited = visited; unvisited = unvisited; args = args; value = value } =
        [ State.mkState visited unvisited args (value (func (Map.tryFind key args))) ]
    inner

Parse a query parameter as a string.

1: 
    parse (s "blog" <?> stringParam "search") location
    /blog/              ==>  Some (Overview None)
    /blog/?search=cats  ==>  Some (Overview (Some "cats"))
1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
let stringParam name =
    customParam name id

let internal intParamHelp =
    Option.bind
        (fun (value: string) ->
            match System.Int32.TryParse value with
            | (true,x) -> Some x
            | _ -> None)

Parse a query parameter as an int. Option you want to show paginated search results. You could have a start query parameter to say which result should appear first.

1: 
    parse (s "results" <?> intParam "start") location
    /results           ==>  Some None
    /results?start=10  ==>  Some (Some 10)
 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
29: 
30: 
31: 
32: 
33: 
34: 
35: 
36: 
37: 
38: 
39: 
40: 
41: 
42: 
43: 
44: 
45: 
46: 
47: 
48: 
49: 
50: 
51: 
let intParam name =
    customParam name intParamHelp


// PARSER HELPERS

let rec internal parseHelp states =
    match states with
    | [] ->
        None
    | state :: rest ->
        match state.unvisited with
        | [] ->
            Some state.value
        | [""] ->
            Some state.value
        | _ ->
            parseHelp rest

let internal splitUrl (url: string) =
    match List.ofArray <| url.Split([|'/'|]) with
    | "" :: segments ->
        segments
    | segments ->
        segments

/// parse a given part of the location
let parse (parser: Parser<'a->'a,'a>) url args =
    { visited = []
      unvisited = splitUrl url
      args = args
      value = id }
    |> parser
    |> parseHelp

open Fable.Core

let internal toKeyValuePair (segment: string) =
    match segment.Split('=') with
    | [| key; value |] ->
        Option.tuple (Option.ofFunc JS.decodeURIComponent key) (Option.ofFunc JS.decodeURIComponent value)
    | _ -> None


let internal parseParams (querystring: string) =
    querystring.Substring(1).Split('&')
    |> Seq.map toKeyValuePair
    |> Seq.choose id
    |> Map.ofSeq

open Browser.Types

Parsers

Parse based on location.pathname and location.search. This parser ignores the hash entirely.

1: 
2: 
let parsePath (parser: Parser<_,_>) (location: Location) =
    parse parser location.pathname (parseParams location.search)

Parse based on location.hash. This parser ignores the normal path entirely.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
let parseHash (parser: Parser<_,_>) (location: Location) =
    let hash, search =
        let hash = location.hash.Substring 1
        if hash.Contains("?") then
            let h = hash.Substring(0, hash.IndexOf("?"))
            h, hash.Substring(h.Length)
        else
            hash, "?"

    parse parser hash (parseParams search)
Multiple items
val string : value:'T -> string

--------------------
type string = System.String
type 'T list = List<'T>
Multiple items
module Map

from Microsoft.FSharp.Collections

--------------------
type Map<'Key,'Value (requires comparison)> =
  interface IReadOnlyDictionary<'Key,'Value>
  interface IReadOnlyCollection<KeyValuePair<'Key,'Value>>
  interface IEnumerable
  interface IComparable
  interface IEnumerable<KeyValuePair<'Key,'Value>>
  interface ICollection<KeyValuePair<'Key,'Value>>
  interface IDictionary<'Key,'Value>
  new : elements:seq<'Key * 'Value> -> Map<'Key,'Value>
  member Add : key:'Key * value:'Value -> Map<'Key,'Value>
  member ContainsKey : key:'Key -> bool
  ...

--------------------
new : elements:seq<'Key * 'Value> -> Map<'Key,'Value>
Multiple items
type RequireQualifiedAccessAttribute =
  inherit Attribute
  new : unit -> RequireQualifiedAccessAttribute

--------------------
new : unit -> RequireQualifiedAccessAttribute
Multiple items
type CompilationRepresentationAttribute =
  inherit Attribute
  new : flags:CompilationRepresentationFlags -> CompilationRepresentationAttribute
  member Flags : CompilationRepresentationFlags

--------------------
new : flags:CompilationRepresentationFlags -> CompilationRepresentationAttribute
type CompilationRepresentationFlags =
  | None = 0
  | Static = 1
  | Instance = 2
  | ModuleSuffix = 4
  | UseNullAsTrueValue = 8
  | Event = 16
CompilationRepresentationFlags.ModuleSuffix: CompilationRepresentationFlags = 4
module String

from Microsoft.FSharp.Core
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
Multiple items
module Result

from Microsoft.FSharp.Core

--------------------
[<Struct>]
type Result<'T,'TError> =
  | Ok of ResultValue: 'T
  | Error of ErrorValue: 'TError
namespace System
type Int32 =
  struct
    member CompareTo : value:obj -> int + 1 overload
    member Equals : obj:obj -> bool + 1 overload
    member GetHashCode : unit -> int
    member GetTypeCode : unit -> TypeCode
    member ToString : unit -> string + 3 overloads
    member TryFormat : destination:Span<char> * charsWritten:int * ?format:ReadOnlySpan<char> * ?provider:IFormatProvider -> bool
    static val MaxValue : int
    static val MinValue : int
    static member Parse : s:string -> int + 3 overloads
    static member TryParse : s:string * result:int -> bool + 1 overload
  end
System.Int32.TryParse(s: string, result: byref<int>) : bool
System.Int32.TryParse(s: string, style: System.Globalization.NumberStyles, provider: System.IFormatProvider, result: byref<int>) : bool
Multiple items
module List

from Microsoft.FSharp.Collections

--------------------
type List<'T> =
  | ( [] )
  | ( :: ) of Head: 'T * Tail: 'T list
    interface IReadOnlyList<'T>
    interface IReadOnlyCollection<'T>
    interface IEnumerable
    interface IEnumerable<'T>
    member GetSlice : startIndex:int option * endIndex:int option -> 'T list
    member Head : 'T
    member IsEmpty : bool
    member Item : index:int -> 'T with get
    member Length : int
    member Tail : 'T list
    ...
val collect : mapping:('T -> 'U list) -> list:'T list -> 'U list
val id : x:'T -> 'T
Multiple items
val int : value:'T -> int (requires member op_Explicit)

--------------------
type int = int32

--------------------
type int<'Measure> = int
val map : mapping:('T -> 'U) -> list:'T list -> 'U list
module Option

from Microsoft.FSharp.Core
type 'T option = Option<'T>
val tryFind : key:'Key -> table:Map<'Key,'T> -> 'T option (requires comparison)
val bind : binder:('T -> 'U option) -> option:'T option -> 'U option
union case Option.Some: Value: 'T -> Option<'T>
union case Option.None: Option<'T>
val ofArray : array:'T [] -> 'T list
module Seq

from Microsoft.FSharp.Collections
val map : mapping:('T -> 'U) -> source:seq<'T> -> seq<'U>
val choose : chooser:('T -> 'U option) -> source:seq<'T> -> seq<'U>
val ofSeq : elements:seq<'Key * 'T> -> Map<'Key,'T> (requires comparison)
val hash : obj:'T -> int (requires equality)