Json contract patterns, validation, lenses and query
"io.higherstate" %% "jsentric" % "1.0.0"
resolvers ++= Seq(
"Sonatype releases" at "http://oss.sonatype.org/content/repositories/releases/",
)
jsentric is built upon argonaut and is designed to facilitate the use of the basic json datatypes in cases where we have partially dynamic data or are regularly moving through bounded context and may not wish to constantly serialize/deserialize from class objects.
jsentric works by describing a singleton contract which represents data we might wish to extract from the json data structure. By doing so, we get easy validation, lenses and even a type safe mongo db query generator.
/*define a contract,
\ \? \! expected, optional, default properties
\: \:? \:! expected, optional, default array properties
\\ \\? expected, option object properties
*/
object Order extends Contract {
val firstName = \[String]("firstName", nonEmptyOrWhiteSpace)
val lastName = \[String]("lastName", nonEmptyOrWhiteSpace)
val orderId = \?[Int]("orderId", reserved && immutable)
val email = new \\("email") {
val friendlyName = \?[String]("friendlyName")
val address = \[String]("address")
}
val status = \?[String]("status", in("pending", "processing", "sent") && reserved)
val notes = \?[String]("notes", internal)
val orderLines = \:[(String, Int)]("orderLines", forall(custom[(String, Int)](ol => ol._2 >= 0, "Cannot order negative items")))
import Composite._
//Combine properties to make a composite pattern matcher
lazy val fullName = firstName @: lastName
}
import argonaut._
//Create a new Json object
val newOrder = Order.$create{o =>
o.firstName.$set("John") ~
o.lastName.$set("Smith") ~
o.email.address.$set("[email protected]") ~
o.orderLines.$append("Socks" -> 3)
}
//validate a new json object
val validated = Order.$validate(newOrder)
//pattern match property values
newOrder match {
case Order.email.address(email) && Order.email.friendlyName(Some(name)) =>
println(s"$email <$name>")
case Order.email.address(email) && Order.fullName(firstName, lastName) =>
println(s"$email <$firstName $lastName>")
}
//make changes to the json object.
val pending =
Order{o =>
o.orderId.$set(123) ~
o.status.$set("pending") ~
o.notes.$modify(maybe => Some(maybe.foldLeft("Order now pending.")(_ + _)))
}(newOrder)
//strip out any properties marked internal
val sendToClient = Order.$sanitize(pending)
//generate query json
val relatedOrdersQuery = Order.orderId.$gt(56) && Order.status.$in("processing", "sent")
//experimental convert to postgres jsonb clause
val postgresQuery = QueryJsonb("data", relatedOrdersQuery)
import scalaz.{\/, \/-}
//create a dynamic property
val dynamic = Order.$dynamic[\/[String, Int]]("age")
sendToClient match {
case dynamic(Some(\/-(ageInt))) =>
println(ageInt)
case _ =>
}
val statusDelta = Order.$create(_.status.$set("processing"))
//validate against current state
Order.$validate(statusDelta, pending)
//apply delta to current state
val processing = pending.delta(statusDelta)
//Define subcontract for reusable or recursive structures
trait UserTimestamp extends SubContract {
val account = \[String]("account")
val timestamp = \[Long]("timestamp")
}
object Element extends Contract {
val created = new \\("created", immutable) with UserTimestamp
val modified = new \\("modified") with UserTimestamp
}
//try to force a match even if wrong type
import LooseCodecs._
Json("orderId" := "23628") match {
case Order.orderId(Some(id)) => id
}
*Auto generation of schema information is still a work in progress
*mongo query is not a full feature set.