anuragsoni / routes

typed bidirectional router for OCaml/ReasonML web applications
https://anuragsoni.github.io/routes/
BSD 3-Clause "New" or "Revised" License
145 stars 11 forks source link

Group targets with different signature under same type #127

Closed jchavarri closed 3 years ago

jchavarri commented 3 years ago

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!

anmonteiro commented 3 years ago

This problem is easily solved if you make it more data-driven instead. Example:

let t = fun () -> [
  s "foo" / str / s "bar" /? nil @-> fun s -> Foo s;
  s "baz" /? nil @-> fun () -> Baz
]
jchavarri commented 3 years ago

@anmonteiro Maybe I'm not understanding well, but the suggested approach would make impossible to have the composability and extensibility that target type provides:

Is there a way to do this currently without having to include the handlers in the list?

The reason why I am looking for a solution that doesn't involve handlers is that using the generic type ('a, 'b) Routes.target list; allows to define targets in multiple places without coupling them together. But still each list of targets can be matched against a handler that fits the target output type.

While having a closed type t list like suggested above does not allow for any kind of composition / extensibility.

anmonteiro commented 3 years ago

it sounded to me like you were sharing routes in the frontend / backend, and you'd want different functions to be called according to the environment. My proposal solves that by having a common data type of routes, instead of running the actions directly in the route handlers.

jchavarri commented 3 years ago

To give a more specific example, I have different "controllers" (each one for a different part of an app), like:

(* projects_controller.ml *)
let config =
    {
      ...;
      targets = (fun () -> [
        s "project" / int64 / s "bar" /? nil;
        s "add" /? nil]);
      ]
    }

(* users_controller.ml *)
let config =
    {
      ...;
      targets = (fun () -> [
        s "profile" / int64 /? nil;
        s "list" /? nil]);
      ]
    }

the nice thing about this design is that each controller can be added or removed without touching anything else. While having a common data type of routes means I have to change this type every time a new controller is added (or an existing one removed).

jchavarri commented 3 years ago

it sounded to me like you were sharing routes in the frontend / backend, and you'd want different functions to be called according to the environment.

Ideally, in the future i'd like to use these same targets to pretty print urls on the frontend, yes.

anuragsoni commented 3 years ago

@jchavarri 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).

I don't think it'd be possible to achive this with the current api, and I can't think of how to do this with a nice interface in general. That's the con of type level lists i think, each time we have a new parameter we pluck, its reflected in the type. One way of hiding this type knowledge would be to wrap things into an existential

type my_route = MyRoute : ('a, 'c) target -> my_route;;

utop # let r () = s "foo" / str / s "bar" /? nil;;
val r : unit -> (string -> 'a, 'a) target = <fun>

utop # let r' = MyRoute (r());;
val r' : my_route = MyRoute <abstr>

But this doesn't help as there is no way to "unpack" this wrapped value and make use of the content.

anuragsoni commented 3 years ago

@anmonteiro Maybe I'm not understanding well, but the suggested approach would make impossible to have the composability and extensibility that target type provides:

Is there a way to do this currently without having to include the handlers in the list?

The reason why I am looking for a solution that doesn't involve handlers is that using the generic type ('a, 'b) Routes.target list; allows to define targets in multiple places without coupling them together. But still each list of targets can be matched against a handler that fits the target output type.

While having a closed type t list like suggested above does not allow for any kind of composition / extensibility.

One possible approach could be to not group things as a list, but define the routes that need to be shared as regular targets and expose them in the mli, and also create a router in the specific module that's also exposed in the mli file. The routers can then be composed elsewhere via https://github.com/anuragsoni/routes/blob/ff9a4e154d88d1342ebb46a145e6bedee9ae0341/src/routes.mli#L219 and the individual targets can be used in places where you need sprintf/pretty printing etc. It'll be a little verbose than being able to pack things in a list, but i'm not sure if there is a better way to achieve this without making the library even more confusing than the current state 😬

jchavarri commented 3 years ago

@anuragsoni I think using a router is conceptually similar to what @anmonteiro was suggesting: in his proposal, one would stop at the point where a list of routes is created, while in yours (afaiu) one would apply the one_of function to that list to get a router.

I will explore these paths and report back any updates. Thanks a lot to both of you for the suggestions!