Giter Site home page Giter Site logo

anuragsoni / routes Goto Github PK

View Code? Open in Web Editor NEW
140.0 7.0 11.0 2.7 MB

typed bidirectional router for OCaml/ReasonML web applications

Home Page: https://anuragsoni.github.io/routes/

License: BSD 3-Clause "New" or "Revised" License

OCaml 99.29% Makefile 0.71%
ocaml router http-router http-routing bidirectional

routes's Introduction

Routes   BuildStatus Coverage Status

This library will help with adding typed routes to OCaml applications. The goal is to have a easy to use portable library with reasonable performance (See benchmark folder).

Users can create a list of routes, and handler function to work on the extracted entities using the combinators provided by the library. To perform URL matching one would just need to forward the URL's path to the router.

Demo

You can follow along with these examples in the OCaml toplevel (repl). down or utop are recommended to enhance your REPL experience while working through these examples. They will add autocompletion support which can be useful when navigating a new library.

We will start by setting up the toplevel by asking it to load routes.

# #require "routes";;

We will start by defining a few simple routes that don't need to extract any path parameter.

# (* A simple route that matches the empty path segments. *);;
# let root () = Routes.nil;;
val root : unit -> ('a, 'a) Routes.path = <fun>

# (* We can combine multiple segments using `/` *);;
# let users () = Routes.(s "users" / s "get" /? nil);;
val users : unit -> ('a, 'a) Routes.path = <fun>

We can use these route definitions to get a string "pattern" that can potentially be used to show what kind of routes your application can match.

# Routes.string_of_path (root ());;
- : string = "/"

# Routes.string_of_path (users ());;
- : string = "/users/get"

Matching routes where we don't need to extract any parameter could be done with a simple string match. The part where routers are useful is when there is a need to extract some parameters are extracted from the path.

# let sum () = Routes.(s "sum" / int / int /? nil);;
val sum : unit -> (int -> int -> 'a, 'a) Routes.path = <fun>

Looking at the type for sum we can see that our route knows about our two integer path parameters. A route can also extract parameters of different types.

# let get_user () = Routes.(s "user" / str / int64 /? nil);;
val get_user : unit -> (string -> int64 -> 'a, 'a) Routes.path = <fun>

We can still pretty print such routes to get a human readable "pattern" that can be used to inform someone what kind of routes are defined in an application.

Once we start working with routes that extract path parameters, there is another operation that can sometimes be useful. Often times there can be a need to generate a URL from a route. It could be for creating hyperlinks in HTML pages, creating target URLs that can be forwarded to HTTP clients, etc.

Using routes we can create url targets from the same type definition that is used for performing a route match. Using this approach for creating url targets has the benefit that whenever a route definition is updated, the printed format for the url target will also reflect that change. If the types remain the same, then the printing functions will automatically start generating url targets that reflect the change in the route type, and if the types change the user will get a compile time error about mismatched types. This can be useful in ensuring that we avoid using bad/outdated URLs in our application.

# Routes.string_of_path (sum ());;
- : string = "/sum/:int/:int"

# Routes.string_of_path (get_user ());;
- : string = "/user/:string/:int64"

# Routes.sprintf (sum ());;
- : int -> int -> string = <fun>

# Routes.sprintf (get_user ());;
- : string -> int64 -> string = <fun>

# Routes.sprintf (sum ()) 45 12;;
- : string = "/sum/45/12"

# Routes.sprintf (sum ()) 11 56;;
- : string = "/sum/11/56"

# Routes.sprintf (get_user ()) "JohnUser" 1L;;
- : string = "/user/JohnUser/1"

# Routes.sprintf (get_user ()) "foobar" 56121111L;;
- : string = "/user/foobar/56121111"

We've seen a few examples so far, but none of any actual routing. Before we can perform a route match, we need to connect a route definition to a handler function that gets called when a successful match happens.

# let sum_route () = Routes.(sum () @--> fun a b -> Printf.sprintf "%d" (a + b));;
val sum_route : unit -> string Routes.route = <fun>

# let user_route () = Routes.(get_user () @--> fun name id -> Printf.sprintf "(%Ld) %s" id name);;
val user_route : unit -> string Routes.route = <fun>

# let root () = Routes.(root () @--> "Hello World");;
val root : unit -> string Routes.route = <fun>

Now that we have a collection of routes connected to handlers, we can create a router and perform route matching. Something to keep in mind is that we can only combine routes that have the same final return type, i.e. handlers attached to every route in a router should have the same type for the values they return.

# let routes = Routes.one_of [sum_route (); user_route (); root ()];;
val routes : string Routes.router = <abstr>

# Routes.match' routes ~target:"/";;
- : string Routes.match_result = Routes.FullMatch "Hello World"

# Routes.match' routes ~target:"/sum/25/11";;
- : string Routes.match_result = Routes.FullMatch "36"

# Routes.match' routes ~target:"/user/John/1251";;
- : string Routes.match_result = Routes.FullMatch "(1251) John"

# Routes.match' routes ~target:(Routes.sprintf (sum ()) 45 11);;
- : string Routes.match_result = Routes.FullMatch "56"

# (* This route is not an exact match because of the final trailing slash. *);;
# Routes.match' routes ~target:"/sum/1/2/";;
- : string Routes.match_result = Routes.MatchWithTrailingSlash "3"

Dealing with trailing slashes

Every route definition can control what behavior it expects when it encounters a trailing slash. In the examples above all route definitions ended with /? nil. This will result in an exact match if the route does not end in a trailing slash. If the input target matches every paramter but has an additional trailing slash, the route will still be considered a match, but it will inform the user that the matching route was found, effectively having disregarded the trailing slash.

# let no_trail () = Routes.(s "foo" / s "bar" / str /? nil @--> fun msg -> String.length msg);;
val no_trail : unit -> int Routes.route = <fun>

# Routes.(match' (one_of [ no_trail () ]) ~target:"/foo/bar/hello");;
- : int Routes.match_result = Routes.FullMatch 5

# Routes.(match' (one_of [ no_trail () ]) ~target:"/foo/bar/hello/");;
- : int Routes.match_result = Routes.MatchWithTrailingSlash 5

More example of library usage can be seen in the examples folder, and as part of the test definition.

Installation

To use the version published on opam:

opam install routes

For development version:

opam pin add routes git+https://github.com/anuragsoni/routes.git

routes's People

Contributors

anmonteiro avatar anuragsoni avatar dinosaure avatar jchavarri avatar lupus avatar nathanreb avatar risto-stevcev avatar tatchi avatar tsnobip avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

routes's Issues

Expose `add` and `remove` function from the router trie

Right now we have the add function inside the router module that can be used to add a new route to the trie. We should expose that as part of the public API so users can update the router by adding content themselves. On the same topic, maybe we should also implement and expose a remove function.

Optional patterns at the end of path

I have a route that can have the following shapes:

  • foo/:id
  • foo/:id/bar
  • foo/:id/baz

And the same routes with trailing slash.

I am trying to put all of these under a single path. So I defined a pattern for bar and baz, like:

type last =
  | Bar
  | Baz
  | Empty

let last_of_string = function
  | "bar" -> Some Bar
  | "baz" -> Some Baz
  | "" -> Some Empty
  | _ -> None

...

Then I define the path as s "foo" / id / last /? nil);

However, this means that I can't match over foo/:id, but only over foo/:id/, with the trailing slash.

Is it possible to achieve this without defining two paths?

Good way to match on methods and paths?

(This is more of a question, so perhaps it would be better on discuss forum or something like that.)

I see in this pull request (https://github.com/anuragsoni/routes/pull/92/files) that there was once support for also matching on http method/verb. What is the recommended way to handle the http verbs now?

I have a bunch of routes that look like this:

let about = 
  let path = s "about" /? nil in
  let handler = function
    | `GET -> Server.respond ...
    | _ -> Server.respond `Method_not_allowed 
  in
  path @--> handler

then all those go into one router with one_of and then matching done with the match'. That's okay, but it's a bit backwards from what I might expect (eg ASP.NET minimal api).

Example

app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");

Alternatively, I could imagine something where you might have a associative map that maps verbs to a router (each set up with one_of, then you have some set up where you match first on the verb, and then do the route matching with match'.

I suppose it is a mostly matter of personal preference, but I wanted to get your input since at one time this library also handled the http verbs.

Fix mdx tests on windows

Mdx tests seem to be broken when running under windows. We should figure out what's broken under windows and if its an upstream issue, file a bug report with steps to reproduce.

API for catch all route

Is it possible to add parser for catch everything else?
For example:

[`GET, search <$> s "search" *> everything]
let search s = print_string s;

allow to match to

GET /search/test          # print "test"
GET /search/test/1/test   # print "test/1/test"

Add more tests

We should have a bigger test suite that tries to check more combinations of route matching

Improve route matching

Right now we work with a regular list of routes, and perform a process of elimination based on route definitions. This is simple and was the easiest implementation, but it has quite a few drawbacks.

We should ideally preprocess the user provided routes into a tree like structure. It would allow us to avoid trying to match on a list of common prefixes multiple times.

Pattern with 2+ options where path is discarded

We have some route paths of the kind:

let paths = [
  s "foo" /? nil;
  s "foo" //? nil;
  s "foo" / s "baz" /? nil;
  s "foo" / s "baz" //? nil;
  s "foo" / s "qux" /? nil;
  s "foo" / s "qux" //? nil;
  s "bar" /? nil;
  s "bar" //? nil;
  s "bar" / s "baz" /? nil;
  s "bar" / s "baz" //? nil;
  s "bar" / s "qux" /? nil;
  s "bar" / s "qux" //? nil;
]

(with dual trailing and no slash as mentioned in issue #125).

Is there a way to write the above list in a more concise way? I have been trying to use pattern to define one that matches either foo and bar using type unit but then types don't match because the unit value is passed through.

E.g:

module Path = struct
  let of_string = function
    | "foo" -> Some ()
    | "bar" -> Some ()
    | _ -> None

  let to_string _x = "path"
end

let path = Routes.pattern Path.to_string Path.of_string ":shape"

let paths = [
  s "foo" /? nil;
  s "foo" //? nil;
  s "bar" /? nil;
  s "bar" //? nil;
  path / s "baz" /? nil; (* This expression has type (unit -> 'a, 'a) target but an expression was expected of type (unit -> 'a, unit -> 'a) target *)
  ^^^^^^^^^^^^^^^^^^^^^
  path / s "baz" //? nil;
  path / s "qux" /? nil;
  path / s "qux" //? nil;
]

Is there some way to do this?

Extra param for trailing slash?

Maybe there is more elegant way to support GET /health and GET /health/ for the same handler?

Right now I have to add 2 routes for this

[
  `GET, health <$ s "health";
  `GET, health <$ s "health" *> s ""
]

Use a less restrictive ocaml/dune constraint

With the removal of the the HTTP method handling we no longer use features that were specific to OCaml 4.06 (we were using Map.update before that). Ubuntu LTS still ships with OCaml 4.05 and there might be value in supporting versions at least as old as that. The code in the current condition should already compile with 4.05 so its just a matter of updating the version constraint in the opam file.

If a version older than 4.05 is needed, we'd need to replace the String.index_opt with something that works in older versions.

On a similar note we aren't using anything that truly needs a feature added only in dune 2. The autogenerated opam files are nice to have, but we don't really anticipate needing to touch the opam file too much so just maintaining that hand-written opam file can work. This would allow dropping the version constraint on dune.

Add human readable route patterns

We had implemented this as part of #64, but with the current re-write on the master branch, this feature is missing. This issue is created to track that we add this feature before the next release.

Compiler error when combining custom pattern with "variable pattern" (e.g. str)

Hi,
I tried to tinker a little bit more (since last year) with this great library and got into troubles when I was trying to use a custom pattern together with str in several paths.

For examle take your routing_test.ml:288 "test custom pattern" and modify it like this:

let%expect_test "test custom pattern reuse" =
  let open Routes in
  let shape = pattern shape_to_string shape_of_string ":shape" in
(*  error if shape function is used twice: let shape' = custom ~serialize:shape_to_string ~parse:shape_of_string ~label:":shape" in *)
  let r1 () =
    (s "foo" / int / s "shape" / shape /? nil) (* NOTE: this call is now using shape as well *)
    @--> fun c shape -> Printf.sprintf "%d - %s" c (shape_to_string shape)
  in
  let r2 () = s "shape" / shape / str /? nil in (* NOTE: str was s "create" previously *)
  let router = one_of [ r1 () ] in
  let results =
    [ ( "can match a custom pattern"
      , ensure_string_match' ~target:"/foo/12/shape/Circle" router )
    ; ( "Invalid shape does not match"
      , ensure_string_match' ~target:"/foo/12/shape/rectangle" router )
    ; "pretty print custom pattern", Some (string_of_route (r1 ()))
    ; "serialize route with custom pattern", Some (sprintf (r2 ()) Square "name")
    ]
  in
  printf !"%{sexp: (string * string option) list}\n" results;
  [%expect
    {|
    (("can match a custom pattern" ("Exact match with result = 12 - circle"))
     ("Invalid shape does not match" ())
     ("pretty print custom pattern" (/foo/:int/shape/:shape))
     ("serialize route with custom pattern" (/shape/square/name))) |}]
;;

This will yield a compiler error:

This has type:
    ('a, 'b) RoutingExample.Routes.path ->
    (string -> 'a, 'b) RoutingExample.Routes.path
  Somewhere wanted:
    ('a, 'b) RoutingExample.Routes.path ->
    (string, string) RoutingExample.Routes.path

regarding str in

 let r2 () = s "shape" / shape / str /?

Is it possible to reuse custom patterns?

Trailing slashes example in the README

Hey, I think this part of the readme (from line 165 to 177) is left over from the pre 2.0 version.

The //? no longer exists and the code block given is the same as the one above but with a different function name. (Same functionality though.)

Route.match' edge case question

first of all, great library! ive been doing a bit of research in this space and stumbled across your library. it's great! i've learned a lot from reading your code (this is my first time working with ocaml code! - other than reading some of oleg's stuff)

while experimenting, i ran into an edge case i was hoping to get information on.

utop # let helloworld () = Routes.(s "hello" / s "world" /? nil);;
val helloworld : unit -> ('a, 'a) target = <fun>
─( 16:32:04 )─< command 45 >─────────────────────────────────────{ counter: 0 }─
utop # let other () = Routes.(str / s "wow" /? nil);;
val other : unit -> (string -> 'a, 'a) target = <fun>
─( 16:35:00 )─< command 46 >─────────────────────────────────────{ counter: 0 }─
utop # let r1 () = Routes.(helloworld () @--> "Hello World");;
val r1 : unit -> string route = <fun>
─( 16:35:52 )─< command 47 >─────────────────────────────────────{ counter: 0 }─
utop # let r2 () = Routes.(other () @--> fun x -> x);;
val r2 : unit -> string route = <fun>
─( 16:36:54 )─< command 48 >─────────────────────────────────────{ counter: 0 }─
utop # let routes = Routes.one_of [r1 (); r2 ();];;
val routes : string router = <abstr>
─( 16:37:22 )─< command 49 >─────────────────────────────────────{ counter: 0 }─
utop # Routes.match' routes ~target:"/hello/world";;
- : string option = Some "Hello World"
─( 16:37:43 )─< command 50 >─────────────────────────────────────{ counter: 0 }─
utop # Routes.match' routes ~target:"/hello/wow";;
- : string option = None

should the last expression evaluate to None or Some "hello"?

utop # Routes.match' routes ~target:"/hello/wow";;
- : string option = None

is the current behavior expected? if not, i think the issue is here: https://github.com/anuragsoni/routes/blob/main/src/routes.ml#L52

specifically, you could return the result of dynamic capture in the case that static children parsing fails.

maybe you made this choice for perf reasons?

looking forward to your response!

Group targets with different signature under same type

I have some targets like:

let t = fun () -> [
  s "foo" / str / s "bar" /? nil;
  s "baz" /? nil
]

that I would like to group under the same list. The above fails to compile as first target will pass the string over, while second one does not.

I have been looking at pattern so that I could wrap the payload in some variant like:

type t = 
  | Foo of string
  | Baz

but it does not seem possible as pattern is limited to one path segment.

Is there a way to do this currently without having to include the handlers in the list? (Handlers are generally platform specific so I'd like to keep them away from this list of targets).

Thanks!

Forward additional custom data to handlers

I really like the way Routes build function signatures on the fly! I was thinking about a way to extend this for passing for example parsed json body as additional named parameter to handler function. This way I can put all my decoders along with route specification and have handler be clean of dealing with invalid inputs and just consume properly typed structure for raw json input received. Is that currently possible?

Add an Opium example once more?

Hello,

I see that you provided an example of an integration with Opium a while ago, but then removed id.

I've been having issues trying to make my demo work so I'd appreciate if you could provide a little example once more.

Thanks :)

Update or remove the bucklescript packaging setup

I don't use bucklescript, and I'm not sure if the current setup (added as part of #104) is relevant to the current landscape of bucklescript/reason/rescript. I've removed the npm publishing setup for now, and i'm leaning towards removing the bucklescript packaging setup entirely due to unfamiliarity with the ecosystem, and the fact that I always need to manually fix something for it every time I cut a release (mostly because I often forget about bucklescript packaging since I never use it).

If someone is actually using the bucklescript support for a project and would like to continue using this library, reach out and I'll be happy to work together to get the packaging sorted out. But failing that, 0.9 is probably the last version of routes that will be published for bucklescript (on npm or otherwise).

Keep the old readme example in examples/

It's not as obvious from the readme or the examples in the examples/ folder that the routes are being partially applied and then the function invocation is completed in the match' result. You'd figure this out eventually by reading the code carefully and/or using the IDE to give you the type info.

It was a lot clearer in the old example in the readme how this works. I think the type info that was returned by mdx (simulating utop) was really helpful. My suggestion is to keep this older example in the examples/ folder.

Why did the mdx get removed anyway?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.