Routes with optional parameter - Play 2.1 Scala
Asked Answered
M

5

15

So in Play 2.0 I had this:

GET     /tasks/add              controllers.Tasks.addTask(parentId: Option[Long] = None)
GET     /tasks/:parentId/add    controllers.Tasks.addTask(parentId: Option[Long])

With a controller method like this:

def addTask(parentId: Option[Long]) = Action { 
    Ok(views.html.addTask(taskForm, parentId))  
}

And it was working. As I migrated to 2.1, it seems to complain about these lines with: No URL path binder found for type Option[Long]. Try to implement an implicit PathBindable for this type. Basically, what I am trying to accomplish is to have route tasks/add and route tasks/123/add link to the same method that accepts an Optional[Long]. Any idea how to do this? Thanks.

Ok, so I got a kind of it's not a bug, it's a feature response on Lighthouse: "We removed Option[Long] support in path bindables since it doesn't make sense to have a optional path parameter. You can implement your own path bindable that supports it if you please." So far I have 2 solutions, passing -1 as parentId, which I do not really like. Or having 2 different methods, which probably makes more sense in this case. Implementing the PathBindable doesn't seem too feasible right now, so I will probably stick with having 2 methods.

Maundy answered 20/2, 2013 at 13:23 Comment(0)
H
15

Play 2.0 supported Option in path parameters, Play 2.1 no longer supports this, they removed the PathBindable for Option.

One possible solution would be:

package extensions
import play.api.mvc._
object Binders {
  implicit def OptionBindable[T : PathBindable] = new PathBindable[Option[T]] {
    def bind(key: String, value: String): Either[String, Option[T]] =
      implicitly[PathBindable[T]].
        bind(key, value).
        fold(
          left => Left(left),
          right => Right(Some(right))
        )

    def unbind(key: String, value: Option[T]): String = value map (_.toString) getOrElse ""
  }
}

And add this to Build.scala using routesImport += "extensions.Binders._". Run play clean ~run and it should work. Reloading the Binders on the fly only sometimes works.

Hyozo answered 20/2, 2013 at 16:26 Comment(11)
Thanks, I have reported it. Do you have any idea how to get the desired behaviour without having to wait for a bugfix?Maundy
Ok, so: "We removed Option[Long] support in path bindables since it doesn't make sense to have a optional path parameter. You can implement your own path bindable that supports it if you please." But passing an Optional instead of some arbitrary value seems to me a lot nicer. Isn't that the reason we have optionals in the first place? Maybe I will rather create 2 different methods in this case, as it would make more sense than passing -1 as id.Maundy
Yes you are right, that's much better. I don't understand their decision, though, why remove something which was allowing for nicer URLs?Hyozo
Btw. implementing the path bindable is not very difficult, check out my post about advanced routes.Hyozo
Not sure, I just got what I posted above with a suggestion that it is not a bug that I am raising, but a question... But I would say deleting something that was working without any information about doing so is pretty much a bug... Anyway, thanks for the link, I will have a look at it.Maundy
I tried to implement the PathBindable[Option[Long]], but for some reason it completely ignores it and still ends with the same error. I pretty much copied the code from your blog, changing where necessary, but no success so far. Does it work in 2.1 as well?Maundy
I changed my answer accordingly.Hyozo
For anyone else who, like me, was interested in reading the advanced routes post by @MariusSoutier the link above is no longer valid but it can now be found at mariussoutier.com/blog/2012/12/11/…Ultimatum
After I add this object Binders { ... } what will I be able to do?Prevision
I'm not terribly interested in deviating from the expectations of a framework (devs who follow me who know the current framework functionality may have trouble). So it looks like the best option is to have another route that is missing the optional path parameter. The only problem is it looks like I need another endpoint in the controller as well. It doesn't let me pass in a default value like "" for the param going to the controller method.Monopolize
Using query string params for GET requests allows for optional params but for POST you have to structure multiple routes as done in the following post to accomplish "optional params" #23315407Monopolize
T
6

I think you have to add a question mark:

controllers.Tasks.addTask(parentId: Option[Long] ?= None)

Trifocals answered 20/2, 2013 at 14:19 Comment(2)
No that's just for query parameters, not for parts of the path.Hyozo
@Marius makes a good point, but +1 anyway, because you answered the question given in the title: "Routes with optional Parameter"Decongestant
R
5

From this routes-with-optional-parameter the suggestion goes like:

GET   /                     controllers.Application.show(page = "home")
GET   /:page                controllers.Application.show(page)
Redroot answered 20/2, 2013 at 14:31 Comment(4)
I know about this, however, I would like to get around having to pass a default value. The parameter should either contain the id, or nothing. I could pass -1 or something similar to indicate the nothing part, but I would prefer using the Option for that, as it seems a lot nicer to me.Maundy
What if I want to calculate the default parameter at runtime? What should I use as a literal for the default parameter?Prevision
I cannot get this technique to work. I get http 500. I'm using POST. POST /copy/:param1/:param2 @controllers.MyController.copy(param1, param2) POST /copy/:param2 @controllers.MyController.copy(param1 = "", param2). The post request being made is "/copy/somestring"Monopolize
Got it working. As I posted above, it needs to match the answer in this SO #23315407 I'm not sure what I did wrong my first attempt, but it works.Monopolize
B
2

The simple solution to your problem, without having to pass a default value, is to add a simple proxy method that wraps the parameter in an option.

Routes:

GET     /tasks/add              controllers.Tasks.addTask()
GET     /tasks/:parentId/add    controllers.Tasks.addTaskProxy(parentId: Long)

Controller:

def addTask(parentId: Option[Long] = None) = Action { 
    Ok(views.html.addTask(taskForm, parentId))  
}

def addTaskProxy(parentId: Long) = addTask(Some(parentId))
Bait answered 6/8, 2016 at 1:6 Comment(0)
M
1

I had the same thing and more if you specify the pass as GET/foo:id and controllers.Application.foo(id : Option[Long] ?= None) you get an error It is not allowed to specify a fixed or default value for parameter: 'id' extracted from the path on the other side you can do as follows GET/foo controllers.Application.foo(id : Option[Long] ?= None) and it will work expecting that your request looks like as .../foo?id=1

Millburn answered 20/2, 2013 at 17:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.