GithubHelp home page GithubHelp logo

trail's Introduction

Build Status Join the chat at https://gitter.im/sparsetech/trail Maven Central

Trail is a routing library for Scala. It is available for the JVM, Scala.js and Scala Native.

Features

  • Define type-safe routes
  • Parse and generate URLs
  • DSL to extract path elements, arguments and fragments
  • Express routing tables via pattern matching
  • Define custom codecs
  • IDE support
  • Cross-platform support (JVM, Scala.js, Scala Native)
  • Zero dependencies

Example

import trail._

val details  = Root / "details" / Arg[Int]
val userInfo = Root / "user" / Arg[String] & Param[Boolean]("show")

val result = "/user/hello?show=false" match {
  case details (a)      => s"details: $a"
  case userInfo((u, s)) => s"user: $u, show: $s"
}

Links

Licence

Trail is licensed under the terms of the Apache v2.0 licence.

Authors

  • Tim Nieradzik
  • Darren Gibson
  • Anatolii Kmetiuk

trail's People

Contributors

anatoliykmetyuk avatar lolgab avatar tindzk avatar zarthross 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

Watchers

 avatar  avatar  avatar  avatar

trail's Issues

Routes match more than advertised

Root.parse("/hello/world").isDefined   // true
(Root / "hello").parse("/hello/world").isDefined   // true

Having read the documentation, I would expect both of those to be false, not true. I don't see this behaviour explicitly tested (except indirectly here) so I'm not sure if this is intended or not.

I don't think this is desirable behaviour:

  • It makes it impossible to handle 404s โ€“ /hello/world is not a URL I want any route to match at all
  • It requires seemingly non-conflicting routes to be evaluated in a certain order โ€“ Root / "hello" has to go before Root

The only possible workaround is to append / "" to the route, but that only works for Root / "". In other cases it appends an undesirable "/" to the route URL, i.e. (Root / "hello" / "").url(()) is "/hello/" instead of the desired "/hello".

I think routes should perform an exact match instead of current startsWith-like behaviour, and the current behaviour should be opt-in for each route using Root / "hello" / Rest or something like that.


Also unrelatedly, it appears that the linked gitter room no longer exists.

no "empty" (zero) path segment

I need to create a different root based on whether I am running against a dev server or a prod server. The base url is different. But there does not seem to be a way to define a zero path segment so I can form the proper root.

// dev server, root has no content after the location's origin
// http://devserver/users
val root = Root / Arg[String]
// but on prod server, http://prodserver/api/v2.0/users
val root  = Root / someString / Arg[String]
// so ideally, I would have
val root = Root / maybeSegment.getOrElse(Zero) / Arg[String]]

Is there any way to handle this? The SPA client needs to handle this in the SPA's router. If the HList is statically defined, I'm not sure how I would be able to do this.

Some architectural improvements

There are architectural problems that would prevent me from using this project should I decide to write a real-world web app. It is possible this project is just fine-tuned for SPAs or that my approach is not what people use in practice. However, here are my thoughts.

Were I writing a web app, I would have seen MetaRouter as a convenient and functional way to specify the routes. I would also have had some backend that is supposed to work with these routes (I know how to work with Java Servlets, Finch, Scalatra and Play, so I'd probably chose one of these. Not the first one for sure :) ).

The lifecycle of a route you present a user with goes as follows:

  1. Create a route
  2. Create a mapping of a route to a case class
  3. Create a parse table with all your routes
  4. Invoke parse method on that table to generate case classes

Examples in the context of other frameworks

Consider we want to describe a path /details/{int}.

Java Servlets

Everything is fine there. You can define a handler for any request that comes your way, extract the request URL, parse it via the parse table and match on the case class. Another thing is that no Scala programmers really use that.

Finch

The endpoints are specified individually for each URL, you can't specify one endpoint for all. For /details/{int} you will write "details" :: int. And you do not want to parse that to a case class - I would have found it a clatter on Finch. What we want to do instead is handle the request in place. We had experience with Finch and MetaRouter, so we know how it went. Precisely, we ended up folding the HList with path fragments.

Scalatra

Endpoints are specified in some controller, and for our example you would write get("/details/:int") {/* handling code */}. In in Finch we were folding the path to its own format, here we need to fold it to a String, with the framework-specific formatting. Again, I do not see any need for case classes here, I want to handle the request in place.

Play

Even more interesting here, it requires you to have a separate file with all your routes mapped to the controllers. Very heavy approach, if you ask me. Were I using MetaRouter here, I would expect to take the entire List of routes I have and fold it into that file, on compile time. Naturally I do not want the mapping to case classes here either - what I want instead is the controllers with the handling code to be generated for me.

Conclusion

The approach MetaRouter presets to users has an implication that the web framework the users are using has a handler method that will handle all the requests, then the control is given up to MetaRouter.

However, this is a scenario valid for the low-level frameworks like Servlets - most people will use something higher-level, and they may have their own ways of specifying handlers.

Also, mapping requests to case classes is heavy, I personally would not want to define extra classes just for the requests, as it clutters the code.

Architectural improvements

Let's see how the lifecycle above could be adapted to make a seamless integration with these frameworks possible:

1. Create a route

The routes in their current form are good: all the elements are reified into a HList, so that they can be easily accessible.

2. Create a mapping of a route to a case class

More often than not we want another operation to be performed instead - not a mapping to a case class, but a handling function execution. Case class is just a special case of handling an incoming request - (id: Int) :: HNil => Details(id). So why have Router.route generate a MappedRoute infrastructure, if we can have an arbitrary function there, resulting in a HandledRoute?

3. Create a parse table with all your routes

This is good, because all the routes end up in the same data structure and are easily accessible from the outside world.

4. Invoke parse method on that table to generate case classes

parse method will hardly ever be invoked in high-level frameworks, since the framework takes care of parsing the URL. Instead, we want to collapse this parse table into a set of instructions for the framework on how to handle the requests.

Theory

What I am talking about here is a more "free" approach to things:

  1. Define your routes in a structure, describing them perfectly (as in HLists and case classes, where all the members and types are visible), but not enforcing you to do things in a particular way (as in mapping stuff to case classes and then parsing URLs via the parse table).
  2. Define capabilities to collapse these data structures to something concrete, like fold in the current implementation - catamorphisms - see this paper. This "something concrete" is a set of instructions for the target framework on how to do things.

Practice

Let's see how to do best our /details/{int} example, so that it is usable from the frameworks described above.

Step 1 from Theory:

The route becomes Route(GET, "details" :: Arg[Int] :: HNil), as it is now.

Moreover, we also want to encapsulate the handling code here - instead of Router.route[CaseClass](ourRoute), we want the actual handling logic encapsulated here:

val route = MappedRoute(Route(GET, "details" :: Arg[Int] :: HNil), (id: Int) :: HNil => FlatMapped(GetModelFromDB(id), model => ReturnTemplate(model))

val table = RouteTable(route :: HNil)

(Free monad goes after =>, probably also doable with an effectful monad. No problem if we don't need the handlers here and actually need just a case class as in the current implementation - just use Route, see Java Servlet example below).

Note how we have managed to describe both the route and the handling logic without depending on any framework. As well as to unify all our routes (one) into a table, which is a wrapper over HList.

Naturally in the real world people usually stick to a single framework (maybe). But for me, this framework independency is for reducing the mental load of learning a new framework foremost: you first define what you want to do, and then take your time to learn how to do it with the framework in question.

Step 2 from Theory

Now let's see how the above works out with the four frameworks from the previous section. For every framework, we need to collapse table (apply a catamorphism) to something the framework will understand.

Java Servlets

The current implementation works fine, let's see how to mimic it.

trait Mapping
case class Details(id: Int) extends Mapping

How the catamorphism will map things:

  • Route --> String => Option[Mapping] - a route optionally can match a string, and if the match succeeds, the case class is returned (more type safety needed here, Mapping should be Details)
  • MappedRoute(route, handler) --> (String => Option[Mapping], Mapping => Unit) - if you have already associated a handler with the mapping.
  • RouteTable(mappedRoutes) --> String => Unit - optionally handle the mapped routes
  • RouteTable(nonMappedRoutes) --> String => Option[Mapping] - if you have not specified the handlers, but need the case classes instead.

Finch

  • Route(GET, "details" :: Arg[Int]) --> get("details" :: int): Endpoint
  • MappedRoute(route, handler) --> collapseRoute(route).apply(handler): Endpoint
  • RouteTable(routes <: HList) --> routes.map(collapseRoute) match { case r1 :: r2 :: ... :: HNil => r1 :+: r2 :+: ... }: Endpoint

Scalatra

  • Route(GET, "details" :: Arg[Int]) => "details/:int": String
  • MappedRoute(route, handler) => get(collapseRoute(route)) { handler } (get apparently is effectful in Scalatra; and translate that GET into get() somehow, I did not think that far, but should not be hard)
  • RouteTable(routes) => routes.foreach(collapse) - effectfulness is evil, but that's what the framework wants.

Play

  • Route(GET, "details" :: Arg[Int]) => "GET /details/:int Handlers.firstHandler" - the entry of the routes file.
  • MappedRoute(route, handler) => inject the def firstHandler = {handler} method in the synthetic Handlers class - probably from a macro. Play is heavy.
  • RouteTable(routes) => routes.map(collapseRoute).mkString("\n") - and then this string becomes the routes file.

Akka support

There should be a JVM counterpart to the JavaScript router. The routes don't rely on any JavaScript functionality and must be reusable on the server. We should consider creating a sub-project "metarouter-akka" that converts the routing table into akka-http Routes.

Reduce url string parsing burden

It looks like each unapply that is used to for a route parses the url independently of others. While I don't think this is a huge burden, if this is true, I'm claiming it would be nice to have a way to parse the url string once and then re-use that parsing in each unapply (case statement).

Support hash routing

I did not see any hash routing support so I'm making a feature request for hash routing support.

Long arguments

Add to Arg:

  implicit case object LongArg extends ParseableArg[Long] {
    override def urlDecode(s: String) = Try(s.toLong).toOption
    override def urlEncode(s: Long) = s.toString
  }

Query parameters

We should support query parameters. Proposal:

val export = Root / "export" / Arg[Long] ? Param[Long]("user") & Param[Option[Int]]("limit")

This will match:

  • /export/42?user=23
  • /export/42?user=23&limit=10

Match error for 3 or more Params

compile error when 3 or more params

    val details = Root / "details" & Param[String]("engine") & Param[String]("device") & Param[String]("query")
    val s       = "/details?engine=google&device=mobile&query=covid19"
    s match {
      case details(engine, device, query) =>
        println(s"engine=$engine")
        println(s"device=$device")
        println(s"query=$query")
      case _ =>
    }
  }

Result:

too many patterns for trait Route offering ((String, String), String): expected 2, found 3
case details(engine, device, query) =>

And if I change the code like below

    val details = Root / "details" & Param[String]("engine") & Param[String]("device") & Param[String]("query")
    val s       = "/details?engine=google&device=mobile&query=covid19"
    s match {
      case details(x, y) =>
        println(s"x=$x")
        println(s"y=$y")
      case _ =>
    }

Result:

x=(google,mobile)
y=covid19

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.