123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865moduletypeURL=sig(** Construct and parse URLs. *)(** {1 Basics} *)(** The protocol (a.k.a. scheme) of the URL. Only web-related protocols are
supported. *)typeprotocol=Http|Httpstypet={protocol:protocol;host:string;port:intoption;path:string;query:stringoption;fragment:stringoption;}(** The parts of a URL.
The {{: https://tools.ietf.org/html/rfc3986 } URL spec} defines the
parts like this:
{[
https://example.com:8042/over/there?name=ferret#nose
\___/ \______________/\_________/ \_________/ \__/
| | | | |
scheme authority path query fragment
]}
The strings in this type are not transformed in any way yet. The
{!Parser} module has functions for that, e.g. splitting the path into
segments, accessing specific query parameters and decoding the results
into custom data types.
Other parts of this library like {!Command.http_request} require URLs as
strings. Those should be constructed directly using functions from the
{!Builder} module, e.g {!Builder.absolute} or {!Builder.custom}.
NOTE: Not all URL features from the URL spec are supported. For example,
the [userinfo] segment as part of the [authority] is not supported.
*)valof_string:string->toption(** [of_string s] tries to split [s] into its URL parts.
Examples:
{[
of_string "http://example.com:443"
=
Some
{
protocol = Http;
host = "example.com";
port = Some 443;
path = "/";
query = None;
fragment = None;
}
of_string "https://example.com/hats?q=top%20hat"
=
Some
{
protocol = Https;
host: "example.com";
port = None;
path = "/hats";
query = Some "q=top%20hat";
fragment = None;
}
of_string "example.com:443" = None (* no protocol *)
of_string "http://tom@example.com" = None (* userinfo not allowed *)
of_string "http://#cats" = None (* no host *)
]}
*)valto_string:t->string(** [to_string url] serializes the given [url] into a string.
This is useful, when we have a {{!t}Url.t} and need to pass it on as string
to {!Command.push_url} or {!debug}. For constructing URL strings from
scratch, it is more convenient to use functions from the {!Builder}
module, e.g {!Builder.absolute} or {!Builder.custom}.
*)(** {1 Percent-encoding} *)valpercent_encode_part:string->string(** [percent_encode_part s] encodes a part of a URL by escaping special
characters like [?], [/] or non-ASCII characters. This uses
Javascript's {{: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent } encodeURIComponent}
internally.
Normally the {!Builder} module should be used for constructing URLs.
This function is here for exceptional cases that require more
control.
*)valpercent_decode_part:string->stringoption(** [percent_decode-part s] decodes a part of a URL recovering special
characters like [?], [/] or non-ASCII characters. This uses
Javascript's {{: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent } decodeURIComponent}
internally.
Normally the {!Parser} module should be used for decoding URLs. This
function is here for exceptional cases that require more control.
*)(** {1 Builders and parsers }*)moduleBuilder:sig(** Construct URLs. *)(** {1 Path builders } *)typet(** The path builder type *)valraw:string->t(** [raw s] creates a URL path segment from string [s] without
percent-encoding it.
This is useful for including readable unicode graphemes in the URL
path. See {!absolute} for examples.
*)valstring:string->t(** [string s] creates a URL path segment from string [s].
Special characters like [?], [/] or non-ASCII characters will be
automatically escaped using percent-encoding. See {!absolute} for
examples.
*)valint:int->t(** [int i] creates a URL path segment from the integer [i].
See {!absolute} for examples.
*)(** {1 Query and fragment builders } *)moduleQuery:sig(** Construct queries *)typet(** The query builder type *)valraw:string->string->t(** [raw key s] creates a query parameter with the given [key]
and string value [s] without percent-encoding either.
This is useful for including readable unicode graphemes in the
URL query. See {!absolute} for examples.
*)valstring:string->string->t(** [string key s] creates a query parameter with the given [key]
and string value [s]..
Special characters in the key and in the value, like [?], [/] or
non-ASCII characters will be automatically escaped using
percent-encoding. See {!absolute} for examples.
*)valint:string->int->t(** [int key i] creates a URL query parameter with the given [key]
and integer value [i].
See {!absolute} for examples.
*)endmoduleFragment:sig(** Construct fragments *)typet(** The fragment builder type *)valraw:string->t(** [raw s] creates a URL fragment from string [s] without
percent-encoding it.
This is useful for including readable unicode graphemes in the URL
fragment.
*)valstring:string->t(** [string s] creates a URL path segment from string [s].
Special characters like [?], [/] or non-ASCII characters will be
automatically escaped using percent-encoding.
*)end(** {1 URL builders} *)(** The type of custom URL we want to construct using {!custom} *)typeroot=|Absolute(** A local URL starting with an absolute path. *)|Relative(** A local URL starting with a relative path (relative to the
current location). *)|Cross_originofstring(** [Cross_origin origin] is a remote URL where the given
[origin] is different from the current location. *)valabsolute:tlist->Query.tlist->string(** [absolute path_segments query_parameters] constructs a local URL
(omitting the scheme and authority parts), containing the given
[path_segments] and [query_parameters].
Examples:
{[
absolute [] []
(* "/" *)
absolute [string "blog"; int 2025; int 8; int 7] []
(* "/blog/2025/8/7" *)
absolute [string "products"] [Query.string "search" "hat"; Query.int "page" 2]
(* "/products?search=hat&page=2" *)
absolute [string "name"; string "Гельмут"] []
(* "/name/%D0%93%D0%B5%D0%BB%D1%8C%D0%BC%D1%83%D1%82" *)
absolute [string "name"; raw "Гельмут"] []
(* "/name/Гельмут" *)
absolute [] [Query.string "emoji" "😅"]
(* "?emoji=%F0%9F%98%85" *)
absolute [] [Query.raw "emoji" "😅"]
(* "?emoji=😅" *)
]}
*)valrelative:tlist->Query.tlist->string(** [relative path_segments query_parameters] constructs a relative
local URL (relative the the current location), containing the given
[path_segments] and [query_parameters].
This behaves the same as {!absolute}, but omits the leading slash.
Examples:
{[
relative [] []
(* "" *)
relative [string "blog"; int 2025; int 8; int 7] []
(* "blog/2025/8/7" *)
]}
*)valcross_origin:string->tlist->Query.tlist->string(** [cross_origin pre_path path_segments query_parameters] allows
constructing a cross-origin URL, (a URL with a different host than
the current location), containing the given [path_segments] and
[query_parameters].
[pre_path] is inserted before the path as-is without further checks
or encoding.
Example:
{[
cross_origin "https://ocaml.org" [string "books"] []
(* "https://ocaml.org/books" *)
]}
*)valcustom:root->tlist->Query.tlist->Fragment.toption->string(** [custom root path_segments query_parameters fragment] allows
constructing custom URLs containing the given [path_segments],
[query_parameters] and [fragment].
Examples:
{[
custom
Absolute
[string "article"; int 42]
[Query.string "search" "ocaml"]
[Some Fragment.string "conclusion"]
(* "/article/42?search=ocaml#conclusion" *)
custom
(Cross_origin "https://ocaml.org")
[string "excercises"]
[]
(Some Fragment.int 6)
(* "https://ocaml.org/excercises#6" *)
]}
*)endmoduleParser:sig(** Parse URLs. *)(** {1 Basics } *)typeurl=ttype('a,'b)t(** The URL parser type *)(** {1 Path} *)valstring:(string->'a,'a)t(** [string] will parse a segment of the path as string.
The segment will be percent-decoded automatically.
{t | input | parse result |
|------------------------------------|------------------|
| ["http://host/alice/"] | [Some "alice"] |
| ["http://host/bob"] | [Some "bob"] |
| ["http://host/%D0%91%D0%BE%D0%B1"] | [Some "Боб"] |
| ["http://host/42/"] | [Some "42"] |
| ["http://host/"] | [None] |}
*)valint:(int->'a,'a)t(** [int] will parse a segment of the path as integer.
{t | input | parse result |
|------------------------|-----------------|
| ["http://host/alice/"] | [None] |
| ["http://host/bob"] | [None] |
| ["http://host/42/"] | [Some "42"] |
| ["http://host/"] | [None] |}
*)vals:string->('a,'a)t(** [s str] will parse a segment of the path if it matches the given
string [str].
The segment will be percent-decoded automatically.
For example, the parser [s "blog" </> int] will behave as follows:
{t | input | parse result |
|-------------------------|-----------------|
| ["http://host/blog/42"] | [Some 42] |
| ["http://host/tree/42"] | [None] |}
*)valcustom:(string->'aoption)->(('a->'b),'b)t(** [custom f] will parse a segment of the path by applying the
function [f] to the raw segment.
Example:
{[
let positive_int: (int -> 'a, 'a) Parser.t =
Parser.custom @@
fun s ->
match int_of_string_opt s with
| Some i when i > 0 ->
Some i
| _ ->
None
]}
The example parser produces the following results:
{t | input | parse result |
|---------------------|-----------------|
| ["http://host/0"] | [None] |
| ["http://host/-42"] | [None] |
| ["http://host/42"] | [Some 42] |}
*)val(</>):('a,'b)t->('b,'c)t->('a,'c)t(** [p1 </> p2] combines the segment parsers [p1] and [p2] and returns
a new parser which will parse two path segments.
Example:
{[
let blog: (int -> 'a, 'a) Parser.t =
let open Parser in
s "blog" </> int
]}
The example parser will behave like this:
{t | input | parse result |
|--------------------------|-----------------|
| ["http://host/blog"] | [None] |
| ["http://host/blog/42"] | [Some 42] |
| ["http://host/blog/42/"] | [Some 42] |}
*)valmap:'a->('a,'b)t->(('b->'c),'c)t(** [map f parser] transforms the [parser] via [f].
[f] can be a function taking as many values as the parser
produces (see example 1) or, in case the parser produces no values
at all, [f] can be a variant constructor or a variant tag (see
example 2).
Examples 1:
{[
type date = {year: int; month: int; day: int}
let date_parser: (date -> 'a, 'a) Parser.t =
let open Parser in
map
(fun year month day -> {year; month; day})
(int </> int </> int)
]}
The parser in example 1 will produce the following results:
{t | input | parse result |
|----------------------------|------------------------------------------|
| ["http://host/2025/08/"] | [None] |
| ["http://host/2025/08/07"] | [Some {year = 2025; month = 8; day = 7}] |}
Example 2:
{[
let language = Haskell | Ocaml | Rust
let language_parser: (language -> 'a, 'a) Parser.t =
let open Parser in
one_of
[
map Haskell (s "hs");
map OCaml (s "ml");
map Rust (s "rs");
]
]}
The parser in example 2 will produce the following results:
{t | input | parse result |
|--------------------|----------------|
| ["http://host/hs"] | [Some Haskell] |
| ["http://host/ml"] | [Some OCaml] |
| ["http://host/rs"] | [Some Rust] |
| ["http://host/py"] | [None] |}
*)valone_of:('a,'b)tlist->('a,'b)t(** [one_of parsers] runs the given [parsers] in the order they are
provided. The result is the result of the first succeeding parser or
[None] if all of them fail.
Example:
{[
type route =
| Index
| Article of int
| Comment of {id: int; article_id: int}
let route_parser: (route -> 'a, 'a) Parser.t =
let open Parser in
one_of
[
map Index top;
map (fun id -> Article id) (s "blog" </> int);
map
(fun article_id id -> Comment {id; article_id})
(s "blog" </> int </> s "comment" </> int)
]
]}
The example parser will behave like this:
{t | input | parse result |
|-----------------------------------|---------------------------------------------|
| ["http://host/"] | [Some Index] |
| ["http://host/blog"] | [None] |
| ["http://host/blog/42"] | [Some (Article 42)] |
| ["http://host/blog/42"] | [Some (Article 42)] |
| ["http://host/blog/42/comment"] | [None] |
| ["http://host/blog/42/comment/5"] | [Some (Comment {id = 5; article_id = 42}))] |}
*)valtop:('a,'a)t(** [top] creates a parser that does not consume any path segment.
It can be used together with {!one_of} in order to use a common
prefix for multiple parsers:
{[
type route = Overview | Post of int
let blog: (route -> 'a, 'a) Parser.t =
let open Parser in
s "blog" </>
one_of
[
map Overview top;
map (fun id -> Post id) (s "post" </> int);
]
]}
The example parser produces the following results:
{t | input | parse result |
|------------------------------|------------------|
| ["http://host/"] | [None] |
| ["http://host/blog"] | [Some Overview] |
| ["http://host/post/42"] | [None] |
| ["http://host/blog/post/42"] | [Some (Post 42)] |}
*)(** {1 Query} *)moduleQuery:sig(** Parse query parameters *)(** {1 Type} *)type'at(** The query parser type.
An query parser of type ['a t] parses the query part of a url
and returns an object of type ['a].
*)(** {1 Elementary Query Parsers} *)valstring:string->(stringoption)t(** [string key] will parse the query parameter named [key] as string.
Both the key and value of the query parameter will be
percent-decoded automatically.
For example. the parser [top </> Query.string "name"] will
behave like this:
{t | input | parse result |
|-----------------------------------------|-----------------------|
| ["http://host?name=Alice"] | [Some (Some "Alice")] |
| ["http://host?name=Bob"] | [Some (Some "Bob")] |
| ["http://host?name=%D0%91%D0%BE%D0%B1"] | [Some (Some "Боб")] |
| ["http://host?name="] | [Some (Some "")] |
| ["http://host?name"] | [Some None] |
| ["http://host"] | [Some None] |}
*)valint:string->(intoption)t(** [int key] will parse the query parameter named [key] as integer.
The key of the query parameter will be percent-decoded
automatically.
For example. the parser [top </> Query.int "year"] will behave
like this:
{t | input | parse result |
|---------------------------|--------------------|
| ["http://host?year=2025"] | [Some (Some 2025)] |
| ["http://host?year=Y2K"] | [Some None] |
| ["http://host?year="] | [Some None] |
| ["http://host?year"] | [Some None] |
| ["http://host"] | [Some None] |}
*)valenum:string->(string*'a)list->'aoptiont(** [enum key table] will parse the query parameter named [key] as
string and will try to transform this string into a value by
looking it up in the given [table].
Example:
{[
let lang_parser:
([`Haskell | `OCaml | `Rust] option -> 'a, 'a) Parser.t
=
let open Parser in
top <?>
Query.enum
"lang"
[("hs", `Haskell); ("ml", `OCaml); ("rs", `Rust)]
]}
The example parser produces the following results:
{t | input | parse result |
|-------------------------|----------------------|
| ["http://host?lang=ml"] | [Some (Some `Ocaml)] |
| ["http://host?lang=py"] | [Some None] |
| ["http://host?lang="] | [Some None] |
| ["http://host?lang"] | [Some None] |
| ["http://host"] | [Some None] |}
*)valcustom:string->(stringlist->'a)->'at(** [custom key f] will parse the query parameter named [key] by
applying the function [f] to the list of raw (undecoded) string
values referenced by [key] in the query string.
While the other query parsers, {!string}, {!int} and {!enum},
only allow at most one occurrence of a key in the query string,
[custom] allows handling multiple occurrences.
Example:
{[
let posts: int list option Parser.t =
let open Parser in
top <?> Query.custom "post" (List.filter_map int_of_string)
]}
The example parser produces the following results:
{t | input | parse result |
|-------------------------------|---------------|
| ["http://host?post=2"] | [Some [2]] |
| ["http://host?post=2&post=7"] | [Some [2; 7]] |
| ["http://host?post=2&post=x"] | [Some [2]] |
| ["http://host?hats=2"] | [Some []] |}
*)(** {1 Combining Query Parsers} *)valreturn:'a->'at(** Make a query parser which parses nothing an returns a result. *)val(<*>):('a->'b)t->'at->'bt(** This function can be used to transform more than one query
parser into a compound query parser.
Example:
{[
type user = {fname: string option; lname: string option}
let user_parser: (user -> 'a, 'a) Parser.t =
let open Parser in
top <?>
Query.(
return (fun fname lname -> {fname; lname})
<*> string "fname"
<*> string "lname"
)
]}
The example parser produces the following results:
{t | input | parse result |
|------------------------------------------|--------------------------------------------------------|
| ["http://host?fname=Xavier&lname=Leroy"] | [Some ({fname = Some "Xavier"; lname = Some "Leroy"))] |
| ["http://host?fname=Xavier"] | [Some ({fname = Some "Xavier"; lname = None})] |
| ["http://host?"] | [Some ({fname = None; lname = None})] |
| ["http://host"] | [Some ({fname = None; lname = None})] |}
*)(** {1 Mapping Query Parsers} *)valmap:('a->'b)->'at->'bt(** [map f query_parser] transforms the [query_parser] which parses
one query parameter via [f].
Example:
{[
let search_term_parser: ([`Search of string] -> 'a, 'a) Parser.t =
let open Parser in
top <?>
Query.map
(fun s -> `Search (Option.value ~default:"" s))
(Query.string "search")
]}
The example parser produces the following results:
{t | input | parse result |
|------------------------------|--------------------------|
| ["http://host?search=ocaml"] | [Some (`Search "ocaml")] |
| ["http://host?search="] | [Some (`Search "")] |
| ["http://host?search"] | [Some (`Search "")] |
| ["http://host"] | [Some (`Search "")] |}
*)endval(<?>):('a,('b->'c))t->'bQuery.t->('a,'c)t(** [url_parser </?> query_parser] combines a [url_parser] with a
[query_parser].
For example, the parser [s "blog" <?> Query.string "search"]
produces the following results:
{t | input | parse result |
|------------------------------|-----------------------|
| ["http://host/blog"] | [Some None] |
| ["http://host?search=ocaml"] | [Some (Some "ocaml")] |
| ["http://host?search="] | [Some (Some "")] |
| ["http://host?search"] | [Some (None)] |}
*)valquery:'aQuery.t->(('a->'b),'b)t(** [query query_parser] converts [query_parser] to a URL parser.
This is useful if a URL has an empty path and we want to parse
query parameters.
Example:
{[
(* The following parsers are equivalent *)
let search_term_parser1: (string -> 'a, 'a) Parser.t =
let open Parser in
query (Query.string "search")
let search_term_parser2: (string -> 'a, 'a) Parser.t =
let open Parser in
top <?> Query.string "search"
]}
The example parsers behave as follows:
{t | input | parse result |
|------------------------------|-----------------------|
| ["http://host"] | [Some None] |
| ["http://host?search=ocaml"] | [Some (Some "ocaml")] |
| ["http://host?search="] | [Some (Some "")] |
| ["http://host?search"] | [Some (None)] |}
*)(** {1 Fragment} *)valfragment:(stringoption->'a)->(('a->'b),'b)t(** [fragment f] creates a fragment parser that produces a value by
calling [f] on the fragment part of the URL.
The fragment part is percent-decoded automatically.
For example. the parser [s "excercises" </> fragment Fun.id]
produces the folloing results:
{t | input | parse result |
|------------------------------|-------------------|
| ["http://host/excercises"] | [Some None] |
| ["http://host/excercises#"] | [Some (Some "")] |
| ["http://host/excercises#6"] | [Some (Some "6")] |}
*)(** {1 Run parsers} *)valparse:(('a->'a),'a)t->url->'aoption(** [parse parser url] runs the given [parser] on the given [url].
Example:
{[
type route = Home | Blog of int | Not_found
let route_parser: (route -> 'a, 'a) Parser.t =
let open Parser in
one_of
[
map Home top;
map (fun id -> Blog id) (s "blog" </> int);
]
let route_of_string (str: string): route =
match of_string str with
| None ->
Not_found
| Some url ->
Option.value ~default:Not_found (parse route_parser url)
]}
The [route_of_string] function above produces the following results:
{t | input | parse result |
|----------------------------------------------|--------------|
| ["/blog/42"] | [Not_found] |
| ["https://example.com/"] | [Home] |
| ["https://example.com/blog"] | [Not_found] |
| ["https://example.com/blog/42"] | [Blog 42] |
| ["https://example.com/blog/42/"] | [Blog 42] |
| ["https://example.com/blog/42#introduction"] | [Blog 42] |
| ["https://example.com/blog/42?search=ocaml"] | [Blog 42] |
| ["https://example.com/settings"] | [Not_found] |}
*)endend