Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
cchantep authored and cchantep committed Jun 26, 2021
1 parent d3e15ce commit 02570d0
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,9 @@ class JsMacroImpl(val c: blackbox.Context) {
selfRef: Boolean
)

val optTpeCtor = typeOf[Option[_]].typeConstructor
val optTpeCtor = typeOf[Option[_]].typeConstructor
val forwardPrefix = "play_jsmacro"
val forwardName = TermName(c.freshName(forwardPrefix))
val forwardName = TermName(c.freshName(forwardPrefix))

// MacroOptions
val options = config.actualType.member(TypeName("Opts")).asType.toTypeIn(config.actualType)
Expand Down Expand Up @@ -691,10 +691,11 @@ class JsMacroImpl(val c: blackbox.Context) {
val (applyFunction, tparams, params, defaultValues) = utility.applyFunction match {
case Some(info) => info

case _ => c.abort(
c.enclosingPosition,
"No apply function found matching unapply parameters"
)
case _ =>
c.abort(
c.enclosingPosition,
"No apply function found matching unapply parameters"
)
}

// ---
Expand Down Expand Up @@ -731,36 +732,38 @@ class JsMacroImpl(val c: blackbox.Context) {
val defaultValue = // not applicable for 'write' only
defaultValueMap.get(name).filter(_ => methodName != "write")

val resolvedImpl = {
val implTpeName = Option(impl.tpe).fold("null")(_.toString)
val resolvedImpl = {
val implTpeName = Option(impl.tpe).fold("null")(_.toString)

if (implTpeName.startsWith(forwardPrefix) ||
(implTpeName.startsWith("play.api.libs.json") &&
!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})"""
}
}
if (implTpeName.startsWith(forwardPrefix) ||
(implTpeName.startsWith("play.api.libs.json") &&
!(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, "Implicit value for '" + $cn + "' was null (forward reference?): " + ${implTpeName})"""
}
}

// - If we're an default value, invoke the withDefault version
// - If we're an option with default value,
// invoke the WithDefault version
(isOption, defaultValue) match {
case (true, Some(v)) =>
val c = TermName(s"${methodName}HandlerWithDefault")
q"$config.optionHandlers.$c($jspathTree, $v)($resolveImpl)"
q"$config.optionHandlers.$c($jspathTree, $v)($resolvedImpl)"

case (true, _) =>
case (true, _) => {
val c = TermName(s"${methodName}Handler")
q"$config.optionHandlers.$c($jspathTree)($resolveImpl)"
q"$config.optionHandlers.$c($jspathTree)($impl)"
}

case (false, Some(v)) =>
val c = TermName(s"${methodName}WithDefault")
q"$jspathTree.$c($v)($resolveImpl)"
q"$jspathTree.$c($v)($resolvedImpl)"

case _ =>
q"$jspathTree.${TermName(methodName)}($resolveImpl)"
q"$jspathTree.${TermName(methodName)}($resolvedImpl)"
}
}
.reduceLeft[Tree] { (acc, r) =>
Expand Down
20 changes: 14 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 @@ -144,9 +144,7 @@ object OWrites extends PathWrites with ConstraintWrites {
/**
* Returns an instance which uses `f` as [[OWrites.writes]] function.
*/
def apply[A](f: A => JsObject): OWrites[A] = new OWrites[A] {
def writes(a: A): JsObject = f(a)
}
def apply[A](f: A => JsObject): OWrites[A] = new FunctionalOWrites[A](f)

/**
* Transforms the resulting [[JsObject]] using the given function,
Expand All @@ -159,6 +157,12 @@ object OWrites extends PathWrites with ConstraintWrites {
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 @@ -177,9 +181,7 @@ object Writes extends PathWrites with ConstraintWrites with DefaultWrites with G
/**
* Returns an instance which uses `f` as [[Writes.writes]] function.
*/
def apply[A](f: A => JsValue): Writes[A] = new Writes[A] {
def writes(a: A): JsValue = f(a)
}
def apply[A](f: A => JsValue): Writes[A] = new FunctionalWrites[A](f)

/**
* Transforms the resulting [[JsValue]] using the given function,
Expand All @@ -194,6 +196,12 @@ object Writes extends PathWrites with ConstraintWrites with DefaultWrites with G
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 @@ -127,7 +127,9 @@ class JsonExtensionScala2Spec extends AnyWordSpec with Matchers {
implicit val jsonConfiguration: JsonConfiguration.Aux[Json.WithDefaultValues] =
JsonConfiguration[Json.WithDefaultValues](optionHandlers = OptionHandlers.WritesNull)
val formatter = Json.format[OptionalWithDefault]
formatter.writes(OptionalWithDefault()).mustEqual(Json.obj("props" -> JsNull))

formatter.writes(OptionalWithDefault()).mustEqual(Json.obj("props" -> JsNull))

formatter.writes(OptionalWithDefault(Some("foo"))).mustEqual(Json.obj("props" -> "foo"))

formatter.reads(Json.obj()).mustEqual(JsSuccess(OptionalWithDefault()))
Expand Down
70 changes: 14 additions & 56 deletions play-json/shared/src/test/scala/play/api/libs/json/MacroSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -151,39 +151,29 @@ class MacroSpec extends AnyWordSpec with Matchers with org.scalatestplus.scalach
val jsLorem = Json.obj("age" -> 11, "ipsum" -> Json.obj("bar" -> "foo"))

try {
jsLorem.validate[Lorem[Simple]]
} catch {
case NonFatal(npe: NullPointerException) => {
val expected = "Invalid implicit resolution"
npe.getMessage.take(expected.size) mustEqual expected
try {
jsLorem.validate[Lorem[Simple]]
} catch {
case e: Throwable if e.getCause != null =>
// scalatest ExceptionInInitializerError
throw e.getCause
}

case NonFatal(cause) => throw cause
}
}

"fails due to forward reference to Format" in {
implicit def format: Format[Lorem[Simple]] =
InvalidForwardResolution.simpleLoremFormat

val jsLorem = Json.obj("age" -> 11, "ipsum" -> Json.obj("bar" -> "foo"))

try {
jsLorem.validate[Lorem[Simple]]
} catch {
case NonFatal(npe: NullPointerException) => {
val expected = "Invalid implicit resolution"
npe.getMessage.take(expected.size) mustEqual expected
val expected = "Implicit value for 'ipsum'"
npe.getMessage.take(expected.size).mustEqual(expected)
}

case NonFatal(cause) => throw cause
case cause: Throwable =>
cause.printStackTrace()
throw cause
}
}
}

"Writes" should {
"be generated for simple case class" in {
Json.writes[Simple].writes(Simple("lorem")) mustEqual Json.obj("bar" -> "lorem")
Json.writes[Simple].writes(Simple("lorem")).mustEqual(Json.obj("bar" -> "lorem"))
}

"as Format for a generic case class" in {
Expand Down Expand Up @@ -521,38 +511,6 @@ class MacroSpec extends AnyWordSpec with Matchers with org.scalatestplus.scalach
formatter.writes(Obj).mustEqual(jsObj)
formatter.reads(jsObj).mustEqual(JsSuccess(Obj))
}

"fails due to forward reference to Writes" in {
implicit def writes: Writes[Lorem[Simple]] =
InvalidForwardResolution.simpleLoremWrites

try {
Json.toJson(Lorem(age = 11, ipsum = Simple(bar = "foo")))
} catch {
case NonFatal(npe: NullPointerException) => {
val expected = "Invalid implicit resolution"
npe.getMessage.take(expected.size) mustEqual expected
}

case NonFatal(cause) => throw cause
}
}

"fails due to forward reference to Format" in {
implicit def format: Format[Lorem[Simple]] =
InvalidForwardResolution.simpleLoremFormat

try {
Json.toJson(Lorem(age = 11, ipsum = Simple(bar = "foo")))
} catch {
case NonFatal(npe: NullPointerException) => {
val expected = "Invalid implicit resolution"
npe.getMessage.take(expected.size) mustEqual expected
}

case NonFatal(cause) => throw cause
}
}
}
}

Expand Down Expand Up @@ -584,11 +542,11 @@ object MacroSpec {

object InvalidForwardResolution {
// Invalids as forward references to `simpleX`
val simpleLoremReads = Json.reads[Lorem[Simple]]
val simpleLoremReads = Json.reads[Lorem[Simple]]
val simpleLoremWrites = Json.writes[Lorem[Simple]]
val simpleLoremFormat = Json.format[Lorem[Simple]]

implicit val simpleReads: Reads[Simple] = Json.reads
implicit val simpleReads: Reads[Simple] = Json.reads
implicit val simpleWrites: OWrites[Simple] = Json.writes[Simple]
}

Expand Down

0 comments on commit 02570d0

Please sign in to comment.