Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
cchantep committed Sep 25, 2018
1 parent bf10151 commit b0222b1
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 85 deletions.
37 changes: 22 additions & 15 deletions play-json/shared/src/main/scala/play/api/libs/json/Format.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,27 @@ 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)
}
}

/**
* Default Json formatters.
*/
object Format extends PathFormat with ConstraintFormat with DefaultFormat {

val constraints: ConstraintFormat = this
val path: PathFormat = this

Expand All @@ -61,20 +62,26 @@ 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)
}
}

/**
* 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)
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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})"""
}
}

Expand Down
134 changes: 76 additions & 58 deletions play-json/shared/src/main/scala/play/api/libs/json/Reads.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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.
*/
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
28 changes: 22 additions & 6 deletions play-json/shared/src/main/scala/play/api/libs/json/Writes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
}
}

/**
Expand All @@ -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,
Expand All @@ -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)
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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
}

Expand Down Expand Up @@ -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
}

Expand All @@ -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
}

Expand Down

0 comments on commit b0222b1

Please sign in to comment.