Specs2 JSONMatchers: mapping over Array elements? - scala

I'm using the Specs2 JSONMatcher to validate that a GET request is being correctly converted from its internal representation (there are some manipulations we do before generating the JSON). What I need to do is, make sure that an element in the JSON array matches the corresponding object from our repository.
What I've tried:
val response = response.entity.asString // Spray's way of getting the JSON response
repository.all.map { obj =>
resp must */ ("id" -> obj.id)
resp must */ ("state" -> generateState(obj)
}
The problem is that the */ matcher just finds that "state": "whatever" (assuming generateState returns "whatever") exists somewhere in the JSON document, not necessarily in the same one matched by the ID
I tried using the indices but the repository.all method doesn't always return the elements in the same order, so there's no way of matching by index.
What I'd like to do is, iterate over the elements of the JSON array and match each one separately. Say an /## operator (or something) that takes matchers for each element:
resp /## { elem =>
val id = elem("id")
val obj = repository.lookup(id)
elem /("state" -> generateState(obj))
}
Does anyone have a way to do this or something similar?

Probably the easiest thing to do for now (until a serious refactoring of JsonMatchers) is to do some parsing and recursively use a JsonMatchers in a Matcher[String]:
"""{'db' : { 'id' : '1', 'state' : 'ok_1'} }""" must /("db" -> stateIsOk)
// a string matcher for the json string in 'db'
def stateIsOk: Matcher[String] = { json: String =>
// you need to provide a way to access the 'id' field
// then you can go on using a json matcher for the state
findId(json) must beSome { id: String =>
val obj = repository.lookup(id)
json must /("state" -> generate(obj))
}
}
// I am using my own parse function here
def findId(json: String): Option[String] =
parse(json).flatMap { a =>
findDeep("id", a).collect { case JSONArray(List(v)) => v.toString }
}
// dummy system
def generate(id: String) = "ok_"+id
case object repository {
def lookup(id: String) = id
}

What I did in the end is use responseAs[JArray], JArray#arr and JObject#values to convert the JSON structures into Lists and Maps, and then used the standard List and Map matchers. Much more flexible.

Related

Can I make json4s's extract method case insensitive?

I am using case classes to extract json with json4s's extract method. Unfortunately, the Natural Earth source data I am using isn't consistent about casing... at some resolutions a field is called iso_a2 and at some it's ISO_A2. I can only make json4s accept the one that matches the field in the case class:
object TopoJSON {
case class Properties(ISO_A2: String)
...
// only accepts capitalised version.
Is there any way to make json4s ignore case and accept both?
There is no way to make it case insensitive using the configuration properties, but a similar result can be achieved by either lowercasing or uppercasing the field names in the parsed JSON.
For example, we have our input:
case class Properties(iso_a2: String)
implicit val formats = DefaultFormats
val parsedLower = parse("""{ "iso_a2": "test1" }""")
val parsedUpper = parse("""{ "ISO_A2": "test2" }""")
We can lowercase all field names using a short function:
private def lowercaseAllFieldNames(json: JValue) = json transformField {
case (field, value) => (field.toLowerCase, value)
}
or make it for specific fields only:
private def lowercaseFieldByName(fieldName: String, json: JValue) = json transformField {
case (field, value) if field == fieldName => (fieldName.toLowerCase, value)
}
Now, to extract the case class instances:
val resultFromLower = lowercaseAllFieldNames(parsedLower).extract[Properties]
val resultFromUpper = lowercaseAllFieldNames(parsedUpper).extract[Properties]
val resultByFieldName = lowercaseFieldByName("ISO_A2", parsedUpper).extract[Properties]
// all produce expected items:
// Properties(test1)
// Properties(test2)
// Properties(test2)

Scala/Play/Squeryl Retrieve multiple params

I have the following url : http://localhost/api/books/?bookId=21&bookId=62?authorId=2
I want to retrieve all the bookId values with Scala and then use Squeryl to do a fetch in a the database.
I'm using the PlayFrameWork as the WebServer, so here's my code :
val params = request.queryString.map { case (k, v) => k -> v(0) } // Retrieve only one the first occurence of a param
So params.get("bookId") will only get the last value in the bookId params. e-g : 62.
To retrieve all my bookId params i tried this :
val params = request.queryString.map { case (k, v) => k -> v } so i can get a Seq[String], but what about the authorId which is not a Seq[String]? .
At the end i want to fetch the bookIds and authorId in my DB using Squeryl :
(a.author_id === params.get("authorId").?) and
(params.get("bookId").map(bookIds: Seq[String] => b.bookId in bookIds))
In my controller i get the params and open the DB connection :
val params = request.queryString.map { case (k, v) => k -> v(0) }
DB.withTransaction() { where(Library.whereHelper(params)}
In my model i use the queries :
def whereHelper(params : Map[String,String]) = {
(a.author_id === params.get("authorId").?) and
(params.get("bookId").map{bookIds: Seq[String] => b.bookId in bookIds})
}
Since bookIds is a list, i need to use the Seq[String]. There's a way to use request.queryString.map { case (k, v) => k -> v } for both a string (authorId) and a list of strings (bookIds) ?
Thanks,
If I really understand what you are trying to do, you want to know how to get the parameters from queryString. This is pretty simple and you can do the following at your controller:
def myAction = Action { request =>
// get all the values from parameter named bookId and
// transforming it to Long. Maybe you don't want the map
// and then you can just remove it.
val bookIds: Seq[Long] = request.queryString("bookId").map(_.toLong)
// Notice that now I'm using getQueryString which is a helper
// method to access a queryString parameter. It returns an
// Option[String] which we are mapping to a Option[Long].
// Again, if you don't need the mapping, just remove it.
val authorId: Option[Long] = request.getQueryString("authorId").map(_.toLong)
DB.withTransaction() { where(Library.whereHelper(authorId, bookIds) }
// Do something with the result
}
At your model you will have:
def whereHelper(authorId: Option[Long], booksId: List[Long]) = authorId match {
case Some(author_id) =>
(a.author_id === author_id) and
(b.bookId in bookIds)
case None =>
(b.bookId in bookIds)
}
I've left explicit types to help you understand what is happen. Now, since you have both values, you can just use the values at your query.
Edit after chat:
But, since you want to receive a params: Map[String, Seq[String]] at your models and is just having problems about how to get the authorId, here is what you can do:
def whereHelper(params: Map[String, Seq[String]]) = {
// Here I'm being defensive to the fact that maybe there is no
// "booksIds" key at the map. So, if there is not, an Seq.empty
// will be returned. map method will run only if there is something
// at the Seq.
val booksIds = params.getOrElse("booksIds", Seq.empty).map(_.toLong)
// The same defensive approach is being used here, and also getting
// the head as an Option, so if the Seq is empty, a None will be
// returned. Again, the map will be executed only if the Option
// is a Some, returning another Some with the value as a Long.
val authorId = params.getOrElse("authorId", Seq.empty).headOption
authorId.map(_.toLong) match {
case Some(author_id) =>
(a.author_id === author_id) and
(b.bookId in booksIds)
case None =>
(b.bookId in booksIds)
}
}
Of course, more parameters you have, more complicated this method will be.

Scala foreach member variable

Is there a way to loop for each member variable of a class? It's trivial with Lists and arrays but I have to construct a case class with each json field mapping to a member variable to use the Play framework's Reads/validator API
Did you mean something like this:
case class SomeClass(a: Int, b: Int)
SomeClass(1,2).productIterator.foreach{ a => println(a)}
this will give you an output of: 1 2
Or if you are trying to construct an object from json. You can define reads in your case class which deserialises json to your object :
override def reads(json: JsValue): JsResult[SomeClass] = JsSuccess(SomeClass(
(json \ "a").as[Int],
(json \ "b").as[Int]
)
)
then to use the deserialisation:
val json = Json.obj() //Your json
json.validate[SomeClass]
json.fold(
errors => {
Json.obj("status" -> "Not OK", "message" -> JsError.toJson(errors))
},
preset => {
Json.obj("status" -> "OK")
}
)
If you want compare Json and get difference, may be better use JsObject methods?
For example fieldSet return all fields as a set. You can use diff on previous and current field set to get changed fields. This is fast solution and no any specific classes.

How to break/escape from a for loop in Scala?

Im new to scala and searched a lot for the solution.
I'm querying the database and storing the value of the http request parsed as a json4s object in response. I wait for the response and parse the json.
val refService = url("http://url//)
val response = Http(refService OK dispatch.as.json4s.Json)
var checkVal :Boolean = true
val json = Await.result(response, 30 seconds)
val data = json \ "data"
I want to run a loop and check if the value of "name" is present in the data returned. If present I want to break and assign checkVal to false. So far I have this:
for {
JObject(obj) <- data
JField("nameValue", JString(t)) <- obj //nameValue is the column name in the returned data
} yield {checkVal= if (t == name){ break }
else
true
}
Eclipse is giving me the following error: type mismatch; found : List[Unit] required:
List[String]
Please advice. Thank you.
One of your problems is that you have different return types in yield: if t==name, return type is the type of break, and if t!=name return type is Boolean.
In scala you don't have break operator, this behaviour is achieved using breakable construct and calling break() method which actually throws an exception to exit from breakable block. Also you can use if statements in for body to filter you results:
import scala.util.control.Breaks._
breakable {
for {
JObject(obj) <- data
JField("nameValue", JString(t)) <- obj
if t == name
} yield {
checkVal = false
break()
}
}
UPDATE:
I used this imperative approach because you are new to scala, but it's not scala way. IMHO you should stick to #Imm code in comments to your question.
I actually don't like using pattern matching in for loops as if for some reason data is not a JObject it won't be handled well. I prefer an approach like below.
data match {
case JObject(fields) => fields.exists{
case (name:String,value:JString) => name == "nameValue" && value.s == "name"
case _ => false
}
case _ => false // handle error as not a JObject
}
Edit: revised to include your matches.
I would suggest to use exists as it is lazy on all collection members.
code:-
val list= Map(
"nameValue1" -> 1,
"nameValue2" -> 2,
"nameValue3" -> 3,
"nameValue4" -> 4,
"nameValue5" -> 5
)
val requiredHeader = "nameValue2"
var keyvalue:Int=0
list.exists(p=>{ if(p._1.equalsIgnoreCase(requiredHeader))keyvalue=p._2;p._1.equalsIgnoreCase(requiredHeader) })
if(keyvalue!=0){
//header present
}else{
//header doesn't exit
}

Modify Future[JsArray] in Scala

I use the Play 2.0 framework for getting data from my MongoDB.
This is done by the following code:
def getTopicsInAppFormat = Action.async {
// let's do our query
val cursor: Cursor[TopicModel] = topicCollection.find(Json.obj()).cursor[TopicModel]
// gather all the JsObjects in a list
val futureTopicsList: Future[List[TopicModel]] = cursor.collect[List]()
// transform the list into a JsArray
val futurePersonsJsonArray: Future[JsArray] = futureTopicsList.map { topics =>
Json.arr(topics)
}
// everything's ok! Let's reply with the array
futurePersonsJsonArray.map {
topics =>
Ok(topics(0))
}
}
But the problem is that I want the function to return an alternative representation of the data. So, I want for example to change the amount of attributes, etc. What is a good way to achieve that? I tried to modify the data (respectively create a new array with the new format) in the last step, right before the Ok() function. However, I didn't have any progress with that :/
EDIT:
At the moment I'm trying to create new JSON objects but I'm stuck while getting the data from the original one...
My current code looks like this:
futurePersonsJsonArray.map {
topics =>
/* Access a attribute */
println(topics(0).\("content"))
/* However: error: JsUndefined('content' is undefined on object */
/* Could be used to set the new attributes */
val myNewJson = Json.obj(
"name" -> "JohnDoe",
"age" -> "123",
"created" -> new java.util.Date().getTime())
Ok(/*topics(0)*/myNewJson)
}
You're probably misinterpreting the format of topics. The message you're getting is telling you that there is no content property on the first element of the topics array. This is a simplified example:
val myObject = Json.obj("a" -> 1, "b" -> 2)
myObject \ "a" //JsNumber(1)
myObject \ "c" //JsUndefined('c' is undefined on object...)
This makes sense, since we get undefined in Javascript when trying to read a property that doesn't exist. In the Play Json library, \ always returns a JsValue, and one of its subtypes is JsUndefined.
You should reexamine the format of the objects in the topics array and reassess how you can access its values.
Now, my final solution for the problem "modify the data just before returning" looks like this:
futurePersonsJsonArray.map {
topics =>
def createNewFormat(uID: JsValue, name: JsValue, content: JsValue, lat: JsValue, lng: JsValue): JsObject = {
return Json.obj(
"id" -> uID,
[...]
}
/* Access the needed attributes */
val uIDs = topics(0).\\("uID")
val names = topics(0).\\("name")
val content = topics(0).\\("content")
val gps = topics(0).\\("gps")
var dataArray = new JsArray()
/* create new Array */
for( i <- 0 to uIDs.length-1){
dataArray = dataArray.append(createNewFormat(uIDs(i),names(i),content(i),gps(i)(0),gps(i)(1)))
}
/* put this into a new JSObject */
var returnThis = Json.obj("data" -> dataArray)
Ok(returnThis)
}
Maybe this does help someone with similar problems :)