diff --git a/build.sbt b/build.sbt index 4a85df3a2..fb50dfd84 100644 --- a/build.sbt +++ b/build.sbt @@ -104,7 +104,8 @@ lazy val root = project `play-jsonJVM`, `play-functionalJS`, `play-functionalJVM`, - `play-json-joda` + `play-json-joda`, + `play-json4s` ).settings( commonSettings, publishTo := None @@ -190,6 +191,17 @@ lazy val `play-json-joda` = project ) .dependsOn(`play-jsonJVM`) +lazy val `play-json4s` = project + .in(file("play-json4s")) + .enablePlugins(PlayLibrary) + .settings(commonSettings ++ playJsonMimaSettings ++ Seq( + libraryDependencies ++= Seq( + "org.json4s" %% "json4s-core" % "3.6.3", + "com.chuusai" %% "shapeless" % "2.3.3" % Test + ) ++ specsBuild.value.map(_ % Test) + )) + .dependsOn(`play-jsonJVM`) + lazy val `play-jsonJVM` = `play-json`.jvm. settings( libraryDependencies ++= diff --git a/play-json4s/src/main/scala/Formats.scala b/play-json4s/src/main/scala/Formats.scala new file mode 100644 index 000000000..c5adaeaff --- /dev/null +++ b/play-json4s/src/main/scala/Formats.scala @@ -0,0 +1,122 @@ +package play.api.libs.json.json4s + +import scala.util.control.NonFatal + +import org.json4s.{ JValue, Reader, Writer } + +import play.api.libs.json.{ + JsError, + JsResult, + JsSuccess, + JsValue, + Reads, + Writes +} + +object Formats extends Formats + +sealed trait Formats extends LowPriorityFormats { + /** + * {{{ + * def test[T](implicit w: Reads[T]): Reader[T] = Formats.reader[T] + * }}} + */ + implicit def reader[T](implicit reads: Reads[T], conv: JValue => JsValue = JValueConverters.jvalue2JsValue): Reader[T] = new JsValueReader[T](reads, conv) + + /** + * {{{ + * def test[T](implicit w: Writes[T]): Writer[T] = Formats.defaultWriter[T] + * }}} + */ + implicit def defaultWriter[T](implicit writes: Writes[T]): Writer[T] = + new JsValueWriter[T](writes, JValueConverters.jsvalue2JValue) + + /** + * {{{ + * def test[T](implicit r: Reader[T]): Reads[T] = Formats.reads[T] + * }}} + */ + implicit def reads[T](implicit reader: Reader[T], conv: JsValue => JValue): Reads[T] = reads[T]((_: JsValue, cause: Throwable) => JsError(cause.getMessage)) + + /** + * {{{ + * def test[T](implicit r: Reader[T]): Reads[T] = + * Formats.reads[T](err = { (v: JsValue, cause: Throwable) => + * JsError(s"Custom message: \$v; \$cause") + * }) + * }}} + */ + def reads[T](err: (JsValue, Throwable) => JsError)(implicit reader: Reader[T], conv: JsValue => JValue): Reads[T] = new JValueReads[T](reader, conv, err) + + /** + * {{{ + * def test[T](implicit w: Writer[T]): Writes[T] = Formats.defaultWrites[T] + * }}} + */ + implicit def defaultWrites[T](implicit writer: Writer[T]): Writes[T] = + new JValueWrites[T](writer, JValueConverters.jvalue2JsValue) + +} + +private[json4s] sealed trait LowPriorityFormats { _: Formats => + /** + * {{{ + * def test[T](implicit w: Reads[T]): Reader[T] = Formats.defaultReader[T] + * }}} + */ + implicit def defaultReader[T](implicit reads: Reads[T]): Reader[T] = + new JsValueReader[T](reads, JValueConverters.jvalue2JsValue) + + /** + * {{{ + * def test[T](implicit w: Writes[T]): Writer[T] = Formats.writer[T] + * }}} + */ + implicit def writer[T](implicit writes: Writes[T], conv: JsValue => JValue): Writer[T] = new JsValueWriter[T](writes, conv) + + /** + * {{{ + * def test[T](implicit r: Reader[T]): Reads[T] = Formats.defaultReads[T] + * }}} + */ + implicit def defaultReads[T](implicit reader: Reader[T]): Reads[T] = + reads[T]((_: JsValue, cause: Throwable) => JsError(cause.getMessage))( + reader, JValueConverters.jsvalue2JValue) + + /** + * {{{ + * def test[T](implicit w: Writer[T]): Writes[T] = Formats.writes[T] + * }}} + */ + implicit def writes[T](implicit writer: Writer[T], conv: JValue => JsValue): Writes[T] = new JValueWrites[T](writer, conv) +} + +private[json4s] final class JsValueReader[T]( + r: Reads[T], conv: JValue => JsValue) extends Reader[T] { + + def read(value: JValue): T = r.reads(conv(value)).get +} + +private[json4s] final class JsValueWriter[T]( + w: Writes[T], conv: JsValue => JValue) extends Writer[T] { + + def write(value: T): JValue = conv(w writes value) +} + +private[json4s] final class JValueWrites[T]( + w: Writer[T], + conv: JValue => JsValue) extends Writes[T] { + def writes(value: T): JsValue = conv(w write value) +} + +private[json4s] final class JValueReads[T]( + r: Reader[T], + conv: JsValue => JValue, + err: (JsValue, Throwable) => JsError) extends Reads[T] { + def reads(js: JsValue): JsResult[T] = try { + val v = r.read(conv(js)) + JsSuccess(v) + } catch { + case NonFatal(cause) => err(js, cause) + } +} diff --git a/play-json4s/src/main/scala/JValueConverters.scala b/play-json4s/src/main/scala/JValueConverters.scala new file mode 100644 index 000000000..745fe2226 --- /dev/null +++ b/play-json4s/src/main/scala/JValueConverters.scala @@ -0,0 +1,130 @@ +package play.api.libs.json.json4s + +import scala.language.implicitConversions + +import org.json4s.{ + JArray, + JBool, + JDecimal, + JDouble, + JField, + JInt, + JLong, + JNothing, + JNull, + JObject, + JSet, + JString, + JValue +} + +import play.api.libs.json.{ + JsArray, + JsBoolean, + JsFalse, + JsNumber, + JsNull, + JsObject, + JsString, + JsTrue, + JsValue +} + +/** + * {{{ + * import play.api.libs.json.json4s.JValueConverters + * + * val jsTrue: JsBoolean = JBool.True + * val jbool: JBool = JsTrue + * }}} + */ +object JValueConverters extends LowPriorityJValueImplicits { + implicit val jnull2JsNull: JNull.type => JsNull.type = _ => JsNull + + implicit val jsnull2JNull: JsNull.type => JNull.type = _ => JNull + + implicit val jnothing2JsNull: JNothing.type => JsNull.type = _ => JsNull + + implicit def jbool2JsBoolean(jb: JBool): JsBoolean = jb match { + case JBool.True => JsTrue + case _ => JsFalse + } + + implicit val jstrue2JBool: JsTrue.type => JBool = _ => JBool.True + + implicit val jsfalse2JBool: JsFalse.type => JBool = _ => JBool.False + + implicit def jsbool2JBool(jb: JsBoolean): JBool = jb match { + case JsTrue => JBool.True + case _ => JBool.False + } + + implicit def jstring2JsString(js: JString): JsString = JsString(js.s) + + implicit def jsstring2Jtring(js: JsString): JString = JString(js.value) + + implicit def jdecimal2JsNumber(jd: JDecimal): JsNumber = JsNumber(jd.num) + + implicit def jsnumber2JDecimal(jn: JsNumber): JDecimal = JDecimal(jn.value) + + implicit def jint2JsNumber(ji: JInt): JsNumber = JsNumber(BigDecimal(ji.num)) + + implicit def jdouble2JsNumber(ji: JDouble): JsNumber = + JsNumber(BigDecimal(ji.num)) + + implicit def jlong2JsNumber(ji: JLong): JsNumber = + JsNumber(BigDecimal(ji.num)) + + implicit def jfield2Field(jf: JField): (String, JsValue) = + jf._1 -> jvalue2JsValue(jf._2) + + implicit def field2JField(jf: (String, JsValue)): JField = + jf._1 -> jsvalue2JValue(jf._2) + + implicit def jobject2JsObject(jo: JObject): JsObject = + JsObject(jo.obj.map({ + case (nme, jv) => nme -> jvalue2JsValue(jv) + })(scala.collection.breakOut)) + + implicit def jsobject2JObject(jo: JsObject): JObject = + JObject(jo.fields.toList.map(field2JField)) + + implicit def jarray2JsArray(ja: JArray): JsArray = + JsArray(ja.arr.map(jvalue2JsValue)) + + implicit def jsarray2JArray(ja: JsArray): JArray = + JArray(ja.value.toList.map(jsvalue2JValue)) + + implicit def jset2JsArray(js: JSet): JsArray = + JsArray(js.set.toList.map(jvalue2JsValue)) +} + +private[json4s] sealed trait LowPriorityJValueImplicits { + _: JValueConverters.type => + + implicit def jvalue2JsValue(jv: JValue): JsValue = jv match { + case a @ JArray(_) => jarray2JsArray(a) + case JDouble(d) => JsNumber(BigDecimal(d)) + case JInt(i) => JsNumber(BigDecimal(i)) + case JLong(l) => JsNumber(BigDecimal(l)) + case JNull => JsNull + case JNothing => JsNull + case JBool.True => JsTrue + case JBool.False => JsFalse + case JBool(b) => JsBoolean(b) + case s @ JSet(_) => jset2JsArray(s) + case JString(s) => JsString(s) + case JDecimal(n) => JsNumber(n) + case o @ JObject(fs) => jobject2JsObject(o) + } + + implicit def jsvalue2JValue(jv: JsValue): JValue = jv match { + case a @ JsArray(_) => jsarray2JArray(a) + case JsNull => JNull + case JsTrue => JBool.True + case JsFalse => JBool.False + case JsString(s) => JString(s) + case JsNumber(n) => JDecimal(n) + case o @ JsObject(_) => jsobject2JObject(o) + } +} diff --git a/play-json4s/src/test/scala/Json4sSpec.scala b/play-json4s/src/test/scala/Json4sSpec.scala new file mode 100644 index 000000000..f8a76a875 --- /dev/null +++ b/play-json4s/src/test/scala/Json4sSpec.scala @@ -0,0 +1,289 @@ +package tests // Keep it outside + +import play.api.libs.json.{ + JsArray, + JsBoolean, + JsFalse, + JsNull, + JsNumber, + JsValue, + JsString, + Json, + JsSuccess, + JsTrue, + OFormat, + Reads, + Writes +} + +import play.api.libs.json.json4s._ + +import org.json4s.{ + Formats => JFormats, + JArray, + JBool, + JDecimal, + JDouble, + JField, + JInt, + JLong, + JNull, + JNothing, + JObject, + JSet, + JString, + JValue, + Reader, + Writer +} + +final class Json4sSpec extends org.specs2.mutable.Specification { + "JSON4S" title + + "Typeclass converters" should { + "support Foo case class" >> { + // Doesn't compile before the appropriate implicits are imported ... + shapeless.test.illTyped("implicitly[Writer[Foo]]") + shapeless.test.illTyped("implicitly[Reader[Foo]]") + + // ... there: + import Formats._ + + val foo = Foo("bar", 12) + + val jo = JObject(List( + "name" -> JString("bar"), + "lorem" -> JDecimal(BigDecimal(12)))) + + "as Writer" in { + val w: Writer[Foo] = implicitly[Writer[Foo]] + + w.getClass.getName must startWith("play.api.libs.json.json4s") and { + JFormats.write(foo)(w) must_=== jo + } and { + JFormats.write(foo) must_=== jo + } + } + + "as Reader" in { + val r: Reader[Foo] = implicitly[Reader[Foo]] + + r.getClass.getName must startWith("play.api.libs.json.json4s") and { + JFormats.read(jo)(r) must_=== foo + } and { + JFormats.read[Foo](jo) must_=== foo + } + } + } + + "support Bar class" >> { + // Doesn't compile before the appropriate implicits are imported ... + shapeless.test.illTyped("implicitly[Writes[Bar]]") + shapeless.test.illTyped("implicitly[Reads[Bar]]") + + // ... there: + import Formats._ + + val bar = new Bar("foo", 1.23D) + val jo = Json.obj("title" -> "foo", "score" -> 1.23D) + + "as Writes" in { + val w: Writes[Bar] = implicitly[Writes[Bar]] + + shapeless.test.illTyped("implicitly[OWrites[Bar]]") + // ... as there is no OWrites equivalent in JSON4S + + w.getClass.getName must startWith("play.api.libs.json.json4s") and { + Json.toJson(bar) must_=== jo + } and { + Json.toJson(bar)(w) must_=== jo + } + } + + "as Reads" in { + val r: Reads[Bar] = implicitly[Reads[Bar]] + + r.getClass.getName must startWith("play.api.libs.json.json4s") and { + jo.validate(r) must_=== JsSuccess(bar) + } and { + jo.validate[Bar] must_=== JsSuccess(bar) + } + } + } + } + + "Value converters" should { + // Doesn't compile before the appropriate conversions are imported ... + shapeless.test.illTyped("implicitly[JValue](JsTrue)") + + // ... there: + import JValueConverters._ + + "handle JBool" in { + converterTest[JBool, JsBoolean](JBool.True, JsTrue) and { + converterTest[JBool, JsBoolean](JBool.False, JsFalse) + } + } + + "handle JNull" in { + converterTest(JNull, JsNull) and { + converterTest[JValue, JsValue](JNull, JsNull) + } + } + + "handle JNothing" in { + // Doesn't provide specific conversion JNothing => JsNull + shapeless.test.illTyped("implicitly[JNothing.type](JsNull)") + + implicitly[JsNull.type](JNothing) must_=== JsNull and { + implicitly[JsValue](JNothing: JValue) must_== JsNull + } + } + + "handle JString" in { + converterTest(JString("foo"), JsString("foo")) and { + converterTest[JValue, JsValue](JString("foo"), JsString("foo")) + } + } + + "handle JDecimal" in { + val dec = BigDecimal(1.2345D) + + converterTest(JDecimal(dec), JsNumber(dec)) and { + converterTest[JValue, JsValue](JDecimal(dec), JsNumber(dec)) + } + } + + "handle JArray" in { + val jarr = JArray(List( + JString("bar"), JDecimal(BigDecimal(12)), JNull)) + + val jsa = Json.arr("bar", 12, JsNull) + + converterTest(jarr, jsa) and { + converterTest[JValue, JsValue](jarr, jsa) + } + } + + "handle JSet" in { + val set = JSet(Set(JString("bar"), JDecimal(BigDecimal(12)))) + val arr = Json.arr("bar", 12) + + // Doesn't provide specific conversion JSet => JsArray + shapeless.test.illTyped("implicitly[JSet](arr)") + + implicitly[JsArray](set) must_=== arr and { + implicitly[JsValue](set: JValue) must_=== arr + } + } + + "handle JObject" in { + val jobj = JObject(List( + "foo" -> JString("bar"), + "lorem" -> JDecimal(BigDecimal(12)), + "ipsum" -> JNull)) + + val jso = Json.obj( + "foo" -> "bar", + "lorem" -> 12, + "ipsum" -> JsNull) + + converterTest(jobj, jso) and { + converterTest[JValue, JsValue](jobj, jso) + } + } + + "handle JInt" in { + val jint = JInt(123) + val jnum = JsNumber(123) + + // Doesn't provide specific conversion JInt => JsNumber + shapeless.test.illTyped("implicitly[JInt](jnum)") + + implicitly[JsNumber](jint) must_=== jnum and { + implicitly[JsValue](jint: JValue) must_=== jnum + } + } + + "handle JLong" in { + val jlong = JLong(123) + val jnum = JsNumber(123) + + // Doesn't provide specific conversion JLong => JsNumber + shapeless.test.illTyped("implicitly[JLong](jnum)") + + implicitly[JsNumber](jlong) must_=== jnum and { + implicitly[JsValue](jlong: JValue) must_=== jnum + } + } + + "handle JDouble" in { + val jdouble = JDouble(123) + val jnum = JsNumber(123) + + // Doesn't provide specific conversion JDouble => JsNumber, ... + shapeless.test.illTyped("implicitly[JDouble](jnum)") + + implicitly[JsNumber](jdouble) must_=== jnum and { + implicitly[JsValue](jdouble: JValue) must_=== jnum + } + } + + "handle JField" in { + val dec = BigDecimal(12.345D) + val jfield = "foo" -> JDecimal(dec) + val field = "foo" -> JsNumber(dec) + + implicitly[(String, JsValue)](jfield) must_=== field and { + implicitly[JField](field) must_=== jfield + } + } + } + + // --- + + private def converterTest[A <: JValue, B <: JsValue](jv: A, js: B)(implicit a2b: A => B, b2a: B => A) = a2b(jv) must_=== js and (b2a(js) must_=== jv) +} + +// --- + +case class Foo( + name: String, + lorem: Int) + +object Foo { + implicit lazy val jsFormat: OFormat[Foo] = Json.format[Foo] +} + +final class Bar(val title: String, val score: Double) { + override def equals(that: Any): Boolean = that match { + case other: Bar => (title, score) == (other.title, other.score) + case _ => false + } + + override def hashCode: Int = (title, score).hashCode +} + +object Bar { + implicit val reader: Reader[Bar] = new Reader[Bar] { + def read(v: JValue): Bar = v match { + case JObject( + ("title", JString(title)) :: ( + "score", JDecimal(score)) :: _) => + new Bar(title, score.toDouble) + + case JObject(("score", JDecimal(score)) :: ( + "title", JString(title)) :: _) => + new Bar(title, score.toDouble) + + case _ => sys.error("unsupported") + } + } + + implicit val writer: Writer[Bar] = new Writer[Bar] { + def write(bar: Bar): JValue = JObject(List( + "title" -> JString(bar.title), + "score" -> JDecimal(BigDecimal(bar.score)) + )) + } +}