play framework get parameter in url with dash - scala

In the url the parameter is like this:
email=TTT&first-name=XXX&last-name=YYY
So how could I get the parameter value from the url?
I know if I use the function
def widget(email: String) = Action{ request =>
I want to show all the parameters.
I could get the email . But I cannot name the value as last-name since dashes cannot be in a name of a variable. So how could I get the value of lastname like this? (Suppose I could not change the link since I grab the link from others url)

If you know all parameters you can create form mapping and use fold method to get all values. It works with a GET method as well. In this case it would look like this
Case class for holding url params with form mapping:
import play.api.data.Form
import play.api.data.Forms._
case class UrlForm(email: String, firstName: String, lastName: String)
object UrlForm {
val form = Form[UrlForm](
mapping(
"email" -> text,
"first-name" -> text,
"last-name" -> text
)(UrlForm.apply)(UrlForm.unapply)
)
}
Controller for this form:
object UrlController {
def widget() = Action { implicit request =>
UrlForm.form.bindFromRequest.fold(
formWithErrors => // validation errors
urlForm => urlForm.email; urlForm.lastName //etc.
)
}
}
Notice that you are able to map url params to any type you want. String is just an example.

Related

Error when use akka http unmarshall entity as case class with default value

Error found when I send a http post request:
The request content was malformed: No usable value for gender Did
not find value which can be converted into java.lang.String
My request body:
{
"name":"test"
}
Route in my scala code:
path("test"){
(post(entity(as[People]) { req =>
val resp = queryData(req)
complete(resp.meta.getOrElse("statusCode", 200).asInstanceOf[Int] -> resp)
}))
} ~
Code for People:
case class People(name: String, gender: String = "male")
Why still get the malformed error ???
Even though you put a default value, the extraction of the Json will look for that field, and it is not present there, so it will fail.
(I am assuming you are using spray-json as it's the default one in akka-http)
In order to avoid the issue, while keeping it simple, I would recommend you to create a case class for the request to create people, which contains an Option[String] for that field, and you can then convert the PeopleCreateRequest to a People easily.
case class PeopleCreateRequest(name: String, gender: Option[String])
That will work nicely with the framework...
Alternatively, if you want to keep the design that way, you'll need to look into implementing your own JsonFormat[People] which will treat this value as optional but add a default value when missing.
Look into spray-json https://github.com/spray/spray-json#providing-jsonformats-for-other-types
But I imagine it would be something like:
implicit val peopleFormat = new RootJsonFormat[People] {
def read(json: JsValue): People = json match {
case JsArray(Seq(JsString(name), JsString(gender))) =>
People(name, gender)
case JsArray(Seq(JsString(name))) =>
People(name)
case _ => deserializationError("Missing fields")
}
def write(obj: People): JsValue = ???
}
I am normally using different JsonSupport, using circe, but hopefully this gives you direction to solve your issue

Play2 - validation of dynamic part of route(s)

I'm building an API, that takes in a variable path parameter, or dynamic part of the route, as the play documentation would specify it.
I would like to validate this as to give the client a proper response.
I have the following route setup
GET /:dynamic/all controller.method(dynamic: String)
The dynamic param for the method is used across the API, for multiple methods, so i would like to get some kind of global validation/whitelist of acceptable strings. (eg: "hello"/"hi" would be accepted, and "noooway" would not be accepted, and i would return a 404 not found as response.
I would preferably like my controller method to not contain any validation so that this would be true:
def method(dynamic: String): Action[AnyContent] = Action.async { _ =>
//I already know "dynamic" is valid here.
Future.successful(Ok(Json.toJson(Map("status" -> "OK"))))
}
Instead of: (excuse my javaisc-psuedo-code)
def method(dynamic: String): Action[AnyContent] = Action.async { _ =>
val valid = Helper.validate(dynamic)
if (!valid) return some result/response else
Future.successful(Ok(Json.toJson(Map("status" -> "OK"))))
}
Play allows you to do this by different ways.
1. PathBindable
You can implement a PathBindable[T] for any type T, so that your value extracted from the path of the request is not a simple String but a T.
If you are ready to change the type of dynamic (which would make sense, since it is not supposed to be just any string but a valid one), you could do the following:
case class Validated(str: String) {
assert(Helper.validate(str))
}
object Validated {
implicit val pathBindable = new PathBindable[Validated] {
val string = implicitly[PathBindable[String]]
override def bind(key: String, value: String): Either[String, Validated] =
string.bind(key, value). // bind as if it were a string
right.filter(Helper.validate).getOrElse(Left("Invalid input")). // filter using your validation function, and give error message
right.map(Validated(_)) // encapsulate in your new type
override def unbind(key: String, value: Validated): String =
string.unbind(key, value.str) //unbind as if it were a string
}
}
Note that you need to implement unbind for reverse routing (get a path for a given action call).
Now, you just need to replace String in your router and in your controller by your.package.Validated.
GET /:dynamic/all controller.method(dynamic: your.package.Validated)
NB: if you want to use the simple name of your class, you need to import it in your build.sbt:
(project in file(".").
enablePlugins(PlayScala).
settings(routesImport += "your.package.Validated")
2. Action Composition
You can also implement an action filter to be used whenever your input needs to be validated:
case class ValidatedAction(input: String) extends ActionFilter[Request] {
override protected def filter[A](request: Request[A]): Future[Option[Result]] = Future.successful{
if (Helper.validate(input)) None else Some(BadRequest("Invalid input"))
}
}
def method(dynamic: String) = (Action andThen ValidatedAction(dynamic)).async {
Future.successful(Ok)
}
The code inside the async block will be executed only if the filter method returns None, otherwise, it will return the specified Result (here, BadRequest("Invalid input").

Add Prefix to Reverse Routes

I am looking for a way to add a prefix to all reverse routes of an html template without wrapping them all in function applications. I planned to use an html base tag but the reverse routes all start with a slash and therefore are relative to the host of the base tag rather than the full URL. Is there any feature I might be missing for a more robust solution?
rename routes to context.routes
create empty routes file
add to routes the line:
-> /context context.Routes
To accomplish something like this, to facilitate communication to my app of my app's own URLs in a DRY way, to propagate query parameters debug settings used in development through internal links, and also because I find Play's reverse router to be super ugly, I created a URL abstraction I like a lot better. It works like this:
1) I created a type to represent URLs symbolically:
/** Represents a URL that can possibly be a webpage */
sealed trait PageUrl
case class SectionUrl(slug: String) extends PageUrl
case class StaticPageUrl(slug: String) extends PageUrl
case class ExternalUrl(url: String) extends PageUrl
2) I created a class for resolving these objects into full URLs:
/** Wrapper to propagate request override flags to internal links */
case class UrlWrapper(params: Seq[(String, String)]) {
def apply(url: PageUrl, additionalParams: Seq[(String, String)] = Seq.empty): String = {
url match {
case SectionUrl(slug) => urlAndParams(routes.PageRendererController.showSectionPage(slug).url)
case StaticPageUrl(slug) => urlAndParams(routes.PageRendererController.showStaticPage(slug).url)
case ExternalUrl(u) => u
}
}
def urlAndParams(url: String, additionalParams: Seq[(String, String)] = Seq.empty): String = {
def urlEncode = (u: String) => java.net.URLEncoder.encode(u, "UTF-8")
val formattedParams = (queryParams ++ additionalParams).map{ case (key, value) => s"$key=${urlEncode(value)}" }.mkString("&")
val paramOption = if (formattedParams.nonEmpty) Some(formattedParams) else None
(Seq(url) ++ paramOption).mkString(if (url.indexOf("?") > 0) "&" else "?")
}
}
You could easily modify this to provide a prefix always by default, or upon request via some other method.
3) In a trait/class that all my views extend, I declare an implicit field of type UrlWrapper, that will be available to my templates, so I can do:
#(option1: String, urlParam: PageUrl)(implicit url: UrlWrapper)
...
My link
...
As a bonus, since my pages all correspond to models in my app, I added to UrlWrapper additional methods for converting model objects to resolved URLs:
case class UrlWrapper(...) {
...
def forSection(section: Section, additionalParams: Seq[(String, String)] = Seq.empty): String = {
apply(SectionUrl(section.slug), additionalParams)
}
def forStaticPage(staticPage: StaticPage, additionalParams: Seq[(String, String)] = Seq.empty): String = {
apply(StaticPageUrl(staticPage.slug), additionalParams)
}
}

type mismatch; found : Int required: String when trying to persist the form data in play framework

I have a model as follows:
case class User(username: String, email: String) {
var id:Int = User.nextId
}
object User {
protected var currentId = 0
def nextId: Int = {
currentId += 1
currentId
}
}
And my Controller looks like this:
object Users extends Controller {
val users: scala.collection.mutable.Map[String, User] = new HashMap
val form = Form(
mapping(
"username" -> text,
"email" -> text
)(User.apply)(User.unapply))
def add = Action {
Ok(views.html.users.add(form))
}
def save = Action{
implicit request =>
val user = form.bindFromRequest.get
users.put(user.id, user)
Ok("contact saved")
}
}
But when I compile the code my browser throws an error telling there is a miss match in the type that is accepted by the users.put(user.id, user).
The error message looks as follows:
When I change the type of the id as string then it will work but I want to keep my id as integer and not string.
Is there any way I can achieve this. I am using play framework 2.11.1.
What is that I am missing. Any help would be greatly appreciated since I am new to play framework.
users is a map from String to User.
val users: scala.collection.mutable.Map[String, User] = new HashMap
The put method expects a key of type String and a value of type User, but you tried to use it with a key of type Int.
You need to change your map to the type
scala.collection.mutable.Map[Int, User]
or instead you could use a value of type String as key.

How to init case class from one JSON string instead of separate fields of form

New to the Play framework.
After reading the doc, I know the standard way to initialize case class by using form like this:
import play.api.data._
import play.api.data.format.Formats._
val userForm = Form(
mapping(
"name" -> of[String],
"age" -> of[Int],
"email" -> of[String]
)(User.apply)(User.unapply)
)
in this case, we provide separate textboxes for user to fill in in the web interface, after submit, the server code will init User according to the form. Works Great!
but, what if what I wanna provide is only one textbox, and user(actually the mobile guy who wanna test the web service API) should fill in a complete JSON string and then post it to server to create a User instance?
Would appreciate if you can provide both the view & controller code.
Taking your question literally (creating an object via JSON from a form field), it could look something like below. For completeness I've also provided the direct (and much simpler) way to unmarshall an object from JSON in a controller action (using content type application/json instead of application/x-form-urlencoded).
To extract a custom type (in your case a User object) from a form field you need to write a mapper to bind/unbind the data to/from a string. The userJsonMapper in the User's companion object does this, giving an error (error.jsonObj, for which you can provide an i18n translation) if JSON parsing fails.
The model code app/models/User.scala:
package models
case class User(name: String, age: Int, email: String)
object User {
// Create a generic JSON format using the macro. NB: This
// does *NOT* include validation of the email or age fields.
import play.api.libs.json.{Format, Json}
implicit val format: Format[User] = Json.format[User]
// Create a form and a field binder that validates the contents
// of the "json" field to ensure it is valid JSON for a user.
import play.api.data.Form
import play.api.data.Forms._
import play.api.data.FormError
import play.api.data.format.Formatter
// The actual form object
val jsonForm: Form[User] = Form(
single("json" -> of(userJsonMapper))
)
// Map a User object to/from a form field
def userJsonMapper: Formatter[User] = new Formatter[User] {
def bind(key: String, data: Map[String, String]) = {
play.api.data.format.Formats.stringFormat.bind(key, data).right.flatMap { s =>
scala.util.control.Exception.allCatch[User]
.either(Json.parse(s).as[User])
.left.map(e => Seq(FormError(key, "error.jsonObj", Nil)))
}
}
def unbind(key: String, value: User) = Map(key -> Json.stringify(Json.toJson(value)))
}
}
The template code app/views/createUserForm.scala.html:
#(form: Form[User], action: Call)(implicit request: RequestHeader)
<html>
<body>
#helper.form(action = action, 'role -> "form") {
#helper.inputText(form("json"))
<button type="submit">Create User</button>
}
</body>
</html>
The route file conf/routes:
GET /createUser controllers.Users.createUser
POST /createUser controllers.Users.createUserPost
# For good measure, include an action to create
# a user directly from JSON via a JSON content-type.
POST /createUserJson controllers.Users.createUserPostJson
The controller code app/controllers/Users.scala:
package controllers
import play.api.mvc._
import models.User
object Users extends Controller {
def createUser = Action { implicit request =>
Ok(views.html.createUserForm(
User.jsonForm,
controllers.routes.Users.createUserPost()
))
}
def createUserPost = Action { implicit request =>
User.jsonForm.bindFromRequest.fold(
// Form data is bad - re-render form.
errForm => BadRequest(views.html.createUserForm(
errForm,
controllers.routes.Users.createUserPost()
)),
user =>
// Create user here
Ok(s"User: $user")
)
}
}
// A more direct way of doing this is to POST the data
// as application/json and unmarshall it directly.
def createUserPostJson = Action(parse.json) { implicit request =>
request.body.validate[User].fold(
errs => BadRequest(play.api.libs.json.JsError.toFlatJson(errs)),
user => Ok(s"User: $user")
)
}
After reading this thread: Scala Play form validation: different forms for one case class - is it possible?, figured out a way to do what I want.
The canonical way of extracting fields from form to create an instance is like this:
import play.api.data._
import play.api.data.format.Formats._
val userForm = Form(
mapping(
"name" -> of[String],
"des" -> of[String],
...
)(User.apply)(User.unapply)
)
here we create a form and then bind the target object's apply and unapply methods to it, thanks to these methods, the form now has the ability to extract fields and turn them into Object.
What I wanna do is a form which can turn a JSON string(instead of separate fields) into an object, then why not provide the form with functions like this:
json:String => User
yes, additional to User's apply&unapply function, we add:
def applyStr(jsonStr: String): User = {
val json: JsValue = Json.parse(jsonStr)
val validateResult: JsResult[User] = json.validate[User](UserReads)
validateResult match{
case s: JsSuccess[User] => s.get
case e: JsError => null
}
}
def unapplyToStr(user: User): Option[String] = {
Some(Json.toJson(user).toString())
}
and create a form like this:
val userJSONForm = Form(
mapping(
"json" -> text
)(User.applyStr)(User.unapplyToStr)
)
now we have a form which can turn json string into User, what left is familiar, we create an action in controller to display form:
def createByJson() = Action {
Ok(views.html.user_json(userJSONForm))
}
the corresponding view is like:
#(userJsonForm: Form[User])
#helper.form(action = routes.UserController.saveJson()) {
#helper.textarea(userJsonForm("json"))
<input type="submit" />
}
after submit, the form will be delivered to UserController.saveJson():
def saveJson() = Action{implicit request =>
userJSONForm.bindFromRequest.fold(
formWithErrors => {
BadRequest(views.html.user_json(formWithErrors))
},
user => {
//do sth to the user
...
Ok("get user")
}
)
}