src/main/scala/com/github/vitalsoftware/util/JsonOps.scala
package com.github.vitalsoftware.util
import java.util.Locale
import org.joda.time.DateTimeZone
import org.joda.time.format.DateTimeFormatter
import play.api.libs.json._
import play.api.libs.json.Reads._
import play.api.libs.functional.syntax._
import scala.collection.Seq
import scala.language.implicitConversions
import scala.language.postfixOps
import scala.util.Try
/**
* Alter operations available on Play-JSON Reads[T] & Writes[T]
*
* Inspired by http://kailuowang.blogspot.co.nz/2013/11/addremove-fields-to-plays-default-case.html
*/
class OWritesOps[A](writes: OWrites[A]) {
def addField[T: Writes](fieldName: String, field: A => T): OWrites[A] =
(writes ~ (__ \ fieldName).write[T])((a: A) => (a, field(a)))
def removeField(fieldName: String): OWrites[A] = OWrites { a: A =>
val transformer = (__ \ fieldName).json.prune
Json.toJson(a)(writes).validate(transformer).get
}
def removeFields(fieldName: String*): OWrites[A] = OWrites { a: A =>
val transformer = fieldName.tail.foldLeft((__ \ fieldName.head).json.prune) {
case (z, field) =>
(__ \ field).json.prune and z reduce
}
Json.toJson(a)(writes).validate(transformer).get
}
}
object OWritesOps {
implicit def from[A](writes: OWrites[A]): OWritesOps[A] = new OWritesOps(writes)
}
class OReadsOps[A](reads: Reads[A]) {
def validate(set: Set[A]): Reads[A] =
reads.filter(JsonValidationError("set.notFoundIn", set)) { a: A =>
set.contains(a)
}
}
object OReadsOps {
implicit def from[A](reads: Reads[A]): OReadsOps[A] = new OReadsOps(reads)
}
trait JsonImplicits {
implicit val jodaISODateReads: Reads[org.joda.time.DateTime] = new Reads[org.joda.time.DateTime] {
import org.joda.time.DateTime
val isoFormatter: DateTimeFormatter = org.joda.time.format.ISODateTimeFormat.dateTime()
val dateDashFormatter: DateTimeFormatter =
org.joda.time.format.DateTimeFormat.forPattern("yyyy-MM-dd").withZone(DateTimeZone.UTC)
val dateNoDashFormatter: DateTimeFormatter =
org.joda.time.format.DateTimeFormat.forPattern("yyyyMMdd").withZone(DateTimeZone.UTC)
def reads(json: JsValue): JsResult[DateTime] = json match {
case JsNumber(d) => JsSuccess(new DateTime(d.toLong))
case JsString(s) =>
parseDate(s) match {
case Some(d) => JsSuccess(d)
case None =>
JsError(Seq(JsPath() -> Seq(JsonValidationError("validate.error.expected.date.isoformat", "ISO8601"))))
}
case _ => JsError(Seq(JsPath() -> Seq(JsonValidationError("validate.error.expected.date"))))
}
private def parseDate(input: String): Option[DateTime] =
Try(DateTime.parse(input, isoFormatter))
.orElse(Try(DateTime.parse(input, dateDashFormatter)))
.orElse(Try(DateTime.parse(input, dateNoDashFormatter)))
.toOption
}
implicit val jodaISODateWrites: Writes[org.joda.time.DateTime] = new Writes[org.joda.time.DateTime] {
def writes(d: org.joda.time.DateTime): JsValue = JsString(d.toString())
}
implicit val jodaISO8601Format = Format(jodaISODateReads, jodaISODateWrites)
// Will read ISO8061 and "yyyy-MM-dd" format
implicit val jodaLocalDateFormat = Format(JodaReads.DefaultJodaLocalDateReads, JodaWrites.DefaultJodaLocalDateWrites)
implicit class JsValueExtensions(jsValue: JsValue) {
def reduceNullSubtrees: JsValue = reduceNullSubtreesImpl(jsValue)
def reduceEmptySubtrees: JsValue = reduceEmptySubtreesImpl(jsValue)
private def reduceNullSubtreesImpl(jv: JsValue): JsValue =
jv match {
case JsObject(o) =>
if (o.valuesIterator.forall(_ == JsNull)) {
JsNull
} else {
JsObject(o.map { case (s, childVal) => s -> reduceNullSubtreesImpl(childVal) })
}
case JsArray(ts) =>
JsArray(
ts.map(reduceNullSubtreesImpl)
.filter(_ match {
case JsNull => false
case _ => true
})
)
case _ =>
jv
}
private def reduceEmptySubtreesImpl(jv: JsValue): JsValue =
jv match {
case JsObject(o) => if (isEmpty(jv)) JsNull else JsObject(o.mapValues(reduceEmptySubtreesImpl))
case JsArray(a) => if (isEmpty(jv)) JsArray.empty else JsArray(a.map(reduceEmptySubtreesImpl))
case _ => jv
}
private def isEmpty(jv: JsValue): Boolean = jv match {
case JsNull => true
case JsArray(a) => a.forall(isEmpty)
case JsObject(o) => o.valuesIterator.forall(isEmpty)
case _ => false
}
}
}
object JsonImplicits extends JsonImplicits