Comments (6)
Thanks for the thorough analysis! I like your idea of considering the case class
mapping just as a special use case, and find an abstraction for it.
The main advantages for choosing case class
were:
- They enforce named arguments
- If we have a set of routes, we can match them easily
- We can map them back to an URL while enforcing correct types
- IDE support
One thing we need to explore more is the case where we map from a route back to an URL. I would like to retain the possibility to do this for case class
es with your proposed architecture. Let's consider this example:
val details = Route("details" :: Arg[Int] :: HNil) // Or shorter: Root / "details" / Arg[Int]
While we can instantiate an URL via Router.url(details, 42 :: HNil)
, we do not know the names of the arguments.
I would argue against including the HTTP method in the route. This library may be used for other protocols like FTP which do not have the notion of GET/POST requests.
Furthermore, I would like to propose the following syntactic changes:
val details = Router.route(Root / "details" / Arg[Int], (i: Int) => i.toString)
val userInfo = Router.route(Root / "user" / Arg[String], (u: String) => u)
val table = Router.table(details :: userInfo :: HNil)
val run1 = Router.run(details, "/details/42") // "42"
val run2 = Router.run(table, "/user/test") // "test"
It is important that the compiler is able to infer the correct types, e.g. it should be possible to know beforehand that run1
and run2
are strings.
from trail.
val run1 = Router.run(details, "/details/42") // "42"
How would you turn that into a Finch/Scalatra/Play handler?
One thing we need to explore more is the case where we map from a route back to an URL. I would like to retain the possibility to do this for case classes with your proposed architecture. Let's consider this example:
We'll just need to have two functions: Route => (String => CaseClass)
and Route => (CaseClass => String)
.
For me, there are two real problems:
- Inability to integrate MetaRouter with other web frameworks
- Case classes are heavy
Personally, for me the fact that you need to do at least three operations to create a new route (the route itself, the case class and the mapping between the two) overrides the benefits you named. It may seem like nothing, but every time you want to define a new route, you need to remember all these operations, and where the code for each should go. This creates mental overhead for such a purely declarative task. If a framework distracts me from my primary task, this is a big disadvantage. IMO case classes should be an option, not an enforced style.
from trail.
How would you turn that into a Finch/Scalatra/Play handler?
The definition of Route
would stay the same, so the user could write a regular route and then call fold
in place:
val r = Root / "details" / Arg[String] / Arg[Int]
val f = ... // Rewrite route for Scalatra
Router.fold(r, f) // "/details/:string/:int"
We'll just need to have two functions:
Route => (String => CaseClass)
andRoute => (CaseClass => String)
.
What do you think about changing MappedRoute
to:
case class MappedRoute[ROUTE <: HList, Args <: HList, Result](route: Route[ROUTE], f: Args => Result)
Then we could write:
val r = Root / "details" / Arg[String] / Arg[Int] // R = Route[String :: Arg[String] :: Arg[Int] :: HNil]
Router.url(r, "test" :: 42 :: HNil) // "/details/test/42"
case class Details(s: String, i: Int)
val m = Router.route(r, Details.apply) // MappedRoute[R, String :: Int :: HNil, Details]
Now we could introduce the route product a little differently:
case class RouteProduct[Results <: HList, Routes <: HList](results: Results, routes: Routes)
This maps each result type to its corresponding route. An alternative would be to use an HMap
here.
val p = Router.product(m :: HNil) // RouteProduct[Details :: HNil, R :: HNil]
Finally, we can overload url
with RouteProduct
:
Router.url(p, Details("test", 42)) // "/details/test/42"
While doing my recent refactoring, I was wondering whether we should eliminate RouteData
and the fill
operation altogether. What is your take on this?
from trail.
What do you think about changing MappedRoute to:
Looks good.
If we have fold
for a simple Route
, we need a similar function for a MappedRoute
. In case of Scalatra, we must first do fold
on the Route
to get "/details/:int"
, then apply another function to register a handler: get("details/:int") { handler }
. The question is, do we need a framework support for this pattern (handler function composed with the fold catamorphism), or will it be convenient for the user to do perform the pattern by hand?
Finally, we can overload url with RouteProduct:
This def url
considers case classes as a special case. How about something like this:
case class IsoMappedRoute[ROUTE <: HList, Args <: HList, Result](route: Route[ROUTE], f: Args => Result, g: Result => Args)
def Route.url(route: IsoMappedRoute, r: route.Result): String = {
val args = route.g(r)
Route.url(route.route, args)
}
def Route.url(route: Route, args: Args): String = ???
val route: IsoMappedRoute = Router.route(r, Details.apply, Details.unapply)
val path = Route.url(route, Details(42))
While doing my recent refactoring, I was wondering whether we should eliminate RouteData and the fill operation altogether. What is your take on this?
Probably; if one does need such a functionality, it is just a MappedRoute[Route, Args, FilledRoute](originalRoute, args => substituteArgsIntoRoute(originalRoute, args))
.
By the way, all these Route
, MappedRoute
, and IsoMappedRoute
should have the same parent and IMO these classes should be hidden from the user. Route
can become RawRoute
:
trait Route[A]
case class RawRoute[Path <: HList, Args <: HList] extends Route[Args] // Raw route's ultimate goal is to extract arguments
case class MappedRoute[A, B](route: Route[A], f: A => B) extends Route[B]
case class IsoMappedRoute[A, B](route: Route[A], f: A => B, g: B => A) extends Route[B]
implicit val f1: Functor[Route] = ???
implicit val f2: Invariant[Route] = ??? // https://static.javadoc.io/org.typelevel/cats-core_2.11/0.9.0/index.html#cats.functor.Invariant
val route = Root / "details" / Arg[Int]
// Warning: Functor <: Invariant, collision will happen here
val mapped = route.imap(route, Details.apply, Details.unapply)
val fromDetails: String = Route.url(mapped, Details(42))
val toDetails: Details = Route.parse(mapped, "/details/42")
val handler: FinchHandler = Route.toHandler(mapped, folder, handler: Details => Unit)
from trail.
Instead of encoding the operations in MappedRoute
, we could also express the routing table as an HList
and provide a parse
method that can be used as follows:
val details = Root / "details" / Arg[String] / Arg[Int]
val routes = details :: HNil
Router.parse(routes, uri).map {
case RouteData(details, s :: i :: HNil) => renderDetails(s, i)
}
This way, we can retain the current functionality of mapping routes to case class
es. Would that be a satisfying solution?
from trail.
Instead of encoding the operations in MappedRoute, we could also express the routing table as an HList
That's a nice idea: no need to define a class if you can do with HList
.
This way, we can retain the current functionality of mapping routes to case classes. Would that be a satisfying solution?
Looks good.
from trail.
Related Issues (17)
- Get rid of casts
- Query parameters HOT 3
- Cast needed when calling `url` on root node
- Design a DSL
- Scala Native support
- doc link is broken in README.md HOT 1
- bump in dependencies to get cats to above 1.0 HOT 1
- Support hash routing HOT 1
- Reduce url string parsing burden HOT 4
- no "empty" (zero) path segment HOT 4
- Akka support
- Routes match more than advertised HOT 2
- Scala.js 1.0 support
- Write manual
- Match error for 3 or more Params HOT 2
- Long arguments
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from trail.