From b0222b18b3a5dedd40549cac197d470276aacbcc Mon Sep 17 00:00:00 2001 From: cchantep Date: Wed, 9 May 2018 15:44:50 +0200 Subject: [PATCH] WIP --- .../scala/play/api/libs/json/Format.scala | 37 +++-- .../play/api/libs/json/JsMacroImpl.scala | 5 +- .../main/scala/play/api/libs/json/Reads.scala | 134 ++++++++++-------- .../scala/play/api/libs/json/Writes.scala | 28 +++- .../scala/play/api/libs/json/MacroSpec.scala | 8 +- 5 files changed, 127 insertions(+), 85 deletions(-) diff --git a/play-json/shared/src/main/scala/play/api/libs/json/Format.scala b/play-json/shared/src/main/scala/play/api/libs/json/Format.scala index 57ae6e0f3..8a5a607cf 100644 --- a/play-json/shared/src/main/scala/play/api/libs/json/Format.scala +++ b/play-json/shared/src/main/scala/play/api/libs/json/Format.scala @@ -32,18 +32,20 @@ object OFormat { implicit def GenericOFormat[T](implicit fjs: Reads[T], tjs: OWrites[T]): Format[T] = apply(fjs, tjs) - def apply[A](read: JsValue => JsResult[A], write: A => JsObject): OFormat[A] = new OFormat[A] { + def apply[A](read: JsValue => JsResult[A], write: A => JsObject): OFormat[A] = + new FunctionalOFormat[A](read, write) - def reads(js: JsValue): JsResult[A] = read(js) + def apply[A](r: Reads[A], w: OWrites[A]): OFormat[A] = + new FunctionalOFormat[A](r.reads(_), w.writes(_)) - def writes(a: A): JsObject = write(a) + // --- - } - - def apply[A](r: Reads[A], w: OWrites[A]): OFormat[A] = new OFormat[A] { - def reads(js: JsValue): JsResult[A] = r.reads(js) - - def writes(a: A): JsObject = w.writes(a) + private[json] final class FunctionalOFormat[A]( + r: JsValue => JsResult[A], + w: A => JsObject + ) extends OFormat[A] { + def reads(js: JsValue): JsResult[A] = r(js) + def writes(a: A): JsObject = w(a) } } @@ -51,7 +53,6 @@ object OFormat { * Default Json formatters. */ object Format extends PathFormat with ConstraintFormat with DefaultFormat { - val constraints: ConstraintFormat = this val path: PathFormat = this @@ -61,9 +62,17 @@ object Format extends PathFormat with ConstraintFormat with DefaultFormat { Format(fa.map(f1), Writes(b => fa.writes(f2(b)))) } - def apply[A](fjs: Reads[A], tjs: Writes[A]): Format[A] = new Format[A] { - def reads(json: JsValue) = fjs.reads(json) - def writes(o: A) = tjs.writes(o) + def apply[A](fjs: Reads[A], tjs: Writes[A]): Format[A] = + new FunctionalFormat[A](fjs, tjs) + + // --- + + private[json] final class FunctionalFormat[A]( + r: Reads[A], + w: Writes[A] + ) extends Format[A] { + def reads(json: JsValue) = r.reads(json) + def writes(o: A) = w.writes(o) } } @@ -71,10 +80,8 @@ object Format extends PathFormat with ConstraintFormat with DefaultFormat { * Default Json formatters. */ trait DefaultFormat { - implicit def GenericFormat[T](implicit fjs: Reads[T], tjs: Writes[T]): Format[T] = new Format[T] { def reads(json: JsValue) = fjs.reads(json) def writes(o: T) = tjs.writes(o) } } - diff --git a/play-json/shared/src/main/scala/play/api/libs/json/JsMacroImpl.scala b/play-json/shared/src/main/scala/play/api/libs/json/JsMacroImpl.scala index 4210c9299..191ef2954 100644 --- a/play-json/shared/src/main/scala/play/api/libs/json/JsMacroImpl.scala +++ b/play-json/shared/src/main/scala/play/api/libs/json/JsMacroImpl.scala @@ -724,10 +724,11 @@ import scala.reflect.macros.blackbox if (implTpeName.startsWith(forwardPrefix) || (implTpeName.startsWith("play.api.libs.json") && - !implTpeName.contains("MacroSpec"))) { + !(implTpeName.startsWith("play.api.libs.json.Functional") || + implTpeName.contains("MacroSpec")))) { impl // Avoid extra check for builtin formats } else { - q"""_root_.java.util.Objects.requireNonNull($impl, "Invalid implicit resolution (forward reference?) for '" + $cn + "': " + ${implTpeName})""" + q"""_root_.java.util.Objects.requireNonNull($impl, "Implicit value for '" + $cn + "' was null (forward reference?): " + ${implTpeName})""" } } diff --git a/play-json/shared/src/main/scala/play/api/libs/json/Reads.scala b/play-json/shared/src/main/scala/play/api/libs/json/Reads.scala index 26625391b..053149891 100644 --- a/play-json/shared/src/main/scala/play/api/libs/json/Reads.scala +++ b/play-json/shared/src/main/scala/play/api/libs/json/Reads.scala @@ -8,7 +8,7 @@ import scala.annotation.implicitNotFound import scala.util.control -import scala.collection.Seq +import scala.collection.{ generic, Seq } import scala.collection.compat._ import scala.collection.immutable.Map import scala.collection.mutable.Builder @@ -144,14 +144,16 @@ object Reads extends ConstraintReads with PathReads with DefaultReads with Gener def map[A, B](m: Reads[A], f: A => B): Reads[B] = m.map(f) - def apply[A, B](mf: Reads[A => B], ma: Reads[A]): Reads[B] = new Reads[B] { def reads(js: JsValue) = applicativeJsResult(mf.reads(js), ma.reads(js)) } - + def apply[A, B](mf: Reads[A => B], ma: Reads[A]): Reads[B] = new Reads[B] { + def reads(js: JsValue): JsResult[B] = + applicativeJsResult(mf.reads(js), ma.reads(js)) + } } implicit def alternative(implicit a: Applicative[Reads]): Alternative[Reads] = new Alternative[Reads] { val app = a def |[A, B >: A](alt1: Reads[A], alt2: Reads[B]): Reads[B] = new Reads[B] { - def reads(js: JsValue) = alt1.reads(js) match { + def reads(js: JsValue): JsResult[B] = alt1.reads(js) match { case r @ JsSuccess(_, _) => r case JsError(es1) => alt2.reads(js) match { case r2 @ JsSuccess(_, _) => r2 @@ -160,13 +162,14 @@ object Reads extends ConstraintReads with PathReads with DefaultReads with Gener } } - def empty: Reads[Nothing] = - new Reads[Nothing] { def reads(js: JsValue) = JsError(Seq()) } - + def empty: Reads[Nothing] = NothingReads } + /** + * Returns an instance which uses `f` as [[Reads.reads]] function. + */ def apply[A](f: JsValue => JsResult[A]): Reads[A] = - new Reads[A] { def reads(json: JsValue) = f(json) } + new Reads[A] { def reads(js: JsValue) = f(js) } implicit def functorReads(implicit a: Applicative[Reads]) = new Functor[Reads] { def fmap[A, B](reads: Reads[A], f: A => B): Reads[B] = a.map(reads, f) @@ -187,44 +190,6 @@ object Reads extends ConstraintReads with PathReads with DefaultReads with Gener implicit val JsArrayReducer = Reducer[JsValue, JsArray](js => JsArray(Array(js))) } -/** - * Low priority reads. - * - * This exists as a compiler performance optimization, so that the compiler doesn't have to rule them out when - * DefaultReads provides a simple match. - * - * See https://github.com/playframework/playframework/issues/4313 for more details. - */ -trait LowPriorityDefaultReads extends EnvReads { - - /** - * Generic deserializer for collections types. - */ - implicit def traversableReads[F[_], A](implicit bf: Factory[A, F[A]], ra: Reads[A]) = new Reads[F[A]] { - def reads(json: JsValue) = json match { - case JsArray(ts) => - - type Errors = Seq[(JsPath, Seq[JsonValidationError])] - def locate(e: Errors, idx: Int) = e.map { case (p, valerr) => (JsPath(idx)) ++ p -> valerr } - - ts.iterator.zipWithIndex.foldLeft(Right(Vector.empty): Either[Errors, Vector[A]]) { - case (acc, (elt, idx)) => (acc, ra.reads(elt)) match { - case (Right(vs), JsSuccess(v, _)) => Right(vs :+ v) - case (Right(_), JsError(e)) => Left(locate(e, idx)) - case (Left(e), _: JsSuccess[_]) => Left(e) - case (Left(e1), JsError(e2)) => Left(e1 ++ locate(e2, idx)) - } - }.fold(JsError.apply, { res => - val builder = bf.newBuilder - builder.sizeHint(res) - builder ++= res - JsSuccess(builder.result()) - }) - case _ => JsError(Seq(JsPath -> Seq(JsonValidationError("error.expected.jsarray")))) - } - } -} - /** * Default deserializer type classes. */ @@ -382,16 +347,17 @@ trait DefaultReads extends LowPriorityDefaultReads { * * @param enum a `scala.Enumeration`. */ - def enumNameReads[E <: Enumeration](enum: E): Reads[E#Value] = new Reads[E#Value] { - def reads(json: JsValue) = json match { - case JsString(str) => - enum.values - .find(_.toString == str) - .map(JsSuccess(_)) - .getOrElse(JsError(Seq(JsPath -> Seq(JsonValidationError("error.expected.validenumvalue"))))) - case _ => JsError(Seq(JsPath -> Seq(JsonValidationError("error.expected.enumstring")))) + def enumNameReads[E <: Enumeration](enum: E): Reads[E#Value] = + new Reads[E#Value] { + def reads(json: JsValue): JsResult[E#Value] = json match { + case JsString(str) => + enum.values + .find(_.toString == str) + .map(JsSuccess(_)) + .getOrElse(JsError(Seq(JsPath -> Seq(JsonValidationError("error.expected.validenumvalue"))))) + case _ => JsError(Seq(JsPath -> Seq(JsonValidationError("error.expected.enumstring")))) + } } - } /** * Deserializer for Boolean types. @@ -535,9 +501,11 @@ trait DefaultReads extends LowPriorityDefaultReads { /** * Deserializer for Array[T] types. */ - implicit def ArrayReads[T: Reads: ClassTag]: Reads[Array[T]] = new Reads[Array[T]] { - def reads(json: JsValue) = json.validate[List[T]].map(_.toArray) - } + implicit def ArrayReads[T: Reads: ClassTag]: Reads[Array[T]] = + new Reads[Array[T]] { + def reads(js: JsValue): JsResult[Array[T]] = + js.validate[List[T]].map(_.toArray) + } /** * Deserializer for java.util.UUID @@ -567,4 +535,54 @@ trait DefaultReads extends LowPriorityDefaultReads { } implicit val uuidReads: Reads[java.util.UUID] = new UUIDReader(false) + + private[json] object NothingReads extends Reads[Nothing] { + def reads(js: JsValue) = JsError(Seq.empty) + } +} + +/** + * Low priority reads. + * + * This exists as a compiler performance optimization, so that the compiler doesn't have to rule them out when + * DefaultReads provides a simple match. + * + * See https://github.com/playframework/playframework/issues/4313 for more details. + */ +trait LowPriorityDefaultReads extends EnvReads { + + /** + * Generic deserializer for collections types. + */ + implicit def traversableReads[F[_], A](implicit bf: generic.CanBuildFrom[F[_], A, F[A]], ra: Reads[A]): Reads[F[A]] = new Reads[F[A]] { + def reads(json: JsValue): JsResult[F[A]] = json match { + case JsArray(ts) => { + type Errors = Seq[(JsPath, Seq[JsonValidationError])] + def locate(e: Errors, idx: Int) = e.map { case (p, valerr) => (JsPath(idx)) ++ p -> valerr } + + ts.iterator.zipWithIndex.foldLeft(Right(Vector.empty): Either[Errors, Vector[A]]) { + case (acc, (elt, idx)) => (acc, ra.reads(elt)) match { + case (Right(vs), JsSuccess(v, _)) => Right(vs :+ v) + case (Right(_), JsError(e)) => Left(locate(e, idx)) + case (Left(e), _: JsSuccess[_]) => Left(e) + case (Left(e1), JsError(e2)) => Left(e1 ++ locate(e2, idx)) + } + }.fold(JsError.apply, { res => + val builder = bf() + builder.sizeHint(res) + builder ++= res + JsSuccess(builder.result()) + }) + } + + case _ => JsError(Seq(JsPath -> Seq(JsonValidationError("error.expected.jsarray")))) + } + } + + // --- + + protected[json] final class FunctionalReads[A]( + r: JsValue => JsResult[A]) extends Reads[A] { + def reads(v: JsValue): JsResult[A] = r(v) + } } diff --git a/play-json/shared/src/main/scala/play/api/libs/json/Writes.scala b/play-json/shared/src/main/scala/play/api/libs/json/Writes.scala index f86f65f11..65325fed4 100644 --- a/play-json/shared/src/main/scala/play/api/libs/json/Writes.scala +++ b/play-json/shared/src/main/scala/play/api/libs/json/Writes.scala @@ -118,9 +118,10 @@ object OWrites extends PathWrites with ConstraintWrites { } - def apply[A](f: A => JsObject): OWrites[A] = new OWrites[A] { - def writes(a: A): JsObject = f(a) - } + /** + * Returns an instance which uses `f` as [[OWrites.writes]] function. + */ + def apply[A](f: A => JsObject): OWrites[A] = new FunctionalOWrites[A](f) /** * Transforms the resulting [[JsObject]] using the given function, @@ -133,6 +134,13 @@ object OWrites extends PathWrites with ConstraintWrites { */ def transform[A](w: OWrites[A])(f: (A, JsObject) => JsObject): OWrites[A] = OWrites[A] { a => f(a, w.writes(a)) } + + // --- + + private[json] final class FunctionalOWrites[A]( + w: A => JsObject) extends OWrites[A] { + def writes(a: A): JsObject = w(a) + } } /** @@ -149,9 +157,10 @@ object Writes extends PathWrites with ConstraintWrites with DefaultWrites with G wa.contramap[B](f) } - def apply[A](f: A => JsValue): Writes[A] = new Writes[A] { - def writes(a: A): JsValue = f(a) - } + /** + * Returns an instance which uses `f` as [[Writes.writes]] function. + */ + def apply[A](f: A => JsValue): Writes[A] = new FunctionalWrites[A](f) /** * Transforms the resulting [[JsValue]] using the given function, @@ -164,6 +173,13 @@ object Writes extends PathWrites with ConstraintWrites with DefaultWrites with G */ def transform[A](w: Writes[A])(f: (A, JsValue) => JsValue): Writes[A] = Writes[A] { a => f(a, w.writes(a)) } + + // --- + + private[json] final class FunctionalWrites[A]( + w: A => JsValue) extends Writes[A] { + def writes(a: A): JsValue = w(a) + } } /** diff --git a/play-json/shared/src/test/scala/play/api/libs/json/MacroSpec.scala b/play-json/shared/src/test/scala/play/api/libs/json/MacroSpec.scala index 1b769f5f5..82ce80ddb 100644 --- a/play-json/shared/src/test/scala/play/api/libs/json/MacroSpec.scala +++ b/play-json/shared/src/test/scala/play/api/libs/json/MacroSpec.scala @@ -164,7 +164,7 @@ class MacroSpec extends WordSpec with MustMatchers jsLorem.validate[Lorem[Simple]] } catch { case NonFatal(npe: NullPointerException) => { - val expected = "Invalid implicit resolution" + val expected = "Implicit value for 'ipsum'" npe.getMessage.take(expected.size) mustEqual expected } @@ -182,7 +182,7 @@ class MacroSpec extends WordSpec with MustMatchers jsLorem.validate[Lorem[Simple]] } catch { case NonFatal(npe: NullPointerException) => { - val expected = "Invalid implicit resolution" + val expected = "Implicit value for 'ipsum'" npe.getMessage.take(expected.size) mustEqual expected } @@ -674,7 +674,7 @@ class MacroSpec extends WordSpec with MustMatchers Json.toJson(Lorem(age = 11, ipsum = Simple(bar = "foo"))) } catch { case NonFatal(npe: NullPointerException) => { - val expected = "Invalid implicit resolution" + val expected = "Implicit value for 'ipsum'" npe.getMessage.take(expected.size) mustEqual expected } @@ -690,7 +690,7 @@ class MacroSpec extends WordSpec with MustMatchers Json.toJson(Lorem(age = 11, ipsum = Simple(bar = "foo"))) } catch { case NonFatal(npe: NullPointerException) => { - val expected = "Invalid implicit resolution" + val expected = "Implicit value for 'ipsum'" npe.getMessage.take(expected.size) mustEqual expected }