I'm writing an endpoint in a play scala application that makes a request to spotify, searching across track, album and artist types. I want to make over them and transform the string's into Future's of the calls.
This is my code:
def index = Action.async { implicit request =>
val futures = List("track", "album", "artist")
.map { type => performSearch("q" -> param(request, "q"), "type" -> type) }
Future.sequence(futures).onComplete {
Ok
}
}
private def performSearch(criteria: (String, String)): Future = {
ws.url("https://api.spotify.com/v1/search")
.withQueryString(criteria)
.get()
}
private def param(request: Request[AnyContent], name: String): String = {
request.queryString.get(name).flatMap(_.headOption).getOrElse("")
}
However I'm getting the error in my map of:
identifier expected but '=>' found
// .map { type => performSearch("q" -> param(request, "q"), "type" -> type) }
type is a keyword. Pick something else or put it inside `:
.map { `type` => performSearch("q" -> param(request, "q"), "type" -> `type`) }
Related
Is it possible to make pureconfig read properties as Map[String, String]? I have the following
application.conf:
cfg{
some.property.name: "value"
some.another.property.name: "another value"
}
Here is the application I tried to read the config with:
import pureconfig.generic.auto._
import pureconfig.ConfigSource
import pureconfig.error.ConfigReaderException
object Model extends App {
case class Config(cfg: Map[String, String])
val result = ConfigSource.default
.load[Config]
.left
.map(err => new ConfigReaderException[Config](err))
.toTry
val config = result.get
println(config)
}
The problem is it throws the following excpetion:
Exception in thread "main" pureconfig.error.ConfigReaderException: Cannot convert configuration to a Model$Config. Failures are:
at 'cfg.some':
- (application.conf # file:/home/somename/prcfg/target/classes/application.conf: 2-3) Expected type STRING. Found OBJECT instead.
at Model$.$anonfun$result$2(Model.scala:11)
at scala.util.Either$LeftProjection.map(Either.scala:614)
at Model$.delayedEndpoint$Model$1(Model.scala:11)
at Model$delayedInit$body.apply(Model.scala:5)
at scala.Function0.apply$mcV$sp(Function0.scala:39)
at scala.Function0.apply$mcV$sp$(Function0.scala:39)
at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:17)
at scala.App.$anonfun$main$1(App.scala:73)
at scala.App.$anonfun$main$1$adapted(App.scala:73)
at scala.collection.IterableOnceOps.foreach(IterableOnce.scala:553)
at scala.collection.IterableOnceOps.foreach$(IterableOnce.scala:551)
at scala.collection.AbstractIterable.foreach(Iterable.scala:920)
at scala.App.main(App.scala:73)
at scala.App.main$(App.scala:71)
at Model$.main(Model.scala:5)
at Model.main(Model.scala)
Is there a way to fix it? I expected that the Map[String, String] will contain the following mappings:
some.property.name -> "value"
some.another.property.name -> "another value"
Your issue is not pureconfig. Your issue is that by HOCON spec what you wrote:
cfg {
some.property.name: "value"
some.another.property.name: "another value"
}
is a syntactic sugar for:
cfg {
some {
property {
name = "value"
}
}
another {
property {
name = "another value"
}
}
}
It's TypeSafe Config/Lightbend Config who decides that your cfg has two properties and both of them are nested configs. Pureconfig only takes these nested configs and maps them into case classes. But it won't be able to map something which has a radically different structure then expected.
If you write:
cfg {
some-property-name: "value"
some-another-property-name: "another value"
}
You'll be able to decode "cfg" path as Map[String, String] and top level config as case class Config(cfg: Map[String, String]). If you wanted to treat . as part of the key and not nesting... then I'm afraid you have to write a ConfigReader yourself because that is non-standard usage.
You can read a Map[String, String] in that way with the following ConfigReader:
implicit val strMapReader: ConfigReader[Map[String, String]] = {
implicit val r: ConfigReader[String => Map[String, String]] =
ConfigReader[String]
.map(v => (prefix: String) => Map(prefix -> v))
.orElse { strMapReader.map { v =>
(prefix: String) => v.map { case (k, v2) => s"$prefix.$k" -> v2 }
}}
ConfigReader[Map[String, String => Map[String, String]]].map {
_.flatMap { case (prefix, v) => v(prefix) }
}
}
Note that this is a recursive val definition, because strMapReader is used within its own definition. The reason it works is that the orElse method takes its parameter by name and not by value.
There is a trait which works perfectly. However, I would like to refactor the part related to generic [T] in order to limit the data type which could be accepted by generic [T] (I need only Option[JsValue] , JsValue , StringEnumEntry , String ). Is it possible to solve this problem through shapeless coproduct? Maybe there are other solutions?
trait ParameterBinders extends Log {
def jsonBinder[T](json: T, jsonType: java.lang.String = "json"): ParameterBinderWithValue = {
val jsonObject = new PGobject()
jsonObject.setType(jsonType)
json match {
case json: Option[JsValue] =>
jsonObject.setValue(json.map(Json.stringify).orNull)
case json: JsValue =>
jsonObject.setValue(Json.stringify(json))
case json: StringEnumEntry =>
jsonObject.setValue(json.value)
case json: String =>
jsonObject.setValue(json)
case _ =>
logger.error("unexpected data type ")
}
if (jsonType == "JSONSCHEMATYPE" || jsonType == "SYSPROPERTYTYPE") {
ParameterBinder(this, (ps, i) => {
ps.setObject(i, jsonObject)
})
} else {
ParameterBinder(json, (ps, i) => {
ps.setObject(i, jsonObject)
})
}
}
}
The easiest way is to use an ADT as described in the link of the first comment.
If you don't want to change the types that are accepted in jsonBinder then you can solve the problem by using a typeclass.
e.g.
trait JsonBindValue[T] {
def value(t: T): String
}
you would then have to provide instances for your accepted datatypes
object JsonBindValue {
implicit val OptJsBinder = new JsonBindValue[Option[JsValue]] {
def value(t: Option[JsValue]): String = {
t.map(Json.stringify).orNull
}
}
... more instances here
}
finally your function would look like this:
def jsonBinder[T : JsonBindValue](json: T, jsonType: java.lang.String = "json"): ParameterBinderWithValue = {
val binder = implicitly[JsonBindValue[T]]
jsonObject.setType(jsonType)
jsonObject.setValue(binder.value(json))
...
}
if you call the function without a implicit instance in scope you will get a compile time error.
This Action.async method in Play for Scala should execute the second future only if the first future returns 1, that's why they are nested. If the first future returns a result different than 1 then the second future should never be executed. But I'm getting the following compilation error in f2.map. Why is this error and how to fix it?
type mismatch; found :
scala.concurrent.Future[scala.concurrent.Future[play.api.mvc.Result]]
required: play.api.mvc.Result
def index = Action.async { request =>
val f1 = Future {1}
f1.map {
access => if (access==1) {
val f2 = Future {2}
f2.map { // <-- compilation error
result => {
val json = JsObject(Seq(
"acc" -> JsNumber(access),
"ret" -> JsString("0")
))
Ok(json)
}
}
}
else {
val json = JsObject(Seq(
"acc" -> JsNumber(0),
"ret" -> JsString("0")
))
Future.successful(Ok(json))
}
}
}
You're basically there - just flatMap on f1 instead of map since you're creating another future.
I'm using Play 2.5 and Scala 2.11. I'm trying to use async Action with Future to return json string but can't get it to work.
Here is my action :
def getData = Action.async { implicit request =>
val futureData = Future { daoObj.fetchData }
futureData onComplete {
case Success(data) =>
Ok(Json.prettyPrint(Json.obj("data" -> data.groupBy(_.name))))
case Failure(t) =>
BadRequest(Json.prettyPrint(Json.obj(
"status" -> "400",
"message" -> s"$t"
)))
}
}
The daoObj.fetchData returns scala.concurrent.Future[List[models.Data]]
Here is the models.Data :
case class Data(name: String, details: String)
I can have data like this
Data (name = "data1", details = "details1")
Data (name = "data1", details = "details1a")
Data (name = "data2", details = "details2")
that I can join on name to return a structure of the form
Map(data1 -> List("details1", "details11"),
data2 -> List("details2"))
I have a compilation error on groupBy :
value groupBy is not a member of scala.concurrent.Future[List[models.Data]]
1) How to get the value (List[models.Data]) from the Future in my action ?
2) It's my first Play Scala app so if you have any comment to improve my code, it's welcome.
You should not use Future.onComplete, which is a callback (with side-effect), but .map and .recover to turn your Future[T] into a Future[Result] which is what's expected by Action.async.
def getData = Action.async { implicit request =>
Future { daoObj.fetchData }.map { data =>
Json.obj("data" -> data.groupBy(_.name))
}.recover {
case error => BadRequest(Json.obj(
"status" -> "400",
"message" -> error.getMessage
))
}
}
There is no need to pretty-print the JsObject which can be directly written as result (except for debugging purposes maybe).
I'm trying to write a DRY CRUD restful service using PlayFramework. Here's the code for it.
def crudUsers(operation: String) = Action(parse.json) { request =>
(request.body).asOpt[User].map { jsonUser =>
try {
DBGlobal.db.withTransaction {
val queryResult = operation match {
case "create" =>
UsersTable.forInsert.insertAll(jsonUser)
Json.generate(Map("status" -> "Success", "message" -> "Account inserted"))
case "update" =>
val updateQ = UsersTable.where(_.email === jsonUser.email.bind).map(_.forInsert)
println(updateQ.selectStatement)
updateQ.update(jsonUser)
Json.generate(Map("status" -> "Success", "message" -> "Account updated"))
case "retrieve" =>
val retrieveQ = for(r <- UsersTable) yield r
println(retrieveQ.selectStatement)
Json.generate(retrieveQ.list)
case "delete" =>
val deleteQ = UsersTable.where(_.email === jsonUser.email)
deleteQ.delete
Json.generate(Map("status" -> "Success", "message" -> "Account deleted"))
}
Ok(queryResult)
}
} catch {
case _ =>
val errMsg: String = operation + " error"
BadRequest(Json.generate(Map("status" -> "Error", "message" -> errMsg)))
}
}.getOrElse(BadRequest(Json.generate(Map("status" -> "Error", "message" -> "error"))))
}
}
I've noticed that update, delete and create operations work nicely. However the retrieve operation fails with For request 'GET /1/users' [Invalid Json]. I'm pretty sure this is because the JSON parser is not tolerant of a GET request with no JSON passed in the body.
Is there a way to special case the GET/Retrieve operation without losing losing the DRY approach I've started here?
My guess would be that you split your methods so that you can create a different routes for the ones with and without body.
It seems that the design of the code would not work even if the empty string was parsed as JSON. The map method would not be executed because there would be no user. That would cause the match on operation to never be executed.
Update
Since you mentioned DRY, I would refactor it into something like this:
type Operations = PartialFunction[String, String]
val operations: Operations = {
case "retrieve" =>
println("performing retrieve")
"retrieved"
case "list" =>
println("performing list")
"listed"
}
def userOperations(user: User): Operations = {
case "create" =>
println("actual create operation")
"created"
case "delete" =>
println("actual delete operation")
"updated"
case "update" =>
println("actual update operation")
"updated"
}
def withoutUser(operation: String) = Action {
execute(operation, operations andThen successResponse)
}
def withUser(operation: String) = Action(parse.json) { request =>
request.body.asOpt[User].map { user =>
execute(operation, userOperations(user) andThen successResponse)
}
.getOrElse {
errorResponse("invalid user data")
}
}
def execute(operation: String, actualOperation: PartialFunction[String, Result]) =
if (actualOperation isDefinedAt operation) {
try {
DBGlobal.db.withTransaction {
actualOperation(operation)
}
} catch {
case _ => errorResponse(operation + " error")
}
} else {
errorResponse(operation + " not found")
}
val successResponse = createResponse(Ok, "Success", _: String)
val errorResponse = createResponse(BadRequest, "Error", _: String)
def createResponse(httpStatus: Status, status: String, message: String): Result =
httpStatus(Json.toJson(Map("status" -> status, "message" -> message)))