Page
Library
Module
Module type
Parameter
Class
Class type
Source
HighwaySourceHighway is a simple, framework-agnostic HTTP router. Its goal is to provide a highly abstract API based on the concepts of response and request so that it can, broadly speaking, be (probably) adapted to any framework in the OCaml ecosystem.
The library allows you to define typed routes, enabling you to generate links using an API similar to the Format module, and to attach controllers to them, similar to the Scanf module.
Highway is primarily used to describe route and associate them with services.
# open Highway ;;To begin with, Highway provides a DSL for describing routes: the combination of an HTTP method, a path and a set of query parameters. For now, we won’t concern ourselves with query parameters.
Let's define a set of routes:
# module Routes = struct
let home = get [] ignore_params
let hello = get [s "hello"] ignore_params
let hello_to = get [s "hello"; string] ignore_params
end ;;
module Routes :
sig
val home : (local, [> `GET ], something, unit, Void.t) route
val hello : (local, [> `GET ], something, unit, Void.t) route
val hello_to :
(local, [> `GET ], something, unit, string -> Void.t) route
endAs you can see, road types contain a lot of information:
scope (can be local or global), that describe the scope of a route. If it is local, it refers to a route that is "internal" to the application, and later, it can be associated with a service. If the route is global, it is associated with a domain and is external to the application (and therefore cannot be associated with a service).met, describes the HTTP method of the route. This tracking is useful because it allows you, for example, to prevent certain link-generation functions from being used on specific routes. (For example, html_href, which generates a link usable for <a> tags, works only for routes associated with the GET method).constraints Specifies whether the route disallows query parameters (the router will continuously reject requests containing query parameters). It can have two values: something, which allows query parameters, and nothing, which disallows query parameters.param_type the type of all (valid) query parameters. If the constraint is nothing, it will always be unit. Here, in our examples, we ignore the query parameters (we don't prohibit them), but their value is also unit.path_params describes a function that returns void parameters extracted from the route path.Here is an other example with a lot of path parameters (and prohibiting query parameters):
# post
[int; string; float; bool; s "foo"; s "bar"; int]
discard_params ;;
- : (local, [> `POST ], nothing, unit,
int -> string -> float -> bool -> int -> Void.t)
route
= <abstr>You can define your own hole using Hole and Pattern.
The separation of a route's definition from its association with a service is heavily inspired by Ocsigen/Eliom; it allows routes to be used within services to describe connections between different services (and to manage form actions).
Link generation is based on three functions:
html_href which generates a link to be used in a <a> tag for the href attribute (among others), and this function only applies to GET routes.html_action which generates a link to be used in a <form> tag for the method attribute (among others), and this function only applies to GET and POST routes.target which generates the arbitrary route link (which can be used, for example, to implement an ambitious fetch function).These three functions fill in the gaps in the routes described by the path and the query params, and allow you to associate additional query parameters (using the extra_params parameter, it is useful since a service can extract other query params after the routing, for example) and an optional anchor (using the anchor parameter).
Functions have "quoted images", which reject extra parameters (traditional functions require routes with something as a constraint).
Here is some example of generating links for routes:
# html_href Routes.home [] ();;
- : string = "/" # html_href Routes.hello [] ();;
- : string = "/hello" # html_href Routes.hello_to ["Xavier"] ();;
- : string = "/hello/Xavier"And with extra parameters and anchor:
# html_href
~extra_params:["foo", "true"; "bar", "hello"]
~anchor:"top-content"
Routes.hello_to ["Xavier"] ();;
- : string = "/hello/Xavier?foo=true&bar=hello#top-content"Or here's a broader example that illustrates the heterogeneous nature of args:
let a_route =
post
[ int; string; float; bool; s "foo"; s "bar"; int; s "a-long-url" ]
discard_params
;;If we use html_action' since it is a POST route that disallows query params. First, we can see the error if we did not give a proper list of path fragment. The error indicates that the list ends prematurely and that some fragments are missing during compilation.
# html_action'
a_route
[42; "foo"; 3.14; false; 42]
~anchor:"a-specific-part-of-the-document"
() ;;
- : string =
"/42/foo/3.14/false/foo/bar/42/a-long-url#a-specific-part-of-the-document"Even though query parameters can be used within the body of a service, it is sometimes convenient to extract some of them directly during the routing phase so they can be used immediately in the body of a service. That is why a route description allows you to define a strategy for extracting query parameters.
Since Highway makes no assumptions about the framework being used, the library assumes that query parameters are represented as an associative list of string * string. So, for example, the following query string: ?foo=bar&x=10&foo=message produces the following list:
[ "foo", "bar"; "x", "10"; "foo", "message" ]That is the choice Dream has made. However, if you are using a framework that makes a different choice — such as Uri's, which uses an associative list of string * string list — you can use the Param.from_nested_list function to convert to that representation.
Unlike path fragments, where order matters, we'd like to be able to process query parameters independently. To do this, the param type defines a validation rule on a set of query parameters to produce a specific value.
Behind the scenes, validation uses the Pidgin library to validate query parameters as if they were records. By using the make_params function, you can define a query parameter validator. As for pattern, you need to give a validation function and a projection function (for link generation).
Let's imagine we want to describe a filtering strategy by author and category:
module Filter = struct
type t =
{ author : string
; category : string
; limit : int option
}
let param =
make_params
~from_query:(fun fields ->
let open Pidgin.Check in
let+ author = req fields "author" string
and+ category = req fields "category" string
and+ limit = opt fields "limit" (int & Int.is_positive) in
{ author; category; limit })
~to_query:(fun { author; category; limit } ->
let all = [ "author", author; "category", category ] in
match limit with
| None -> all
| Some x -> ("limit", string_of_int x) :: all)
(* This is just for testing reason, you do not need, obviously,
to disable ocamlformat here... *)
[@@ocamlformat "disable"]
endWe define from_query, which uses a Pidgin record validator (you can use the full Pidgin API for making fine-grained validators), and a to_query function that returns an associative array. (We do not return a Pidgin expression because that would be too expressive for query parameters.)
Now, we can define a route that will use the filter:
let another_route = get [ s "books"; s "filter" ] Filter.paramAnd when we try to generate the corresponding link:
# html_href another_route []
{author = "John Doe"; category = "Novel"; limit = None} ;;
- : string = "/books/filter?author=John Doe&category=Novel"# html_href another_route []
{author = "John Doe"; category = "Novel"; limit = Some 42} ;;
- : string = "/books/filter?limit=42&author=John Doe&category=Novel"And during the routing phase, query parameters will be validated and given to the controller.
Although the manual definition of a validator is very flexible and allows you to describe a wide variety of scenarios, sometimes you might want a more direct and straightforward approach. For that, there is a slightly simpler (and composable) API available.
In fact, there are functions that allow you to process query parameters one by one, for example:
string_param which takes a string as an argument—the key—and checks whether a string associated with the given key exists.string_opt_param which takes a string as an argument—the key—and checks whether a string associated with the given key exists (and wrap it into an option).int_param, float_param, char_param, bool_param.By default, these validators support only one query parameter, for example:
# html_href
(get [ s "books"; s "filter" ] (string_param "author"))
[] "Xavier" ;;
- : string = "/books/filter?author=Xavier"However, using the & operator, they can be combined. In fact, & composes two arbitrary validators. For example, the original example could be reproduced only in this way:
# html_href
(get [ s "books"; s "filter" ]
(string_param "author"
& string_param "category"
& int_opt_param "limit"))
[] ("Xavier", ("novel", Some 43)) ;;
- : string = "/books/filter?author=Xavier&category=novel&limit=43"Similarly, there is / that uses Either to construct a sum.
In our view, route description tools provide a robust and well-defined approach to describing access points to resources. It would therefore be a shame to limit ourselves to internal links.
Fortunately, the global function allows you to convert a local route into a global route. For example, here is a very small, minimalist (and partial) binding for the JsonPlaceholder API:
module Json_api = struct
open Highway
let base_url = "https://jsonplaceholder.typicode.com"
let all_posts =
global base_url (get [ s "posts" ] ignore_params)
let one_post =
global base_url (get [ s "posts"; int ] ignore_params)
let one_post_with_comments =
global base_url (get [ s "posts"; int; s "comments" ] ignore_params)
let comments =
global base_url (get [ s "comments" ] (int_opt_param "postId"))
end
(* This is just for testing reason, you do not need, obviously,
to disable ocamlformat here... *)
[@@ocamlformat "disable"]You cannot associate global routes with services (which makes sense), but you can still use them to generate links:
# [ html_href Json_api.all_posts [] ()
; html_href Json_api.one_post [1] ()
; html_href Json_api.one_post_with_comments [1] ()
; html_href Json_api.comments [] None
; html_href Json_api.comments [] (Some 1)
] ;;
- : string list =
["https://jsonplaceholder.typicode.com/posts";
"https://jsonplaceholder.typicode.com/posts/1";
"https://jsonplaceholder.typicode.com/posts/1/comments";
"https://jsonplaceholder.typicode.com/comments";
"https://jsonplaceholder.typicode.com/comments?postId=1"]There is therefore no particular reason not to describe your external endpoints using this API, which provides typed functions.
Now that we’ve looked at routes, let’s see how to associate behaviour with them. In other words, how to associate a controller with a route. In Highway terminology (also inspired by Ocsigen), this involves describing a service.
Creating a service is done using the service function:
# service ;;
- : ?middleware:('request, 'response) middleware ->
?precondition:('request -> bool) ->
?postcondition:('args args -> 'param_ty -> 'request -> bool) ->
context:('ctx, 'request, 'response) context ->
route:(local, meth, 'cstr, 'param_ty, 'args) route ->
('args args -> 'param_ty -> 'ctx -> ('request, 'response) handler) ->
('request, 'response) service
= <fun>For now, we won’t worry about the specific settings; we’ll get straight to the point by using a pre-configured version of service:
# let simple_service ~route handler =
service ~context:no_context ~route handler ;;
val simple_service :
route:(local, meth, 'a, 'b, 'c) route ->
('c args -> 'b -> unit -> ('d, 'e) handler) -> ('d, 'e) service = <fun>So the main idea of Services is to associate a route with an handler. As we can see in the signature of simple_service, a handler is a function that takes the following form:
fun [ args_from_route_path ] query_parameter context request -> a_responseHere, since our simple_service is fixed to no_context the context parameter will be unit.
As we have said on numerous occasions, Highway is HTTP server-agnostic, so for the purposes of this tutorial, let’s assume that a response is a string and define a dummy request type, and let’s create our first service:
type request =
{ user : string option
; query : (string * string) list
}
let req ?user ?(query = []) () = { user; query }let a_first_service =
simple_service ~route:Routes.home (fun [] () () _req ->
"Welcome to my website")
;;Let's write an other service with a more complicated route:
let an_other_service =
simple_service ~route:Routes.hello_to (fun [ name ] () () _req ->
"Hello world, Hello" ^ name)
;;And let’s write one final service that utilises query parameters:
let yet_another_service =
simple_service
~route:another_route
(fun [] { author; category; limit } () _req ->
[ "Author: " ^ author
; "Category: " ^ category
; ("Limit: "
^ Option.(value ~default:"none" (map string_of_int limit)))
]
|> String.concat "\n")
;;As we can see, the callback function – the handler – is heavily dependent on the route. This allows us to be guided by the type system.
A middleware is a composable function that wraps a web handler to process a request before it reaches the handler and/or a response after it returns. They can provide capabilities, guards etc.
They are applied once routing has been completed (and therefore do not allow the user to proceed to the next page). Let’s imagine, for example, that we want to have services that are only accessible if the user is logged in:
(* An helper for errors *)
let error_response ?message code _req =
"Error "
^ string_of_int code
^
match message with
| None -> ""
| Some message -> "\n" ^ message
;;
let user_required next_handler ({ user; _ } as req) =
match user with
| None -> error_response ~message:"You need to be logged" 401 req
| Some _ -> next_handler req
;;Now, our function is a middleware; if the user is present in the request, the programme continues; otherwise, an error response is returned. We can now use it in a service definition:
let yet_another_service =
service
~middleware:user_required
~context:no_context
~route:another_route
(fun [] { author; category; limit } () _req ->
[ "Author: " ^ author
; "Category: " ^ category
; ("Limit: "
^ Option.(value ~default:"none" (map string_of_int limit)))
]
|> String.concat "\n")
;;Now, if the service is running but the user is not logged in, the application will return an error response. It is possible to combine several middleware components sequentially using middleware_list.
A context is a “type of middleware” that is applied and allows you to retrieve a value to pass to the handler function. For example, if instead of just verifying, via middleware, that a user is registered (using our test request), we also want to “pass it to our controller,” we could describe a context provider this way:
let provide_user handler ({ user; _ } as req) =
match user with
| None -> error_response ~message:"You need to be logged" 401 req
| Some user -> handler user req
;;It's roughly the same as our user_required middleware, except that this time, the user is “sent” to the next handler (which is our service's controller), which will change the nature of the slot (previously ()) located between the query parameter and the request..
let yet_another_service =
service
~middleware:user_required
~context:provide_user
~route:another_route
(fun [] { author; category; limit } provided_user _req ->
[ "Author: " ^ author
; "Category: " ^ category
; ("Limit: "
^ Option.(value ~default:"none" (map string_of_int limit)))
; "Hello " ^ provided_user
]
|> String.concat "\n")
;;If you don't need context, you can simply use the no_context function, which returns unit as the context (as in our definition of simple_service).
In addition, a service can “hold” two conditions: precondition and postcondition, which are two functions that return Boolean values. If either of these functions returns false, the router moves on to analyzing the route for the next service. The conditions therefore allow the router to skip the route currently being analyzed during the routing process.
This is convenient because once a service has booted (i.e., once it has been selected in the routing), it is not possible to move on to the next route from the controller—but why are there two levels of conditions?
precondition is a function 'request -> bool, It takes action immediately after verifying that the methods match, which is why it uses only the request as the subject of observation.postcondition is a function 'args args -> 'param -> 'request -> bool It runs after the path fragments have been parsed and after the query parameters have been validated. It provides additional context to help determine whether to validate the route. It runs immediately before the middleware (and the context) are applied.By default, both conditions always evaluate to true.
Now that we've seen how to describe route and service (and how to constrain them using middleware, context, and conditions), we'll take a broad look at how routing works.
To route, we use the dispatch function, which has the following type:
# let dispatch = Service.dispatch ;;
val dispatch :
given_method:meth ->
given_path:string list ->
given_query_params:(string * string) list ->
('a, 'b) service list -> ('a, 'b) middleware = <fun>For reasons of generality (once again), the function assumes that query parameters have been normalized as described in the previous sections and that the path is a list of strings; for example, a/b/foo becomes [“a”; ‘b’; “foo”]. It then takes a list of services as an argument and returns a middleware. In other words, if the function doesn't find a route, it passes control to the next handler (which could be another piece of middleware).
Here is a general overview of how the router determines whether a service is a candidate or not:
given_method.precondition.path using given_path (and extract path values into an args).param using given_query_params.postcondition.At this stage, the service is validated as a result and the router apply middleware and context:
context of the service.middleware (if it exists).At this time, Highway does not “intelligently” sort service routes.
There you go—you've just gotten a quick overview of the various features offered by Highway, just like a generic router. Here is the full API.
Re-exporting utility types to simplify the API.
args type describes a heterogeneous list used to generate links associated with a route. It can also serve as a controller parameter.
hole describes an arbitrary value that can be serialized or deserialized. They are used to describe route patterns that introduce variables.
path is a heterogeneous list of patterns (which introduces holes in the final type signature).
meth describes an HTTP method in a rather simplistic way. Since certain capabilities are unlocked by using specific HTTP verbs, the representation of methods uses polymorphic variants to allow for intersections.
Describes a set of query parameters (parameters in the query string) associated with a validation strategy, as supported by the Pidgin library. 'cstrs allows you to specify whether you want to disallow query parameters (or not); 'ty is the type to which you cast your set of parameters.
Describes the prohibition of parameters.
Describes the authorization of parameters.
type ('scope, +'meth, 'cstrs, 'params_ty, 'k) route =
('scope, 'meth, 'cstrs, 'params_ty, 'k) Route.tA route is a combination of a scope (local or global), a method, a path, and a query parameter validator.
Routes are the building blocks for creating services (a controller associated with a route) and for generating links for given routes while adhering to the typing defined by the holes in a path.
Describes the local scope (inside the application).
Describes the global scope (outside the application).
Describes a function from 'request to 'response.
Describes a middleware that extends a given handler.
Describes a specific handler that passes a context to handlers.
Describes a service. A controller associated with a route.
s value describes a Literal pattern, a constant that does not introduce a variable into a pattern.
You can define your own patterns using Hole and Pattern.
Describes a pattern that introduces a variable of type string.
Describes a pattern that introduces a variable of type float.
Describes a pattern that introduces a variable of type char.
Describes a pattern that introduces a variable of type bool.
Describes a potentially empty hole. It use empty to define if a value is present or not in a route path.
Allowing query parameters to be taken into account is a potentially debatable choice because, unlike the placeholders introduced in a route's patterns, the order of the parameters is of little importance. For this reason, all parameters observable in the router are processed by a parameter validation function.
You can describe and compose more Query Param description using the module Param.
Describes a validator that explicitly rejects all query parameters. (Or Param.nop)
Describes a validator that explicitly ignores all query parameters. (Or Param.lax)
val make_params :
from_query:((string * Pidgin.Repr.t) list -> 'a Pidgin.Check.record) ->
to_query:('a -> (string * string) list) ->
(something, 'a) parammake_params ~from_query ~to_query Creates a query parameter validator.
from_query uses a Pidgin record validator and to_query produces an associative list, where the arrays repeat the keys. (Or Param.define)
Same as make_params but use a module. (Or Param.make)
For "unique" query parameters, you can use Hole to describe them "on the fly."
param_from_hole ~key hole define a single param indexed by key using a Hole as validator.
opt_param_from_hole ~key hole define a single optional param indexed by key using a Hole as validator.
A set of pre-built query parameters based on Hole. All of these parameters take a string (the query parameter key) as an argument.
string_param key describes the key=a_string parameter.
string_opt_param key describes the optional key=a_string parameter.
int_param key describes the key=an_int parameter.
int_opt_param key describes the optional key=an_int parameter.
float_param key describes the key=a_float parameter.
float_opt_param key describes the optional key=a_float parameter.
char_param key describes the key=a_char parameter.
char_opt_param key describes the optional key=a_char parameter.
bool_param key describes the key=a_bool parameter.
bool_opt_param key describes the optional key=a_bool parameter.
val get :
('k, Void.t) path ->
('cstr, 'param_ty) param ->
(local, [> `GET ], 'cstr, 'param_ty, 'k) routeget path describes a local route, associated to the method GET for the given path.
val post :
('k, Void.t) path ->
('cstr, 'param_ty) param ->
(local, [> `POST ], 'cstr, 'param_ty, 'k) routepost path describes a local route, associated to the method POST for the given path.
val connect :
('k, Void.t) path ->
('cstr, 'param_ty) param ->
(local, [> `CONNECT ], 'cstr, 'param_ty, 'k) routeconnect path describes a local route, associated to the method CONNECT for the given path.
val delete :
('k, Void.t) path ->
('cstr, 'param_ty) param ->
(local, [> `DELETE ], 'cstr, 'param_ty, 'k) routedelete path describes a local route, associated to the method DELETE for the given path.
val head :
('k, Void.t) path ->
('cstr, 'param_ty) param ->
(local, [> `HEAD ], 'cstr, 'param_ty, 'k) routehead path describes a local route, associated to the method HEAD for the given path.
val options :
('k, Void.t) path ->
('cstr, 'param_ty) param ->
(local, [> `OPTIONS ], 'cstr, 'param_ty, 'k) routeoptions path describes a local route, associated to the method OPTIONS for the given path.
val patch :
('k, Void.t) path ->
('cstr, 'param_ty) param ->
(local, [> `PATCH ], 'cstr, 'param_ty, 'k) routepatch path describes a local route, associated to the method PATCH for the given path.
val put :
('k, Void.t) path ->
('cstr, 'param_ty) param ->
(local, [> `PUT ], 'cstr, 'param_ty, 'k) routeput path describes a local route, associated to the method PUT for the given path.
val query :
('k, Void.t) path ->
('cstr, 'param_ty) param ->
(local, [> `QUERY ], 'cstr, 'param_ty, 'k) routequery path describes a local route, associated to the method QUERY for the given path.
val trace :
('k, Void.t) path ->
('cstr, 'param_ty) param ->
(local, [> `TRACE ], 'cstr, 'param_ty, 'k) routetrace path describes a local route, associated to the method TRACE for the given path.
val global :
string ->
(local, 'meth, 'cstr, 'param_ty, 'k) route ->
(global, 'meth, 'cstr, 'param_ty, 'k) routeglobal base_url local_route makes local_route a global one.
Routes are used to generate links and generally follow this pattern: function ?anchor ?extra_params route args.
They are backed by quoted versions that prevent extra parameters from being passed to routes that do not accept query parameters.
val html_href :
?anchor:string ->
?extra_params:(string * string) list ->
('scope, Method.for_html_links, something, 'param_ty, 'args) route ->
'args args ->
'param_ty ->
stringhtml_href ?anchor ?extra_params route args param generates a link that can be used in a <a> tag for a given route (using args and param). The link can be attached to anchor and extra_params
val html_href' :
?anchor:string ->
('scope, Method.for_html_links, 'cstrs, 'param_ty, 'args) route ->
'args args ->
'param_ty ->
stringhtml_href' ?anchor route args param same as html_href but disallow extra_params (usable with nothing constraint).
val html_action :
?anchor:string ->
?extra_params:(string * string) list ->
('scope, Method.for_html_form, something, 'param_ty, 'args) route ->
'args args ->
'param_ty ->
stringhtml_action ?anchor ?extra_params route args param generates a link that can be used in a <form action=...> tag for a given route (using args and param). The link can be attached to anchor and extra_params
val html_action' :
?anchor:string ->
('scope, Method.for_html_form, 'cstrs, 'param_ty, 'args) route ->
'args args ->
'param_ty ->
stringhtml_action' ?anchor route args param same as html_action but disallow extra_params (usable with nothing constraint).
val target :
?anchor:string ->
?extra_params:(string * string) list ->
('scope, 'meth, something, 'param_ty, 'args) route ->
'args args ->
'param_ty ->
stringtarget ?anchor ?extra_params route args compute a link for a given route, without any method constraints (this can be used, for example, to create fetch calls in JavaScript).
val target' :
?anchor:string ->
('scope, 'meth, 'cstrs, 'param_ty, 'args) route ->
'args args ->
'param_ty ->
stringtarget' ?anchor route args param same as target but disallow extra_params (usable with nothing constraint).
A middleware is a composable function that wraps a web handler to process a request before it reaches the handler and/or a response after it returns.
val middleware_list :
('request, 'response) middleware list ->
('request, 'response) middlewaremiddleware_list some_middlware reduce a list of middleware into one, sequentially. It allows to collapse multiple middleware. (or Middleware.fold)
A context is a specific type of middleware that allows data to be injected arbitrarily into a service handler associated with a route.
No context, inject unit as a context. (or Context.unit)
value_context x inject x as a context. (or Context.const)
A service is a controller associated with a route. Along with route, it is the main component of Highway.
val service :
?middleware:('request, 'response) middleware ->
?precondition:('request -> bool) ->
?postcondition:('args args -> 'param_ty -> 'request -> bool) ->
context:('ctx, 'request, 'response) context ->
route:(local, meth, 'cstr, 'param_ty, 'args) route ->
('args args -> 'param_ty -> 'ctx -> ('request, 'response) handler) ->
('request, 'response) serviceservice ?middleware ?precondition ?postcondition ~context ~route handler describes a service/controller. (or Service.make)
middleware allows you to assign additional middleware to a route, which is executed after the route is selected. (You can use middleware_list to sequentially compose multiple middleware components.)precondition checks a precondition only if the method matches, before proceeding to extract the path and query parameters. If the function returns false, it moves on to the next route. By default, the function always returns true.postcondition checks a precondition after the extraction the path and query parameters (before the middlware and context application). If the function returns false, it moves on to the next route. By default, the function always returns true.context A type of middleware that allows you to provision an additional value. To ignore it, use no_context.route the route of the service.Now that we can describe services, the final step is to choose the right service from a given list.
val dispatch :
given_method:meth ->
given_path:string list ->
given_query_params:(string * string) list ->
('request, 'response) service list ->
('request, 'response) middlewaredispatch ~given_method ~give_path ~given_query_params services describes a middleware that selects a service from a given list (or falls back to the next middleware).
A set of infix operators to simplify the use and composition of some Highway objects.
tl1 ++ tl2 is Path.append tl2 tl2, see Path.append.
val (&) :
(something, 'ty_a) param ->
(something, 'ty_b) param ->
(something, 'ty_a * 'ty_b) paramparam_a & param_b compose the product of two query params.
val (/) :
(something, 'ty_a) param ->
(something, 'ty_b) param ->
(something, ('ty_a, 'ty_b) Either.t) paramparam_a / param_b compose the sum of two query params.
Re-exporting internal modules (if functions are not re-exported in the main module).
module Sigs : sig ... endSet of Reusable Interfaces.
Describes a heterogeneous list that uses arrow notation to characterize the various elements of the list.
A hole is a fragment of a URL that describes a variable (which must be filled in) when interpreting a service or constructing a link. It is used to describe a Pattern.
A pattern is a fragment of a path (a segment is an element separated by slashes). It can be either a literal value or a Hole.
A route is the combination of a method, a path and set of query parameters. It allows you to identify resources and is used to describe internal (local) links and external (global) links, which can be associated with controllers to create services.
A handler describes a function that takes a response as an argument and returns a request. Since Highway is generic, the library makes no assumptions about the type of the request or response; this is left to specialized frameworks.
A middleware is a composable function that wraps a web handler to process a request before it reaches the handler and/or a response after it returns.
A context is a type of middleware that allows a value to be passed to the handler functions assigned to services.