diff --git a/doc/dune b/doc/dune new file mode 100644 index 0000000..a24f868 --- /dev/null +++ b/doc/dune @@ -0,0 +1 @@ +(documentation) diff --git a/doc/index.mld b/doc/index.mld new file mode 100644 index 0000000..38afa41 --- /dev/null +++ b/doc/index.mld @@ -0,0 +1,83 @@ +{0 Routes} + +{1 Introduction} + +Routes is a routing library for OCaml that allows defining type safe routes, to dispatch a request to a +matching handler, based on path parameters in the input URI target. +Type safe in this context, refers to processing the input URI in a manner that assigns +concrete types to the values extracted from the path parameters. + +The library has no external dependencies aside from the OCaml standard library, and it +can be used in both native (via ocamlopt) and javascript usecases (via js_of_ocaml). +It isn't tied to any particular framework, with the intention for frameworks to provide +a higher level wrapper around it. + +{2 Installation} + +Routes is published on the opam repository. If using opam, install it via + +stable version: +{[ opam install routes ]} + +development version: +{[ opam pin add routes.dev git+https://github.com/anuragsoni/routes.git ]} + +If using esy, add the dependency [@opam/routes] to [package.json/esy.json]. +Or you can use [esy add @opam/routes] to add it to the manifest file automatically. + +{2 Usage } + +{[ + +let greet_user (name : string) (id : int) = + Printf.sprintf "Hello, %s [%d]" name id + +let add_user (name : string) (id : int) (is_admin : bool) = + Printf.sprintf "Added user %s with id %d. IsAdmin? %b" name id is_admin + +let greet_user_route () = Routes.(s "user" / str / int /? nil) +let add_user_route () = Routes.(s "user" / str / int / bool / s "add" /? nil) + +let router = Routes.one_of [ Some `GET, greet_user_route () @--> greet_user + ; Some `POST, add_user_route () @--> add_user ] +]} + + +Routes ships with patterns that match the following types: int, int32, int64, bool, string, +but it is possible to define custom patterns that can be used +to extract path parameters that can be parsed into a user defined type. + +{[ +type shape = + | Square + | Circle + +let shape_of_string = function + | "square" -> Some Square + | "circle" -> Some Circle + | _ -> None + +let shape_to_string = function + | Square -> "square" + | Circle -> "circle" + +let shape = Routes.pattern shape_to_string shape_of_string + +(* Now the shape pattern can be used just like any + of the built in patterns like int, bool etc *) +let route () = s "shape" / shape / s "create" /? nil +]} + +{1 Support} + +Routes' git repository is located on {{: https://github.com/anuragsoni/routes} Github}. Use the repository's {{: https://github.com/anuragsoni/routes/issues} issue tracker} to file bug reports and feature requests. + +{1 License } + +Routes is distributed under the BSD-3-clause license. + +{1 API documentation} +{!modules: +Routes +} + diff --git a/src/routes.mli b/src/routes.mli index 2b2bae6..f8c0ce5 100644 --- a/src/routes.mli +++ b/src/routes.mli @@ -34,25 +34,116 @@ module Method : sig end type ('a, 'b) path +(** [path] represents a sequence of path parameter patterns that are expected in a route. *) + type 'b route -type 'b router +(** [route] is a combination of a path sequence, with a function that will be + called on a successful match. When a path sequence matches, the patterns + that are extracted are forwarded to said function with the types that the user + defined. -val pattern - : ('c -> string) - -> (string -> 'c option) - -> ('a, 'b) path - -> ('c -> 'a, 'b) path + Example: + + {[ + let route () = Routes.(s "foo" / str / int /? nil @--> + (fun (a : string) (b : int) -> + Printf.sprintf "%s %d" a b)) + ]} + +*) + +type 'b router +(** [router] is a collection of multiple routes. It transforms a list of routes + into a trie like structure, that is then used for matching an input target url. + It works for routes that are grouped by an HTTP verb and for standalone routes + that have no HTTP verb attached to it. *) val int : ('a, 'b) path -> (int -> 'a, 'b) path +(** [int] matches a path segment if it can be successfully coerced into an integer. *) + val int32 : ('a, 'b) path -> (int32 -> 'a, 'b) path +(** [int32] matches a path segment if it can be successfully coerced into a 32 bit integer. *) + val int64 : ('a, 'b) path -> (int64 -> 'a, 'b) path +(** [int64] matches a path segment if it can be successfully coerced into a 64 bit integer. *) + val str : ('a, 'b) path -> (string -> 'a, 'b) path +(** [str] matches any path segment and forwards it as a string. *) + val bool : ('a, 'b) path -> (bool -> 'a, 'b) path +(** [bool] matches a path segment if it can be successfully coerced into a boolean. *) + val s : string -> ('a, 'b) path -> ('a, 'b) path +(** [s word] matches a path segment if it exactly matches [word]. The matched path param is then discarded. *) + +val nil : ('a, 'a) path + +val pattern + : ('c -> string) + -> (string -> 'c option) + -> ('a, 'b) path + -> ('c -> 'a, 'b) path +(** [pattern] accepts two functions, one for converting a user provided type to + a string representation, and another to potentially convert a string to the said type. + With these two functions, it creates a pattern that can be used for matching a path segment. + This is useful when there is a need for types that aren't provided out of the box + by the library. + + Example: + + {[ + type shape = + | Square + | Circle + + let shape_of_string = function + | "square" -> Some Square + | "circle" -> Some Circle + | _ -> None + + let shape_to_string = function + | Square -> "square" + | Circle -> "circle" + + let shape = Routes.pattern shape_to_string shape_of_string + + (* Now the shape pattern can be used just like any + of the built in patterns like int, bool etc *) + let route () = s "shape" / shape / s "create" /? nil + ]} + +*) + val ( / ) : (('a, 'b) path -> 'c) -> ('d -> ('a, 'b) path) -> 'd -> 'c +(** [l / r] joins two path match patterns [l] and [r] into a pattern sequence, parse l followed by parse r. + Example: If we want to define a route that matches a string followd by + a constant "foo" and then an integer, we'd use the [/] operator like below: + {[ + let route () = Routes.(str / s "foo" / int /? nil) + ]} *) + val ( /? ) : (('a, 'b) path -> 'c) -> ('a, 'b) path -> 'c +(** [l /? r] is used to express the sequence of, parse l followed by parse r and then stop parsing. + This is used at the end of the route pattern to define how a route should end. The right hand parameter + [r] should be a pattern definition that cannot be used in further chains joined by [/] (One such operator is [nil]). *) + val ( @--> ) : (unit -> ('a, 'b) path) -> 'a -> 'b route +(** [r @--> h] is used to connect a route pattern [r] to a function [h] that gets called + if this pattern is successfully matched.*) + +val one_of : (Method.t option * 'b route) list -> 'b router +(** [one_of] accepts a list of tuples comprised of an optional HTTP verb and a route definition + of type ['b route] where 'b is the type that a successful route match will return. + + It transforms the input list of routes into a trie like structure that can later be used + to perform route matches. *) + val match' : ?meth:Method.t -> 'a router -> target:string -> 'a option +(** [match'] accepts an optional HTTP verb, a router and the target url to match. + if the HTTP verb is provided, it tries to look for a matching route that was defined + with the specific HTTP verb provided as input. Otherwise it looks for a route + that is not associated to any HTTP verb. +*) + val sprintf : (unit -> ('a, string) path) -> 'a -val nil : ('a, 'a) path -val one_of : (Method.t option * 'b route) list -> 'b router +(** [sprintf] takes a route pattern as an input, and returns a string with the result of formatting the pattern into a URI path. *)