I can not come up with solution for Patch semantic:
if json has no property, I need skip modification
if json property has null, I need to remove this property (for not required properties)
in other cases I need to set the value
I need transformation into mongo.db update command ("$unset" for 2, "$set" for 3)
For example I need store json with required property "summary". So:
{"summary": "modified by patch", "description": null}
must be transformed to:
{
"$set" : {
"summary": "modified by patch"
},
"$unset": {
"description": ""
}
}
this json
{"description": null}
must be transformed to ("summary" is skipped):
{
"$unset" : {
"description": ""
}
}
and for this
{"summary": null}
I need transformation error (can't remove required properties)
My solution is
def patch(path: JsPath)(r: Reads[JsObject]) = Reads{json =>
path.asSingleJsResult(json).fold(
_ => JsSuccess(Json.obj()),
_ => r.reads(json)
)
}
and for requiered properties
def requiredError = ValidationError("error.remove.required")
val summaryPatch = patch(__ \ "summary")(
(__ \ "$set" \ "summary").json.copyFrom(
(__ \ "summary").json.pick.filterNot(requiredError)(_ == JsNull)
)
)
for other
val descriptionPatch = patch(__ \ "description")(
(__ \ "$set" \ "description").json.copyFrom(
(__ \ "description").json.pick.filterNot(_ == JsNull)
) orElse
(__ \ "$unset" \ "description").json.copyFrom(
(__ \ "description").json.pick)
)
)
to mongo.db trasformer:
toMongoPatch = (summaryPatch and descriptionPatch).reduce
Related
Technology stack: Scala, PlayFramework 2.3.8, MongoDB ( Salat + casbah ).
I am developing some restful APIs, with simple CRUD.
The entity:
case class Company(
_id: ObjectId = new ObjectId,
user_id: Any = "",
cinema_id: Int = 0,
name: String = "",
status: String = "",
releaseId: Int, fileId: Int,
date: CompanyDate,
showTime: List[Int] = List())
case class CompanyDate(start: DateTime, end: DateTime)
I create Reads for creating the object from JSON:
def createFromJson(json: JsValue, user: User): Reads[Company] = {
val user_id = (for {
cinema_id <- (json \ "cinema_id").asOpt[Int]
cinema <- user.cinema.find(_.id == cinema_id)
} yield cinema.getUser(user._id.toString)
).getOrElse(0)
(
Reads.pure(new ObjectId) and
Reads.pure[Any](user_id).filter(ValidationError("You have no access to this cinema"))(_ != 0) and
(__ \ "cinema_id").read[Int] and
(__ \ "name").read[String](minLength[String](1) keepAnd maxLength[String](100)) and
Reads.pure[String]("new") and
(__ \ "release_id").read[Int] and
(__ \ "file_id").read[Int] and
(__ \ "date").read[CompanyDate] and
(__ \ "show_time").read[List[Int]](minLength[List[Int]](1))
)(Company.apply _).flatMap(company => Reads { _ =>
if (company.date.start isAfter company.date.end) {
JsError(JsPath \ "date" \ "start" -> ValidationError("Start < end"))
} else {
JsSuccess(company)
}
})
}
It took me create wrapped function to pass user to Reads, because some field ( user_id ) calculate from current user - client doesn't know this information.
It looks a little ugly, but works ok.
The Controller for create action is very simple, it takes json, casts it to class and saves to DB.
Next thing: Update. In update method i want reuse some validation cases ( check date.start < date.end, e.g. ), but i don't receive the full object. My client only sends me changed fields.
Some fields ( cinema_id and user_id ) can only be set when creating object, they can't be updated.
Example request (UPDATED):
PUT /api/company/1
{
"name": "Name",
"status": "delete"
}
How can i validate this json and update existing object?
What is the best approach to solve this task?
Given the following JSON..
{
"ask":"428.00",
"bid":"424.20"
}
... I need to convert the values of ask and bid to numbers:
{
"ask": 428.00,
"bid": 424.20
}
To do that, I've created a validator that reads the string value and passes it to method toNumber, which validates and converts the given string:
def validate = (
((__ \ 'ask).json.pickBranch(Reads.of[JsString] <~ toNumber)) ~
((__ \ 'bid).json.pickBranch(Reads.of[JsString] <~ toNumber))
).reduce
private def toNumber(implicit reads: Reads[String]) = {
Reads[Double](js =>
reads.reads(js).flatMap { value =>
parse[Double](value) match {
case Some(number) => JsSuccess(number)
case _ => JsError(ValidationError("error.number", value))
}
}
)
}
The code above only validates the value but of course does not replace the original string with the converted number. How do I convert string values to numbers while validating?
EDIT
Just wanted to share the solution provided by Ben:
def validate = (
((__ \ 'ask).json.update(toNumber)) ~
((__ \ 'bid).json.update(toNumber))
).reduce
private def toNumber(implicit reads: Reads[String]) = {
Reads[JsNumber](js =>
reads.reads(js).flatMap { value =>
parse[Double](value) match {
case Some(number) => JsSuccess(JsNumber(number))
case _ => JsError(ValidationError("error.number", value))
}
}
)
}
If you make toNumber a Reads[JsNumber] instead of a Reads[Double] (simply by wrapping number in JsNumber), then you can use transform together with update:
val transformer = (__ \ "ask").json.update(toNumber)
val json = Json.parse(""" { "ask" : "44" } """)
json.transorm(transformer) //JsSuccess({"ask":44.0},/ask)
val json = Json.parse(""" { "ask" : "foo" } """)
json.transorm(transformer) //JsError(List((/ask,List(ValidationError(error.number,WrappedArray(foo))))))
In a sense, transformers are validators. Instead of checking if something is valid, and then transforming it, you can use transform to simply transform the value, and get a JsError if the transformation is invalid. Read more about transform here.
I'm trying to parse the data from GovTrack, for example, https://www.govtrack.us/api/v2/bill/74369 . But titles is in a peculiar format:
"titles": [
[
"short",
"introduced",
"Public Online Information Act of 2011"
],
[
"official",
"introduced",
"To establish an advisory committee to issue nonbinding governmentwide guidelines..."
]
]
titles is an array of each title type, with fields in a particular order. I want to read this into a more standard JSON format:
{
'short_title': "Public Online Information Act of 2011",
'official_title': "To establish an advisory committee to issue nonbinding governmentwide guidelines..."
}
The short title or official title may or may not be there, and there could actually be several short titles.
How do I make a Reads for this? Right now I've got:
implicit val billReads: Reads[Bill] = (
(JsPath \ "id").read[Int] and
(JsPath \ "display_number").read[String] and
(JsPath \ "current_status").read[String] and
(JsPath \ "titles")(0)(2).read[String]
)(Bill.apply _)
How do I specify "The member of the array that has a first element equal to 'official'"?
As far as I know, there is no out of the box way to do it, but I would do it with additional custom reader, like this:
val officialReads = new Reads[String] {
override def reads(json: JsValue): JsResult[String] = (json \ "titles") match {
case JsArray(titles) => titles.collectFirst({
case JsArray(values) if (values.headOption.map(v => "official".equals(v.as[String])).getOrElse(false)) =>
JsSuccess(values.tail.tail.head.as[String])
}).getOrElse(JsError("No official title"))
case _ => JsError("Can't read official title")
}
}
And your Bill reader would look like this:
val implicitReads: Reads[Bill] = (
(__ \ "id").read[Int] and
(__ \ "display_number").read[String] and
(__ \ "current_status").read[String] and
officialReads
)(Bill.apply _)
I've tested, this works :)
I'm trying to extract the data from a JSON file that looks like that :
val json: JsValue = Json.parse("""
{
"item": {
"id" : "11111111",
"name" : "box",
"categories" : [{
"name" : "blue box",
"id" : "12345",
},
{
"name" : "fancy box",
"id" : "98765",
}]
}
}
""")
I would like to do it using the play JSON library. I came up with this code :
//I define my class and reader for one category
case class ItemCategory(id: Option[String], name: String)
implicit val categoryRead: Reads[ItemCategory] = (
(JsPath \ "item" \ "categories" \ "id").readNullable[String] and
(JsPath \ "item" \ "categories" \ "name").read[String]
)(ItemCategory.apply _)
//I define my class and reader for one item
case class MyItem(categories : Option[List[ItemCategory]], id : Option[String], name : Option[String])
implicit val myItemRead: Reads[MyItem] = (
(JsPath \ "item" \ "categories").readNullable[List[ItemCategory]] and
(JsPath \ "item" \ "id").readNullable[String] and
(JsPath \ "item" \ "name").readNullable[String]
)(MyItem.apply _)
//I then try to read :
var item: JsResult[MyItem] = json.validate[MyItem](myItemRead)
println(item)
However this code gives a JsError :List(ValidationError(validate.error.missing-path,WrappedArray())).
Which to my understanding simply means that some path went missing. I tried to read just one category and it worked fine, I try to read an item without trying to get the categories and here again it went fine. Hence I think the problem is on reading a list of items. I would really appreciate if you could help me with this.
Path is relative. categoryRead js path should be relative. Such as _ \ xxx
Given a JSON array like this one:
{
"success": true,
"data": [
{
"id": 600,
"title": "test deal",
"e54cbe3a434d8e6": 54
},
{
"id": 600,
"title": "test deal",
"e54cbe3a434d8e6": 54
},
],
"additional_data": {
"pagination": {
"start": 0,
"limit": 100,
"more_items_in_collection": false
}
}
}
In my Play 2.2.2 application, using the Scala JSON Reads Combinator, everything works going this way:
implicit val entityReader = Json.reads[Entity]
val futureJson: Future[List[Entity]] = futureResponse.map(
response => (response.json \ "data").validate[List[Entity]].get
The problem now is the key named 'e54cbe3a434d8e6' which I would like to name 'value' in my object:
// This doesn't work, as one might expect
case class Entity(id: Long, title: String, e54cbe3a434d8e6: Long)
// I would like to use 'value' instead of 'e54cbe3a434d8e6'
case class Entity(id: Long, title: String, value: Long)
There is vast information about the combinators here and here but I only want to use a fieldname which is different from the key name in the JSON array. Can some one help me to find a simple way?
I suppose it has got something to do with JSON.writes?!
One simple way without trying to apply transformations on json itself is to define a custom Reads in such a way to handle this:
val json = obj(
"data" -> obj(
"id" -> 600,
"title" -> "test deal",
"e54cbe3a434d8e6" -> 54))
case class Data(id: Long, title: String, value: Int)
val reads = (
(__ \ "id").read[Long] ~
(__ \ "title").read[String] ~
(__ \ "e54cbe3a434d8e6").read[Int] // here you get mapping from your json to Scala case class
)(Data)
def index = Action {
val res = (json \ "data").validate(reads)
println(res) // prints "JsSuccess(Data(600,test deal,54),)"
Ok(json)
}
Another way is to use combinators like this:
... the same json and case class
implicit val generatedReads = reads[Data]
def index = Action {
val res = (json \ "data").validate(
// here we pick value at 'e54cbe3a434d8e6' and put into brand new 'value' branch
__.json.update((__ \ "value").json.copyFrom((__ \ "e54cbe3a434d8e6").json.pick)) andThen
// here we remove 'e54cbe3a434d8e6' branch
(__ \ "e54cbe3a434d8e6").json.prune andThen
// here we validate result with generated reads for our case class
generatedReads)
println(res) // prints "JsSuccess(Data(600,test deal,54),/e54cbe3a434d8e6/e54cbe3a434d8e6)"
Ok(prettyPrint(json))
}