I'm using Scala to make HTTP GET requests to an API (Play Framework's WS, to be exact) which responds with a JSON response that looks like;
{
data: [
{text: "Hello there", id: 1},
{text: "Hello there again", id: 2}
],
next_url: 'http://request-this-for-more.com/api?page=2' //optional
}
So, the next_url field in the returned JSON may or may not be present.
What my method needs to do is start with calling the first URL, check if the response has a next_url and then do a GET on that. In the end, I should have all the data fields from the responses combined into one single future of all the data fields. I terminate when the response has no next_url present in it.
Now, doing this in a blocking way is easier, but I don't want to do that. What is the best way do tackle a problem like this?
There's probably a method to do this somewhere in scalaz, but if you don't know a specific solution it's usually possible to construct one with recursion and flatMap. Something like:
//Assume we have an async fetch method that returns Result and Option of next Url
def fetch(url: Url): Future[(Result, Option[Url])] = ...
//Then we can define fetchAll with recursion:
def fetchAll(url: Url): Future[Vector[Result]] =
fetch(url) flatMap {
case (result, None) => Future.successful(Vector(result))
case (result, Some(nextUrl)) =>
fetchAll(nextUrl) map {results => result +: results}
}
(Note that this uses a stack frame for each call - if you want to do thousands of fetches then we need to write it a little more carefully so that it's tail-recursive)
the Future.flatMap method exists exaclty for cases like that
Suppose you have such things:
case class Data(...)
def getContent(url:String):Future[String]
def parseJson(source:String):Try[JsValue]
def getData(value: JsValue):Seq[Data]
and JsValue type have methods inspired by play json library
def \ (fieldName: String): JsValue
def as[T](implicit ...):T //probably throwing exception
you could compose final result like
def innerContent(url:String):Future[Seq[Data]] = for {
first <- getContent(url)
json <- Future.fromTry(parseJson(first))
nextUrlAttempt = Try((json \ "next_url").as[String])
dataAttempt = Try(getData(json \ "data"))
data <- Future.fromTry(dataAttempt)
result <- nextUrlAttempt match {
case Success(nextUrl) => innerContent(nextUrl)
case Failure(_) => Future.successful(Seq())
} yield data ++ result
Also check out libraries that are targeted for complex asynchronous streams like your one:
play iteratees
scalaz iteratees
scalaz stream
Related
I am new to Scala and having a hard time to handle Future.
I am calling a method which returns Future[Seq[String]]
def getBookingIds(headers: Seq[(String, String)]): Future[Seq[String]] = {
val responseF = legacyService.get(new URL(Settings.Legacy.getBookingURL), headers)
responseF.map { response =>
response.extract[Seq[String]]
}
}
I am calling above method here and need to loop through this list and check if booking exist.
def verifyUserBooking(bookingId: Int, headers: Seq[(String, String)]): Unit = {
bookedCruisesService.getBookingIds(headers).onComplete {
case Success(s) => println()
case Failure(ex) => println(ex.toString)
case _ => println("what the hell happened")
}
I am getting here JsonParseException. Why?
I have used different methods to loop through future list, but I keep getting Future in debug more.
Can someone help me to solve this issue, please.
Thank you.
It sounds obvious, but you are getting a JSON parsing error because Play can't parse the JSON to the type that you specify. It is nothing to do with how you are handling the Future, which looks OK.
The type for extract is Seq[String], so to match this the JSON would have to be an array of strings, e.g. ["A", "B", "C"]. Check that this is what that URL is returning by printing the response before extracting it inside the map call, or by running it under a debugger and inspecting the values.
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
I am using Akka Http to make requests to a 3rd party API. The responses are "application/json", and I would like to use Akka Http to convert them to a custom case class. I would like to do something like this:
val request = RequestBuilding.Get("https://service.com/v1/api/items")
val response : Future[ItemsResponse] = http.singleRequest(request).flatMap({ response =>
Unmarshal(response.entity).to[ItemsResponse]
})
This fails to compile, because I am missing an implicit unmarshaller of type akka.http.scaladsl.unmarshalling.Unmarshaller[akka.http.scaladsl.model.ResponseEntity, com.mycompany.models.ItemsResponse].
It's unclear to me what the idiomatic way to do this with akka http is. I am aware that I could use spray-json, but I'd like to understand how to do this without importing another library. It seems possible with Akka Http, but the documentation isn't clear (to me at least).
The simplest way is to use spray-json as it comes as part of Akka HTTP:
import spray.json._
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
// change 2 to the number of attributes of ItemsResponse
implicit val ItemsResponseFormat = jsonFormat2(ItemsResponse)
This should make your existing code compile.
I think your question is valid, and there are cases where avoiding extra dependencies makes sense. Mine is from making an authentication library, where I don't want to impose my JSON library preferences to the users of such library. The library needs JSON unmarshalling for understanding a token info response.
To the code! :)
case class TokenInfo private (uid: String, realm: String, scope: Seq[String])
object TokenInfo {
private
def parseOpt(s: String): Option[TokenInfo] = {
util.parsing.json.JSON.parseFull(s) match {
case Some(map: Map[String,Any] #unchecked) =>
val tmp: Map[String,Any] = map.collect {
case (k# "uid",x: String) => k -> x
case (k# "realm",x: String) => k -> x
case (k# "scope",x: Seq[String] #unchecked) => k -> x
// other keys are ignored
}.toMap
if (tmp.size == 3) {
Some( TokenInfo( tmp("uid").asInstanceOf[String], tmp("realm").asInstanceOf[String], tmp("scope").asInstanceOf[Seq[String]]) )
} else {
None
}
case _ => None
}
}
implicit
val unm: FromEntityUnmarshaller[TokenInfo] = {
PredefinedFromEntityUnmarshallers.stringUnmarshaller.map{ s => parseOpt(s).getOrElse{
throw new RuntimeException(s"Unknown TokenInfo: $s")
}}
}
}
I chose to use util.parsing.json which comes within Scala. The other option was simply regex's, but in that case I'm either fixing the expected order of fields, or the code might get complex.
I am new to Scala and Play!, but have a reasonable amount of experience of building webapps with Django and Python and of programming in general.
I've been doing an exercise of my own to try to improve my understanding - simply pull some records from a database and output them as a JSON array. I'm trying to use the Enumarator/Iteratee functionality to do this.
My code follows:
TestObjectController.scala:
def index = Action {
db.withConnection { conn=>
val stmt = conn.createStatement()
val result = stmt.executeQuery("select * from datatable")
logger.debug(result.toString)
val resultEnum:Enumerator[TestDataObject] = Enumerator.generateM {
logger.debug("called enumerator")
result.next() match {
case true =>
val obj = TestDataObject(result.getString("name"), result.getString("object_type"),
result.getString("quantity").toInt, result.getString("cost").toFloat)
logger.info(obj.toJsonString)
Future(Some(obj))
case false =>
logger.warn("reached end of iteration")
stmt.close()
null
}
}
val consume:Iteratee[TestDataObject,Seq[TestDataObject]] = {
Iteratee.fold[TestDataObject,Seq[TestDataObject]](Seq.empty[TestDataObject]) { (result,chunk) => result :+ chunk }
}
val newIteree = Iteratee.flatten(resultEnum(consume))
val eventuallyResult:Future[Seq[TestDataObject]] = newIteree.run
eventuallyResult.onSuccess { case x=> println(x)}
Ok("")
}
}
TestDataObject.scala:
package models
case class TestDataObject (name: String, objtype: String, quantity: Int, cost: Float){
def toJsonString: String = {
val mapper = new ObjectMapper()
mapper.registerModule(DefaultScalaModule)
mapper.writeValueAsString(this)
}
}
I have two main questions:
How do i signal that the input is complete from the Enumerator callback? The documentation says "this method takes a callback function e: => Future[Option[E]] that will be called each time the iteratee this Enumerator is applied to is ready to take some input." but I am unable to pass any kind of EOF that I've found because it;s the wrong type. Wrapping it in a Future does not help, but instinctively I am not sure that's the right approach.
How do I get the final result out of the Future to return from the controller view? My understanding is that I would effectively need to pause the main thread to wait for the subthreads to complete, but the only examples I've seen and only things i've found in the future class is the onSuccess callback - but how can I then return that from the view? Does Iteratee.run block until all input has been consumed?
A couple of sub-questions as well, to help my understanding:
Why do I need to wrap my object in Some() when it's already in a Future? What exactly does Some() represent?
When I run the code for the first time, I get a single record logged from logger.info and then it reports "reached end of iteration". Subsequent runs in the same session call nothing. I am closing the statement though, so why do I get no results the second time around? I was expecting it to loop indefinitely as I don't know how to signal the correct termination for the loop.
Thanks a lot in advance for any answers, I thought I was getting the hang of this but obviously not yet!
How do i signal that the input is complete from the Enumerator callback?
You return a Future(None).
How do I get the final result out of the Future to return from the controller view?
You can use Action.async (doc):
def index = Action.async {
db.withConnection { conn=>
...
val eventuallyResult:Future[Seq[TestDataObject]] = newIteree.run
eventuallyResult map { data =>
OK(...)
}
}
}
Why do I need to wrap my object in Some() when it's already in a Future? What exactly does Some() represent?
The Future represents the (potentially asynchronous) processing to obtain the next element. The Option represents the availability of the next element: Some(x) if another element is available, None if the enumeration is completed.
I want to implement a BodyParser which parses and validates request.body, it's based on parse.json and currently looks like this:
def parseModel[A](implicit reads: Reads[A]): BodyParser[JsResult[A]] =
parse.json.map(_.validate[A])
The problem is it is currently of type BodyParser[JsResult[A]] while I want it to be of type BodyParser[A]. In case of JsError I want it basically to return 400 Bad Request with validation errors.
In Play API docs I can't find a method which allows me to inspect result of previous body parser and return a result or continue to a controller.
A BodyParser, after parsing the body, produces an Either[SimpleResult, A], where SimpleResult is an error result that should be returned immediately instead of processing the action. The convenience methods on BodyParser don't let you do this, so instead, create a new body parser that delegates to the JSON body parser:
def validateJson[A](implicit reads: Reads[A]) = new BodyParser[A] {
def apply(request: RequestHeader) = parse.json(request).map(_.right.flatMap { json =>
json.validate[A].asEither.left.map(e => BadRequest(JsError.toFlatJson(e)))
})
}
You can see here that we're mapping the result of the parse, and then taking the right value (a successful parse, will be JsValue), and calling flatMap on it. Our flatMap method converts the JsResult from validate to an Either[JsError, A], so we're halfway there with A, after that we map the left JsError to SimpleResult, and we're good to go.
Ok, I've implemented the desired behaviour via a method which produces an Action:
def validateJson[A](implicit reads: Reads[A]) =
parse.json.map(_.validate[A])
def ModelAction[A](action: A => Result)(implicit r: Reads[A]) =
Action(validateJson[A]) { request =>
request.body match {
case JsSuccess(model, _) => action(model)
case JsError(e) => BadRequest(JsError.toFlatJson(e))
}
}
I can use it like this:
def create = ModelAction[MyModel] { model =>
???
}
I'm still interested if it's possible to do the same with BodyParser and if I need to do so or it's better as it is now?