diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index df592f129..3fd1ffbf7 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -81,7 +81,7 @@ jobs: strategy: matrix: jdk: [ 11, 8 ] - scala: [ 2.12.15, 2.13.8, 3.0.2 ] # Should be sync with Mergify conditions (.mergify.yml) + scala: [ 2.12.15, 2.13.8, 3.1.2-RC2 ] # Should be sync with Mergify conditions (.mergify.yml) name: Check / Tests (Scala ${{ matrix.scala }} & JDK ${{ matrix.jdk }}) steps: - name: Checkout diff --git a/build.sbt b/build.sbt index f20eac477..b34e69280 100644 --- a/build.sbt +++ b/build.sbt @@ -16,17 +16,10 @@ val isScala3 = Def.setting { CrossVersion.partialVersion(scalaVersion.value).exists(_._1 != 2) } -// specs2 hasn't been doing Scala 3 releases, so we use for3Use2_13. -// -// Since these are just test dependencies, it isn't really a problem. -// But if too much more time passes and specs2 still hasn't released -// for Scala 3, we should consider ripping it out. - def specs2(scalaVersion: String) = - Seq( - "org.specs2" %% "specs2-core" % "4.14.1" % Test, - "org.specs2" %% "specs2-junit" % "4.14.1" % Test, - ).map(_.cross(CrossVersion.for3Use2_13)) + Seq("core", "junit").map { n => + ("org.specs2" %% s"specs2-$n" % "4.14.1").cross(CrossVersion.for3Use2_13) % Test + } val jacksonVersion = "2.11.4" val jacksonDatabindVersion = jacksonVersion @@ -147,28 +140,27 @@ lazy val `play-json` = crossProject(JVMPlatform, JSPlatform) .settings( commonSettings ++ playJsonMimaSettings ++ Def.settings( libraryDependencies ++= ( - if (isScala3.value) Nil + if (isScala3.value) Seq.empty else - Seq( - "org.scala-lang" % "scala-reflect" % scalaVersion.value, - "com.chuusai" %% "shapeless" % "2.3.8" % Test, - ) + Seq("org.scala-lang" % "scala-reflect" % scalaVersion.value) ), libraryDependencies ++= Seq( "org.scalatest" %%% "scalatest" % "3.2.11" % Test, "org.scalatestplus" %%% "scalacheck-1-15" % "3.2.11.0" % Test, "org.scalacheck" %%% "scalacheck" % "1.15.4" % Test, + ("com.chuusai" %% "shapeless" % "2.3.7").cross(CrossVersion.for3Use2_13) % Test ), libraryDependencies += { - if (isScala3.value) + if (isScala3.value) { "org.scala-lang" %% "scala3-compiler" % scalaVersion.value % Provided - else + } else { "org.scala-lang" % "scala-compiler" % scalaVersion.value % Provided + } }, libraryDependencies ++= (CrossVersion.partialVersion(scalaVersion.value) match { - case Some((2, 13)) => Seq() - case Some((3, _)) => Nil + case Some((2, 13)) => Seq.empty + case Some((3, _)) => Seq.empty case _ => Seq(compilerPlugin(("org.scalamacros" % "paradise" % "2.1.1").cross(CrossVersion.full))) }), Compile / unmanagedSourceDirectories += { diff --git a/docs/manual/working/scalaGuide/main/json/ScalaJsonAutomated.md b/docs/manual/working/scalaGuide/main/json/ScalaJsonAutomated.md index 3d478ad83..6625c5a35 100644 --- a/docs/manual/working/scalaGuide/main/json/ScalaJsonAutomated.md +++ b/docs/manual/working/scalaGuide/main/json/ScalaJsonAutomated.md @@ -49,10 +49,17 @@ The above example can be made even more concise by using body parsers with a typ The macros work for classes and traits meeting the following requirements. +**Class in Scala 2.x:** + - It must have a companion object having `apply` and `unapply` methods. - The return types of the `unapply` must match the argument types of the `apply` method. - The parameter names of the `apply` method must be the same as the property names desired in the JSON. +**Class in Scala 3.1.x:** (+3.1.2-RC2) + +- It must be provided a [`Conversion`](https://dotty.epfl.ch/api/scala/Conversion.html) to a `_ <: Product`. +- It must be provided a valid [`ProductOf`](https://dotty.epfl.ch/api/scala/deriving/Mirror$.html#ProductOf-0). + Case classes automatically meet these requirements. For custom classes or traits, you might have to implement them. A trait can also supported, if and only if it's a sealed one and if the sub-types comply with the previous requirements: diff --git a/play-json/shared/src/main/scala-2/play/api/libs/json/JsMacros.scala b/play-json/shared/src/main/scala-2/play/api/libs/json/JsMacros.scala index e30d60b49..119e3a34c 100644 --- a/play-json/shared/src/main/scala-2/play/api/libs/json/JsMacros.scala +++ b/play-json/shared/src/main/scala-2/play/api/libs/json/JsMacros.scala @@ -139,7 +139,7 @@ private[json] trait JsValueMacros { } -trait JsMacrosWithOptions extends JsMacros { +trait JsMacrosWithOptions[Opts <: Json.MacroOptions] extends JsMacros { override def reads[A]: Reads[A] = macro JsMacroImpl.withOptionsReadsImpl[A] override def writes[A]: OWrites[A] = macro JsMacroImpl.withOptionsWritesImpl[A] override def format[A]: OFormat[A] = macro JsMacroImpl.withOptionsFormatImpl[A] diff --git a/play-json/shared/src/main/scala-3/play/api/libs/json/ImplicitResolver.scala b/play-json/shared/src/main/scala-3/play/api/libs/json/ImplicitResolver.scala new file mode 100644 index 000000000..460f3c1a1 --- /dev/null +++ b/play-json/shared/src/main/scala-3/play/api/libs/json/ImplicitResolver.scala @@ -0,0 +1,263 @@ +/* + * Copyright (C) 2009-2021 Lightbend Inc. + */ + +package play.api.libs.json + +import scala.quoted.{ Expr, Quotes, Type } + +private[json] trait ImplicitResolver[A] { + type Q <: Quotes + + protected implicit val quotes: Q + + import quotes.reflect.* + + protected implicit val aTpe: Type[A] + + protected final lazy val aTpeRepr: TypeRepr = TypeRepr.of(using aTpe) + + import Json.Placeholder + + // The placeholder type + protected final lazy val PlaceholderType: TypeRepr = + TypeRepr.of[Placeholder] + + /** + * Refactor the input types, by replacing any type matching the `filter`, + * by the given `replacement`. + */ + @annotation.tailrec + private def refactor( + in: List[TypeRepr], + base: (TypeRepr, /*Type*/ Symbol), + out: List[TypeRepr], + tail: List[ + (List[TypeRepr], (TypeRepr, /*Type*/ Symbol), List[TypeRepr]) + ], + filter: TypeRepr => Boolean, + replacement: TypeRepr, + altered: Boolean + ): (TypeRepr, Boolean) = in match { + case tpe :: ts => + tpe match { + case t if filter(t) => + refactor( + ts, + base, + (replacement :: out), + tail, + filter, + replacement, + true + ) + + case AppliedType(t, as) if as.nonEmpty => + refactor( + as, + t -> t.typeSymbol, + List.empty, + (ts, base, out) :: tail, + filter, + replacement, + altered + ) + + case t => + refactor( + ts, + base, + (t :: out), + tail, + filter, + replacement, + altered + ) + } + + case _ => { + val tpe = base._1.appliedTo(out.reverse) + + tail match { + case (x, y, more) :: ts => + refactor( + x, + y, + (tpe :: more), + ts, + filter, + replacement, + altered + ) + + case _ => tpe -> altered + } + } + } + + /** + * Replaces any reference to the type itself by the Placeholder type. + * @return the normalized type + whether any self reference has been found + */ + private def normalized(tpe: TypeRepr): (TypeRepr, Boolean) = + tpe match { + case t if t =:= aTpeRepr => PlaceholderType -> true + + case AppliedType(t, args) if args.nonEmpty => + refactor( + args, + t -> t.typeSymbol, + List.empty, + List.empty, + _ =:= aTpeRepr, + PlaceholderType, + false + ) + + case t => t -> false + } + + /* Restores reference to the type itself when Placeholder is found. */ + private def denormalized(ptype: TypeRepr): TypeRepr = ptype match { + case t if t =:= PlaceholderType => + aTpeRepr + + case AppliedType(base, args) if args.nonEmpty => + refactor( + args, + base -> base.typeSymbol, + List.empty, + List.empty, + _ == PlaceholderType, + aTpeRepr, + false + )._1 + + case _ => + ptype + } + + private val PlaceholderHandlerName = + "play.api.libs.json.Json.Placeholder.Format" + + /** + * @param tc the type representation of the typeclass + * @param forwardExpr the `Expr` that forward to the materialized instance itself + */ + private class ImplicitTransformer[T](forwardExpr: Expr[T]) extends TreeMap { + private val denorm = denormalized _ + + @SuppressWarnings(Array("AsInstanceOf")) + override def transformTree(tree: Tree)(owner: Symbol): Tree = tree match { + case TypeApply(tpt, args) => + TypeApply( + transformTree(tpt)(owner).asInstanceOf[Term], + args.map(transformTree(_)(owner).asInstanceOf[TypeTree]) + ) + + case t @ (Select(_, _) | Ident(_)) if t.show == PlaceholderHandlerName => + forwardExpr.asTerm + + case tt: TypeTree => + super.transformTree( + TypeTree.of(using denorm(tt.tpe).asType) + )(owner) + + case Apply(fun, args) => + Apply(transformTree(fun)(owner).asInstanceOf[Term], args.map(transformTree(_)(owner).asInstanceOf[Term])) + + case _ => + super.transformTree(tree)(owner) + } + } + + private def createImplicit[M[_]]( + debug: String => Unit + )(tc: Type[M], ptype: TypeRepr, tx: TreeMap): Option[Implicit] = { + val pt = ptype.asType + val (ntpe, selfRef) = normalized(ptype) + val ptpe = ntpe + + // infers given + val neededGivenType = TypeRepr + .of[M](using tc) + .appliedTo(ptpe) + + val neededGiven: Option[Term] = Implicits.search(neededGivenType) match { + case suc: ImplicitSearchSuccess => { + if (!selfRef) { + Some(suc.tree) + } else { + tx.transformTree(suc.tree)(suc.tree.symbol) match { + case t: Term => Some(t) + case _ => Option.empty[Term] + } + } + } + + case _ => + Option.empty[Term] + } + + debug { + val show: Option[String] = + try { + neededGiven.map(_.show) + } catch { + case e: MatchError /* Dotty bug */ => + neededGiven.map(_.symbol.fullName) + } + + s"// Resolve given ${prettyType( + TypeRepr.of(using tc) + )} for ${prettyType(ntpe)} as ${prettyType( + neededGivenType + )} (self? ${selfRef}) = ${show.mkString}" + } + + neededGiven.map(_ -> selfRef) + } + + protected def resolver[M[_], T]( + forwardExpr: Expr[M[T]], + debug: String => Unit + )(tc: Type[M]): TypeRepr => Option[Implicit] = { + val tx = + new ImplicitTransformer[M[T]](forwardExpr) + + createImplicit(debug)(tc, _: TypeRepr, tx) + } + + /** + * @param sym a type symbol + */ + protected def typeName(sym: Symbol): String = + sym.fullName.replaceAll("(\\.package\\$|\\$|java\\.lang\\.|scala\\.Predef\\$\\.)", "") + + // To print the implicit types in the compiler messages + private[json] final def prettyType(t: TypeRepr): String = t match { + case _ if t <:< TypeRepr.of[EmptyTuple] => + "EmptyTuple" + + case AppliedType(ty, a :: b :: Nil) if ty <:< TypeRepr.of[*:] => + s"${prettyType(a)} *: ${prettyType(b)}" + + case AppliedType(_, args) => + typeName(t.typeSymbol) + args.map(prettyType).mkString("[", ", ", "]") + + case OrType(a, b) => + s"${prettyType(a)} | ${prettyType(b)}" + + case _ => { + val sym = t.typeSymbol + + if (sym.isTypeParam) { + sym.name + } else { + typeName(sym) + } + } + } + + type Implicit = (Term, Boolean) +} diff --git a/play-json/shared/src/main/scala-3/play/api/libs/json/JsMacroImpl.scala b/play-json/shared/src/main/scala-3/play/api/libs/json/JsMacroImpl.scala index 4d7a6f0c8..ee812c2d3 100644 --- a/play-json/shared/src/main/scala-3/play/api/libs/json/JsMacroImpl.scala +++ b/play-json/shared/src/main/scala-3/play/api/libs/json/JsMacroImpl.scala @@ -4,159 +4,956 @@ package play.api.libs.json -import scala.collection.Map -import scala.compiletime._ -import scala.deriving._ +import scala.util.{ Failure => TryFailure, Success => TrySuccess } +import scala.collection.mutable.{ Builder => MBuilder } + +import scala.deriving.Mirror.ProductOf + +import scala.quoted._ + +import play.api.libs.functional.ContravariantFunctor /** * Implementation for the JSON macro. */ -object JsMacroImpl { - inline def format[A](using m: Mirror.Of[A]): OFormat[A] = OFormat[A](reads, writes) - - inline def reads[A](using m: Mirror.Of[A]): Reads[A] = new Reads[A] { self => - given subject: Reads[A] = this - def reads(js: JsValue) = js match { - case obj @ JsObject(_) => - inline m match { - case m: Mirror.ProductOf[A] => readElems[A](obj)(using self, m) - case m: Mirror.SumOf[A] => readCases[A](obj)(using self, m) - } - case _ => JsError("error.expected.jsobject") - } +object JsMacroImpl { // TODO: debug + import Json.MacroOptions + + def withOptionsReads[A: Type, Opts <: MacroOptions: Type]( + configExpr: Expr[JsonConfiguration.Aux[Opts]] + )(using + Quotes, + Type[Reads] + ): Expr[Reads[A]] = + configuredReads[A, Opts](configExpr) + + def withOptionsWrites[A: Type, Opts <: MacroOptions: Type]( + configExpr: Expr[JsonConfiguration.Aux[Opts]] + )(using + Quotes, + Type[OWrites] + ): Expr[OWrites[A]] = + configuredWrites[A, Opts](configExpr) + + def withOptionsFormat[A: Type, Opts <: MacroOptions: Type]( + configExpr: Expr[JsonConfiguration.Aux[Opts]] + )(using + Quotes, + Type[OFormat], + Type[Format] + ): Expr[OFormat[A]] = + formatImpl(configExpr) + + def reads[A: Type](using + Quotes, + Type[Reads] + ): Expr[Reads[A]] = + withSummonedConfig(configuredReads(_)) + + def writes[A: Type](using + Quotes, + Type[OWrites] + ): Expr[OWrites[A]] = + withSummonedConfig(configuredWrites(_)) + + def format[A: Type](using + Quotes, + Type[Format] + ): Expr[OFormat[A]] = + withSummonedConfig(formatImpl(_)) + + def anyValFormat[A <: AnyVal: Type](using + Quotes + ): Expr[Format[A]] = '{ + // format: off + Format[A](${ anyValReads[A] }, ${ anyValWrites[A] }) } - inline def writes[A](using m: Mirror.Of[A]): OWrites[A] = new OWrites[A] { self => - given subject: OWrites[A] = this - def writes(x: A) = inline m match { - case m: Mirror.ProductOf[A] => writeElems[A](x)(using self, m) - case m: Mirror.SumOf[A] => writeCases[A](x)(using self, m) + // --- + + private def withSummonedConfig[T: Type](f: Expr[JsonConfiguration.Aux[_ <: MacroOptions]] => Expr[T])(using + q: Quotes + ): Expr[T] = + Expr.summon[JsonConfiguration.Aux[_ <: MacroOptions]] match { + case Some(config) => + f(config) + + case None => + q.reflect.report.errorAndAbort("No instance of JsonConfiguration is available in the implicit scope") + } + + inline private def withSelfOReads[A]( + f: Reads[A] => Reads[A] + ): Reads[A] = { + new Reads[A] { self => + lazy val underlying = f(self) + + def reads(js: JsValue): JsResult[A] = underlying.reads(js) } } - inline def readElems[A: Reads](obj: JsObject)(using m: Mirror.ProductOf[A]): JsResult[A] = { - inline val size = constValue[Tuple.Size[m.MirroredElemTypes]] - readElemsL[A, m.MirroredElemLabels, m.MirroredElemTypes](obj, new Array[Any](size))(0) + private def configuredReads[A: Type, OptsT <: MacroOptions: Type]( + config: Expr[JsonConfiguration], + )(using + q: Quotes + ): Expr[Reads[A]] = { + import q.reflect.* + + val expr = '{ + lazy val cfg = ${ config } + + withSelfOReads[A] { (forwardReads: Reads[A]) => ${ readsExpr[A, OptsT]('cfg, 'forwardReads) } } + } + + if (debugEnabled) { + report.info(s"/* Generated Reads:\n${expr.asTerm.show(using Printer.TreeAnsiCode)}\n*/") + } + + expr } - inline def writeElems[A: OWrites](x: A)(using m: Mirror.ProductOf[A]): JsObject = - writeElemsL[A, m.MirroredElemLabels, m.MirroredElemTypes](x)(0, Map.empty) + private def readsExpr[A: Type, OptsT <: MacroOptions: Type]( + config: Expr[JsonConfiguration], + forwardExpr: Expr[Reads[A]] + )(using + q: Quotes, + rt: Type[Reads] + ): Expr[Reads[A]] = { + import q.reflect.* + + val tpe = Type.of[A] + val repr = TypeRepr.of[A](using tpe) + + val helper = new ReadsHelper[q.type, A] { + type Q = q.type + val quotes = q + + val aTpe = tpe + + type Opts = OptsT + val optsTpe = Type.of[Opts] + + val readsTpe = rt + } + + def singletonReader: Expr[Reads[A]] = + Expr.summon[ValueOf[A]] match { + case Some(vof) => + '{ Reads[A](_ => JsSuccess(${ vof }.value)) } - inline def readCases[A: Reads](obj: JsObject)(using m: Mirror.SumOf[A]): JsResult[A] = { - obj.value.get(config.discriminator) match { - case None => JsError(JsPath \ config.discriminator, "error.missing.path") - case Some(tjs) => { - val vjs = obj.value.get("_value").getOrElse(obj) - tjs.validate[String].flatMap { dis => - readCasesL[A, m.MirroredElemTypes](vjs, dis) + case _ => + report.errorAndAbort( + s"Something weird is going on with '${helper.prettyType(repr)}'. Should be a singleton but can't parse it" + ) + } + + helper.knownSubclasses(repr) match { + case Some(subTypes) => + helper.familyReads(config, forwardExpr, subTypes) + + case _ => { + import helper.{ prettyType, productReads } + + if (repr.isSingleton) { + singletonReader + } else if (repr.typeSymbol == repr.typeSymbol.moduleClass) { + val instance = Ref(repr.typeSymbol.companionModule).asExprOf[A] + + '{ Reads[A](_ => JsSuccess($instance)) } + } else { + tpe match { + case '[IsProduct[t]] => + Expr.summon[ProductOf[t]] match { + case Some(pof) => + productReads[t, t](config, forwardExpr.asExprOf[Reads[t]], pof).asExprOf[Reads[A]] + + case _ => + report.errorAndAbort( + s"Instance not found: 'ProductOf[${prettyType(repr)}]'" + ) + } + + case '[t] => + Expr.summon[Conversion[t, _ <: Product]] match { + case Some('{ $conv: Conversion[t, IsProduct[p]] }) => + Expr.summon[ProductOf[t]] match { + case Some(pof) => + productReads[t, p](config, forwardExpr.asExprOf[Reads[t]], pof).asExprOf[Reads[A]] + + case _ => + report.errorAndAbort( + s"Instance not found: 'ProductOf[${prettyType(repr)}]'" + ) + } + + case _ => + report.errorAndAbort(s"Instance not found: 'Conversion[${prettyType(repr)}, _ <: Product]'") + } + } } } } } - inline def writeCases[A: OWrites](x: A)(using m: Mirror.SumOf[A]): JsObject = - writeCasesL[A, m.MirroredElemTypes](x, m.ordinal(x))(0) - - inline def readElemsL[A: Reads, L <: Tuple, T <: Tuple](obj: JsObject, elems: Array[Any])( - n: Int - )(using m: Mirror.ProductOf[A]): JsResult[A] = - inline (erasedValue[L], erasedValue[T]) match { - case _: (EmptyTuple, EmptyTuple) => JsSuccess(m.fromProduct(new ArrayProduct(elems))) - case _: (l *: ls, t *: ts) => - readElems1[A, l, t](obj) match { - case e @ JsError(_) => e - case JsSuccess(x, _) => - elems(n) = x - readElemsL[A, ls, ts](obj, elems)(n + 1) + private sealed trait ReadsHelper[Qts <: Quotes, A] + extends MacroHelpers + with QuotesHelper + with OptionSupport + with ImplicitResolver[A] { + type Q = Qts + val quotes: Q + + protected val readsTpe: Type[Reads] + + given _q: Quotes = quotes + import quotes.reflect.* + + def familyReads( + config: Expr[JsonConfiguration], + forwardExpr: Expr[Reads[A]], + subTypes: List[TypeRepr] + ): Expr[Reads[A]] = { + def handleSubTypes(discriminator: Expr[String], input: Expr[JsValue]): Expr[JsResult[A]] = { + type Subtype[U <: A] = U + + val cases = subTypes + .filter { + case tpr @ AppliedType(_, _) => { + report.warning( + s"Generic type ${prettyType(tpr)} is not supported as member of sealed family ${prettyType(aTpeRepr)}." + ) + + false + } + + case _ => true + } + .zipWithIndex + .map { (tpr, i) => + tpr.asType match { + case st @ '[Subtype[sub]] => + val subTpr = TypeRepr.of[sub](using st) + + val bind = Symbol.newBind( + Symbol.spliceOwner, + s"macroTpe${i}", + Flags.Case, + TypeRepr.of[String] + ) + + val tpeCaseName: Expr[String] = '{ + ${ config }.typeNaming(${ Expr(typeName(tpr.typeSymbol)) }) + } + + val resolve = resolver[Reads, sub]( + '{ + @SuppressWarnings(Array("AsInstanceOf")) + def forward = + ${ forwardExpr }.asInstanceOf[Reads[sub]] + + forward + }, + debug + )(readsTpe) + + val body: Expr[JsResult[sub]] = resolve(subTpr) match { + case Some((givenReads, _)) => + '{ + ${ givenReads.asExprOf[Reads[sub]] }.reads(${ input }) + } + + case _ => + report.errorAndAbort(s"Instance not found: ${classOf[Reads[_]].getName}[${prettyType(tpr)}]") + } + + val matchedRef: Expr[String] = Ref(bind).asExprOf[String] + val cond: Expr[Boolean] = '{ ${ matchedRef } == ${ tpeCaseName } } + + CaseDef(Bind(bind, Wildcard()), guard = Some(cond.asTerm), rhs = body.asTerm) + } + } + + val fallback = CaseDef(Wildcard(), None, '{ JsError("error.invalid") }.asTerm) + + Match(discriminator.asTerm, cases :+ fallback).asExprOf[JsResult[A]] + } + + '{ + Reads[A] { + case obj @ JsObject(_) => + obj.value.get(${ config }.discriminator) match { + case Some(jsDiscriminator) => { + // Either read the whole object, or a nested _value object + lazy val input = obj.value.get("_value").getOrElse(obj) + + jsDiscriminator.validate[String].flatMap { discriminator => + ${ handleSubTypes('discriminator, 'input) } + } + } + + case _ => + JsError(JsPath \ ${ config }.discriminator, "error.missing.path") + } + + case _ => JsError("error.expected.jsobject") + } + } + } + + private case class ReadableField[T](sym: Symbol, i: Int, tpr: TypeRepr, default: Option[Expr[T]]) + + def productReads[T: Type, P <: Product: Type]( + config: Expr[JsonConfiguration], + forwardExpr: Expr[Reads[T]], + pof: Expr[ProductOf[T]] + ): Expr[Reads[T]] = { + val tpr = TypeRepr.of[T] + val tprElements = productElements[T, P](tpr, pof) match { + case TryFailure(cause) => + report.errorAndAbort(cause.getMessage) + + case TrySuccess(elms) => + elms + } + + val types = tprElements.map(_._2) + val resolve = resolver[Reads, T](forwardExpr, debug)(readsTpe) + val compCls = tpr.typeSymbol.companionClass + val compMod = Ref(tpr.typeSymbol.companionModule) + + val (optional, required) = tprElements.zipWithIndex + .map { case ((sym, rpt), i) => + val pt = rpt.dealias + + pt.asType match { + case '[t] => + val default: Option[Expr[t]] = + compCls.declaredMethod(f"$$lessinit$$greater$$default$$" + (i + 1)).headOption.collect { + case defaultSym if sym.flags.is(Flags.HasDefault) => + compMod.select(defaultSym).asExprOf[t] + } + + ReadableField(sym, i, pt, default) + } + } + .toSeq + .partition { case ReadableField(_, _, t, _) => isOptionalType(t) } + + def readFields(input: Expr[JsObject]): Expr[JsResult[T]] = { + val reqElmts: Seq[(Int, Expr[JsResult[_]])] = required.map { case ReadableField(param, n, pt, defaultValue) => + pt.asType match { + case ptpe @ '[p] => + val reads: Expr[Reads[p]] = resolve(pt) match { + case Some((givenReads, _)) => + givenReads.asExprOf[Reads[p]] + + case _ => + report.errorAndAbort(s"Instance not found: ${classOf[Reads[_]].getName}[${prettyType(pt)}]") + } + + val pname = param.name + + val get: Expr[JsResult[p]] = { + val field = '{ ${ config }.naming(${ Expr(pname) }) } + val path = '{ JsPath \ ${ field } } + + val pathReads: Expr[Reads[p]] = defaultValue match { + case Some(v) => + '{ ${ path }.readWithDefault[p](${ v.asExprOf[p] })($reads) } + + case _ => + '{ ${ path }.read[p]($reads) } + } + + '{ ${ pathReads }.reads($input) } + } + + n -> get + } + } + + val exElmts: Seq[(Int, Expr[JsResult[_]])] = optional + .map { + case p @ ReadableField(_, _, OptionTypeParameter(it), _) => + p.copy(tpr = it) + + case ReadableField(param, _, pt, _) => + report.errorAndAbort( + s"Invalid optional field '${param.name}': ${prettyType(pt)}" + ) + + } + .map { case ReadableField(param, n, it, defaultValue) => + val pname = param.name + + it.asType match { + case '[i] => + val reads: Expr[Reads[i]] = resolve(it) match { + case Some((givenReads, _)) => + givenReads.asExprOf[Reads[i]] + + case _ => + report.errorAndAbort(s"Instance not found: ${classOf[Reads[_]].getName}[Option[${prettyType(it)}]]") + } + + type p = Option[i] + + val get: Expr[JsResult[p]] = { + val field = '{ ${ config }.naming(${ Expr(pname) }) } + val path = '{ JsPath \ ${ field } } + + val pathReads: Expr[Reads[p]] = defaultValue match { + case Some(v) => + '{ ${ config }.optionHandlers.readHandlerWithDefault($path, ${ v.asExprOf[p] })($reads) } + + case _ => + '{ ${ config }.optionHandlers.readHandler($path)($reads) } + } + + '{ ${ pathReads }.reads($input) } + } + + n -> get + } + } + + val tupElmts: Seq[Expr[JsResult[_]]] = + (reqElmts ++ exElmts).toSeq.sortBy(_._1).map(_._2) + + '{ + trySeq(${ Expr.ofSeq(tupElmts) }).map { ls => ${ pof }.fromProduct(Tuple.fromArray(ls.toArray)) } + } + } + + '{ + Reads[T] { + case obj @ JsObject(_) => + ${ readFields('obj) } + + case _ => + JsError("error.expected.jsobject") } + } + } + } + + inline private def withSelfOWrites[A]( + f: OWrites[A] => OWrites[A] + ): OWrites[A] = { + new OWrites[A] { self => + lazy val underlying = f(self) + + def writes(a: A): JsObject = underlying.writes(a) } + } + + private def configuredWrites[A: Type, OptsT <: MacroOptions: Type]( + config: Expr[JsonConfiguration], + )(using + q: Quotes + ): Expr[OWrites[A]] = { + import q.reflect.* + + ensureFindType[A] + + val expr = '{ + lazy val cfg = ${ config } + + withSelfOWrites[A] { (forwardWrites: OWrites[A]) => ${ writesExpr[A, OptsT]('cfg, 'forwardWrites) } } + } + + if (debugEnabled) { + report.info(s"/* Generated OWrites:\n${expr.asTerm.show(using Printer.TreeAnsiCode)}\n*/") + } + + expr + } - inline def writeElemsL[A: OWrites, L <: Tuple, T <: Tuple](x: A)(n: Int, kvs: Map[String, JsValue]): JsObject = - inline (erasedValue[L], erasedValue[T]) match { - case _: (EmptyTuple, EmptyTuple) => JsObject(kvs) - case _: (l *: ls, t *: ts) => writeElemsL[A, ls, ts](x)(n + 1, kvs ++ writeElems1[A, l, t](x, n)) + private def writesExpr[A: Type, OptsT <: MacroOptions: Type]( + config: Expr[JsonConfiguration], + forwardExpr: Expr[OWrites[A]] + )(using + q: Quotes, + wt: Type[Writes] + ): Expr[OWrites[A]] = { + import q.reflect.* + + val tpe = Type.of[A] + val repr = TypeRepr.of[A](using tpe) + + val helper = new WritesHelper[q.type, A] { + type Q = q.type + val quotes = q + + val aTpe = tpe + + type Opts = OptsT + val optsTpe = Type.of[Opts] + + val writesTpe = wt } - inline def readCasesL[A: Reads, T <: Tuple](js: JsValue, name: String): JsResult[A] = - inline erasedValue[T] match { - case _: (t *: ts) => - if (name == typeName[t]) summonReads[t & A].reads(js) - else readCasesL[A, ts](js, name) - case _: EmptyTuple => JsError("error.invalid") + import helper.{ prettyType, productWrites } + + helper.knownSubclasses(repr) match { + case Some(subTypes) => + helper.familyWrites(config, forwardExpr, subTypes) + + case _ => + if ( + repr.isSingleton || + repr.typeSymbol == repr.typeSymbol.moduleClass + ) { + '{ + val empty = Json.obj() + OWrites[A](_ => empty) + } + } else { + tpe match { + case '[IsProduct[t]] => + Expr.summon[ProductOf[t]] match { + case Some(pof) => + productWrites[t, t](config, forwardExpr.asExprOf[OWrites[t]], '{ identity[t] }, pof) + .asExprOf[OWrites[A]] + + case _ => + report.errorAndAbort( + s"Instance not found: 'ProductOf[${prettyType(repr)}]'" + ) + } + + case '[t] => + Expr.summon[Conversion[t, _ <: Product]] match { + case Some('{ $conv: Conversion[t, IsProduct[p]] }) => + Expr.summon[ProductOf[t]] match { + case Some(pof) => + productWrites[t, p](config, forwardExpr.asExprOf[OWrites[t]], conv, pof).asExprOf[OWrites[A]] + + case _ => + report.errorAndAbort( + s"Instance not found: 'ProductOf[${prettyType(repr)}]'" + ) + } + + case _ => + report.errorAndAbort(s"Instance not found: 'Conversion[${prettyType(repr)}, _ <: Product]'") + } + } + } } + } + + private sealed trait WritesHelper[Qts <: Quotes, A] + extends MacroHelpers + with QuotesHelper + with OptionSupport + with ImplicitResolver[A] { + type Q = Qts + val quotes: Q + + protected val writesTpe: Type[Writes] + + given _q: Quotes = quotes + import quotes.reflect.* + + def familyWrites( + config: Expr[JsonConfiguration], + forwardExpr: Expr[OWrites[A]], + subTypes: List[TypeRepr] + ): Expr[OWrites[A]] = { + def handleSubTypes(input: Expr[A]): Expr[JsObject] = { + type Subtype[U <: A] = U + + val cases = subTypes + .filter { + case tpr @ AppliedType(_, _) => { + report.warning( + s"Generic type ${prettyType(tpr)} is not supported as a member of sealed family ${prettyType(aTpeRepr)}" + ) + + false + } + + case _ => + true + } + .zipWithIndex + .map { (tpr, i) => + tpr.asType match { + case st @ '[Subtype[sub]] => + val subTpr = TypeRepr.of[sub](using st) + + val bind = Symbol.newBind( + Symbol.spliceOwner, + s"macroVal${i}", + Flags.Case, + subTpr + ) + + val tpeCaseName: Expr[String] = '{ + ${ config }.typeNaming(${ Expr(typeName(tpr.typeSymbol)) }) + } + + val resolve = resolver[Writes, sub]( + '{ ${ forwardExpr }.asInstanceOf[Writes[sub]] }, + debug + )(writesTpe) + + val matchedRef: Expr[sub] = Ref(bind).asExprOf[sub] + + val body: Expr[JsObject] = resolve(subTpr) match { + case Some((givenWrites, _)) => + '{ + def output: JsObject = ${ givenWrites.asExprOf[Writes[sub]] }.writes($matchedRef) match { + case obj @ JsObject(_) => + obj + + case jsValue => + Json.obj("_value" -> jsValue) + } - inline def writeCasesL[A: OWrites, T <: Tuple](x: A, ord: Int)(n: Int): JsObject = - inline erasedValue[T] match { - case _: (t *: ts) => - if (ord == n) { - val xjs = summonWrites[t].writes(x.asInstanceOf[t]) - def jso = xjs match { - case xo @ JsObject(_) => xo - case jsv => JsObject(Seq("_value" -> jsv)) + output ++ JsObject(Map(${ config }.discriminator -> JsString(${ tpeCaseName }))) + } + + case _ => + report.errorAndAbort(s"Instance not found: ${classOf[Writes[_]].getName}[${prettyType(tpr)}]") + + } + + CaseDef(Bind(bind, Typed(Wildcard(), Inferred(tpr))), guard = None, rhs = body.asTerm) + } } - JsObject(Map(config.discriminator -> JsString(typeName[t]))) ++ jso - } else writeCasesL[A, ts](x, ord)(n + 1) - case _: EmptyTuple => throw new MatchError(x) + + Match(input.asTerm, cases).asExprOf[JsObject] + } + + '{ + OWrites[A] { (a: A) => ${ handleSubTypes('a) } } + } } - inline def readElems1[A: Reads, L, T](obj: JsObject): JsResult[T] = { - val reader = inline erasedValue[T] match { - case _: Option[a] => config.optionHandlers.readHandler(path[L])(summonReads[a]).asInstanceOf[Reads[T]] - case _ => path[L].read(summonReads[T]) + private case class WritableField(sym: Symbol, i: Int, pt: TypeRepr) + + def productWrites[T: Type, P <: Product: Type]( + config: Expr[JsonConfiguration], + forwardExpr: Expr[OWrites[T]], + toProduct: Expr[T => P], + pof: Expr[ProductOf[T]] + ): Expr[OWrites[T]] = { + val tpr = TypeRepr.of[T] + val tprElements = productElements[T, P](tpr, pof) match { + case TryFailure(cause) => + report.errorAndAbort(cause.getMessage) + + case TrySuccess(elms) => + elms + } + + val types = tprElements.map(_._2) + val resolve = resolver[Writes, T](forwardExpr, debug)(writesTpe) + + val (optional, required) = tprElements.zipWithIndex.view + .map { case ((sym, rpt), i) => + val pt = rpt.dealias + + pt.asType match { + case '[t] => + WritableField(sym, i, pt) + } + } + .toSeq + .partition { case WritableField(_, _, t) => isOptionalType(t) } + + type ElementAcc = MBuilder[(String, JsValue), Map[String, JsValue]] + + def withIdents[U: Type](f: Expr[ElementAcc] => Expr[U]): Expr[U] = + '{ + val ok = Map.newBuilder[String, JsValue] + + ${ f('{ ok }) } + } + + val (tupleTpe, withTupled) = + withTuple[T, P, JsObject](tpr, toProduct, types) + + def writeFields(input: Expr[T]): Expr[JsObject] = + withTupled(input) { tupled => + val fieldMap = withFields(tupled, tupleTpe, tprElements, debug) + + withIdents[JsObject] { bufOk => + val values: Seq[Expr[Unit]] = required.map { case WritableField(param, i, pt) => + val pname = param.name + + val withField = fieldMap.get(pname) match { + case Some(f) => f + + case _ => + report.errorAndAbort( + s"Field not found: ${prettyType(tpr)}.${pname}" + ) + } + + pt.asType match { + case pTpe @ '[p] => + val writes: Expr[Writes[p]] = resolve(pt) match { + case Some((givenWrites, _)) => + givenWrites.asExprOf[Writes[p]] + + case _ => + report.errorAndAbort(s"Instance not found: ${classOf[Writes[_]].getName}[${prettyType(pt)}]") + } + + withField { v => + ('{ + val nme = ${ config }.naming(${ Expr(pname) }) + ${ bufOk } += nme -> ${ writes }.writes(${ v.asExprOf[p] }) + () + }).asTerm + }.asExprOf[Unit] + } + } // end of required.map + + val extra: Seq[Expr[Unit]] = optional.map { + case WritableField(param, i, optType @ OptionTypeParameter(pt)) => + val pname = param.name + + val withField = fieldMap.get(pname) match { + case Some(f) => f + + case _ => + report.errorAndAbort( + s"Field not found: ${prettyType(tpr)}.${pname}" + ) + } + + pt.asType match { + case pTpe @ '[p] => + val writes: Expr[Writes[p]] = resolve(pt) match { + case Some((givenWrites, _)) => + givenWrites.asExprOf[Writes[p]] + + case _ => + report.errorAndAbort(s"Instance not found: ${classOf[Writes[_]].getName}[${prettyType(pt)}]") + } + + val field = '{ ${ config }.naming(${ Expr(pname) }) } + val path = '{ JsPath \ ${ field } } + + val pathWrites: Expr[OWrites[Option[p]]] = '{ + ${ config }.optionHandlers.writeHandler($path)($writes) + } + + withField { v => + ('{ + val nme = ${ config }.naming(${ Expr(pname) }) + val js = ${ pathWrites }.writes(${ v.asExprOf[Option[p]] }) + + ${ bufOk } ++= js.value + () + }).asTerm + }.asExprOf[Unit] + } + } // end of extra.collect + + if (values.isEmpty && extra.isEmpty) { + debug( + s"No field found: class ${prettyType(TypeRepr.of[T])}" + ) + + '{ JsObject(Map.empty) } + } else { + val fields = values ++ extra + + val resExpr = '{ JsObject(${ bufOk }.result()) } + + Block(fields.map(_.asTerm).toList, resExpr.asTerm).asExprOf[JsObject] + } + } + } + + '{ + OWrites[T] { (t: T) => ${ writeFields('t) } } + } } - reader.reads(obj) } - inline def writeElems1[A: OWrites, L, T](x: A, n: Int): Map[String, JsValue] = { - val value = x.asInstanceOf[Product].productElement(n).asInstanceOf[T] - inline erasedValue[T] match { - case _: Option[a] => - val writer = config.optionHandlers.writeHandler(path[L])(summonWrites[a]).asInstanceOf[OWrites[T]] - writer.writes(value).underlying + private def formatImpl[A: Type, Opts <: MacroOptions: Type]( + config: Expr[JsonConfiguration], + )(using + Quotes + ): Expr[OFormat[A]] = '{ + val reads = ${ configuredReads[A, Opts](config) } + val writes = ${ configuredWrites[A, Opts](config) } + + OFormat[A](reads, writes) + } + + def anyValReads[A <: AnyVal: Type](using + q: Quotes + ): Expr[Reads[A]] = { + import q.reflect.* + + val aTpr = TypeRepr.of[A] + val ctor = aTpr.typeSymbol.primaryConstructor + + ctor.paramSymss match { + case List(v: Symbol) :: Nil => + v.tree match { + case vd: ValDef => { + val tpr = vd.tpt.tpe + + tpr.asType match { + case vtpe @ '[t] => + Expr.summon[Reads[t]] match { + case Some(reads) => { + def mapf(in: Expr[t]): Expr[A] = + New(Inferred(aTpr)).select(ctor).appliedTo(in.asTerm).asExprOf[A] + + val expr = '{ + ${ reads }.map { v => ${ mapf('v) } } + } + + debug(expr.show) + + expr + } + + case None => + report.errorAndAbort( + s"Instance not found: ${classOf[Reads[_]].getName}[${tpr.typeSymbol.fullName}]" + ) + } + } + } + + case _ => + report.errorAndAbort( + s"Constructor parameter expected, found: ${v}" + ) + } + case _ => - Map((fieldName[L], summonWrites[T].writes(value))) + report.errorAndAbort( + s"Cannot resolve value reader for '${aTpr.typeSymbol.name}'" + ) + } } - inline def path[L]: JsPath = JsPath \ fieldName[L] - inline def fieldName[L]: String = config.naming(summonLabel[L].asInstanceOf[String]) - inline def typeName[A]: String = config.typeNaming(summonClassName[A]) - inline def config: JsonConfiguration = summonInline[JsonConfiguration] - inline def summonReads[A]: Reads[A] = summonInline[Reads[A]] - inline def summonWrites[A]: Writes[A] = summonInline[Writes[A]] - inline def summonLabel[L]: L = inline erasedValue[L] match { case _: String => constValue[L] } + def anyValWrites[A <: AnyVal: Type](using + q: Quotes + ): Expr[Writes[A]] = { + import q.reflect.* + + val aTpr = TypeRepr.of[A] + val ctor = aTpr.typeSymbol.primaryConstructor - inline def summonClassName[A]: String = ${ summonClassNameImpl[A] } + ctor.paramSymss match { + case List(v: Symbol) :: Nil => + v.tree match { + case vd: ValDef => { + val tpr = vd.tpt.tpe - final class ArrayProduct[A](elems: Array[A]) extends Product { - def canEqual(that: Any): Boolean = true - def productArity: Int = elems.size - def productElement(idx: Int): Any = elems(idx) + tpr.asType match { + case vtpe @ '[t] => + Expr.summon[Writes[t]] match { + case Some(writes) => { + def contramapf(in: Expr[A]): Expr[t] = { + val term = in.asTerm + + term + .select(term.symbol.fieldMember(v.name)) + .asExprOf[t](using vtpe) + } + + val expr = '{ + val fn = summon[ContravariantFunctor[Writes]] + + fn.contramap[t, A](${ writes }, (in: A) => ${ contramapf('in) }) + } + + debug(expr.show) + + expr + } + + case None => + report.errorAndAbort( + s"Instance not found: ${classOf[Writes[_]].getName}[${tpr.typeSymbol.fullName}]" + ) + } + } + } + + case _ => + report.errorAndAbort( + s"Constructor parameter expected, found: ${v}" + ) + } + + case _ => + report.errorAndAbort( + s"Cannot resolve value writer for '${aTpr.typeSymbol.name}'" + ) + + } } - // lampepfl/dotty#7000 No Mirrors for value classes - inline def valueReads[A]: Reads[A] = boom("https://github.com/lampepfl/dotty/issues/7000", "value classes") - inline def valueWrites[A]: Writes[A] = boom("https://github.com/lampepfl/dotty/issues/7000", "value classes") - inline def valueFormat[A]: Format[A] = boom("https://github.com/lampepfl/dotty/issues/7000", "value classes") - - // lampepfl/dotty-feature-requests#162 No support in Mirror for default arguments - inline def withOptionsReads[A: Mirror.Of]: Reads[A] = - boom("https://github.com/lampepfl/dotty-feature-requests/issues/162", "default arguments") - inline def withOptionsWrites[A: Mirror.Of]: OWrites[A] = - boom("https://github.com/lampepfl/dotty-feature-requests/issues/162", "default arguments") - inline def withOptionsFormat[A: Mirror.Of]: OFormat[A] = - boom("https://github.com/lampepfl/dotty-feature-requests/issues/162", "default arguments") - - inline def boom(url: String, name: String) = { - inline val msg = "No support in Scala 3's type class derivation (Mirrors) for " + name - error(msg + ", see " + url) + // --- + + inline private def trySeq(in: Seq[JsResult[_]]): JsResult[Seq[Any]] = { + type JsFail = (JsPath, collection.Seq[JsonValidationError]) + + @annotation.tailrec + def execute( + in: Seq[JsResult[_]], + suc: List[Any], + fail: collection.Seq[JsFail] + ): JsResult[List[Any]] = in.headOption match { + case Some(JsSuccess(v, _)) => + execute(in.tail, v :: suc, fail) + + case Some(JsError(details)) => + execute(in.tail, suc, details ++: fail) + + case _ => + if (fail.nonEmpty) { + JsError(fail) + } else { + JsSuccess(suc.reverse) + } + } + + execute(in, List.empty, List.empty) } - import scala.quoted._ - private def summonClassNameImpl[A: Type](using Quotes): Expr[String] = { - import quotes.reflect._ - val sym = TypeRepr.of[A].typeSymbol - val fqcn = sym.owner.fullName.stripSuffix("$") + "." + sym.name.stripSuffix("$") - Expr(fqcn) + private def ensureFindType[A](using + q: Quotes, + tpe: Type[A] + ): Unit = { + import q.reflect.* + + TypeRepr + .of[A](using tpe) + .dealias match { + case OrType(_, _) => + + case notFound if notFound.typeSymbol == Symbol.noSymbol => + report.errorAndAbort("Type not found") + + case _ => + } } + + private type IsProduct[U <: Product] = U + + private def debug(msg: => String)(using + q: Quotes + ): Unit = + if (debugEnabled) q.reflect.report.info(msg) + + private lazy val debugEnabled: Boolean = + Option(System.getProperty("play.json.macro.debug")).filterNot(_.isEmpty).map(_.toLowerCase).exists { v => + "true".equals(v) || v.substring(0, 1) == "y" + } } diff --git a/play-json/shared/src/main/scala-3/play/api/libs/json/JsMacros.scala b/play-json/shared/src/main/scala-3/play/api/libs/json/JsMacros.scala index 8d9ef2e56..84a088ad4 100644 --- a/play-json/shared/src/main/scala-3/play/api/libs/json/JsMacros.scala +++ b/play-json/shared/src/main/scala-3/play/api/libs/json/JsMacros.scala @@ -25,7 +25,7 @@ private[json] trait JsMacros { * Json.using[Json.MacroOptions with Json.DefaultValues].reads[User] * }}} */ - inline def reads[A](using m: Mirror.Of[A]): Reads[A] = JsMacroImpl.reads[A] + inline def reads[A]: Reads[A] = ${ JsMacroImpl.reads[A] } /** * Creates a `OWrites[T]` by resolving, at compile-time, @@ -50,7 +50,7 @@ private[json] trait JsMacros { * )(unlift(User.unapply)) * }}} */ - inline def writes[A](using m: Mirror.Of[A]): OWrites[A] = JsMacroImpl.writes[A] + inline def writes[A]: OWrites[A] = ${ JsMacroImpl.writes[A] } /** * Creates a `OFormat[T]` by resolving, at compile-time, @@ -75,8 +75,7 @@ private[json] trait JsMacros { * )(User.apply, unlift(User.unapply)) * }}} */ - inline def format[A](using m: Mirror.Of[A]): OFormat[A] = JsMacroImpl.format[A] - + inline def format[A]: OFormat[A] = ${ JsMacroImpl.format[A] } } private[json] trait JsValueMacros { @@ -98,7 +97,8 @@ private[json] trait JsValueMacros { * val r: Reads[IdText] = Json.valueReads * }}} */ - inline def valueReads[A]: Reads[A] = JsMacroImpl.valueReads[A] + inline def valueReads[A <: AnyVal]: Reads[A] = + ${ JsMacroImpl.anyValReads[A] } /** * Creates a `OWrites[T]`, if `T` is a ValueClass, @@ -117,7 +117,8 @@ private[json] trait JsValueMacros { * val w: Writes[TextId] = Json.valueWrites[TextId] * }}} */ - inline def valueWrites[A]: Writes[A] = JsMacroImpl.valueWrites[A] + inline def valueWrites[A <: AnyVal]: Writes[A] = + ${ JsMacroImpl.anyValWrites[A] } /** * Creates a `OFormat[T]` by resolving, if `T` is a ValueClass @@ -135,12 +136,38 @@ private[json] trait JsValueMacros { * implicit val userFormat: Format[User] = Json.valueFormat[User] * }}} */ - inline def valueFormat[A]: Format[A] = JsMacroImpl.valueFormat[A] + inline def valueFormat[A <: AnyVal]: Format[A] = + ${ JsMacroImpl.anyValFormat[A] } + + // --- + + /** Only for internal purposes */ + final class Placeholder {} // TODO: Common with Scala-2 + + /** Only for internal purposes */ + object Placeholder { + + implicit object Format extends OFormat[Placeholder] { + val success = + JsSuccess(new Placeholder()) + + def reads(json: JsValue): JsResult[Placeholder] = success + + def writes(pl: Placeholder) = Json.obj() + } + } } -trait JsMacrosWithOptions { - inline def reads[A: Mirror.Of]: Reads[A] = JsMacroImpl.withOptionsReads[A] - inline def writes[A: Mirror.Of]: OWrites[A] = JsMacroImpl.withOptionsWrites[A] - inline def format[A: Mirror.Of]: OFormat[A] = JsMacroImpl.withOptionsFormat[A] +trait JsMacrosWithOptions[Opts <: Json.MacroOptions] { + withOpts: Json.WithOptions[Opts] => + + inline def reads[A]: Reads[A] = + ${ JsMacroImpl.withOptionsReads[A, Opts]('config) } + + inline def writes[A]: OWrites[A] = + ${ JsMacroImpl.withOptionsWrites[A, Opts]('config) } + + inline def format[A]: OFormat[A] = + ${ JsMacroImpl.withOptionsFormat[A, Opts]('config) } } diff --git a/play-json/shared/src/main/scala-3/play/api/libs/json/MacroHelpers.scala b/play-json/shared/src/main/scala-3/play/api/libs/json/MacroHelpers.scala new file mode 100644 index 000000000..09fbc7dfb --- /dev/null +++ b/play-json/shared/src/main/scala-3/play/api/libs/json/MacroHelpers.scala @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2009-2021 Lightbend Inc. + */ + +package play.api.libs.json + +import scala.deriving._ + +import scala.quoted._ + +private[json] trait MacroHelpers { self: OptionSupport => + type Q <: Quotes + protected val quotes: Q + + import quotes.reflect.* + + // format: off + private given q: Q = quotes + + /* Some(A) for Option[A] else None */ + protected object OptionTypeParameter { + + def unapply(tpr: TypeRepr): Option[TypeRepr] = { + if (self.isOptionalType(tpr)) { + // TODO: tpr.typeArgs.headOption + + tpr match { + case AppliedType(_, args) => args.headOption + case _ => None + } + } else None + } + } +} + +private[json] trait OptionSupport { + type Q <: Quotes + protected val quotes: Q + + import quotes.reflect.* + + // format: off + private given q: Q = quotes + + protected type Opts <: Json.MacroOptions + + protected final lazy val optionTpe: TypeRepr = TypeRepr.of[Option[_]] + + /* Type of compile-time options; See [[MacroOptions]] */ + protected def optsTpe: Type[Opts] + protected final def optsTpr: TypeRepr = TypeRepr.of(using optsTpe) + + @inline protected final def hasOption[O: Type]: Boolean = + optsTpr <:< TypeRepr.of[O] + + @inline protected final def isOptionalType(tpr: TypeRepr): Boolean = + tpr <:< optionTpe +} diff --git a/play-json/shared/src/main/scala-3/play/api/libs/json/QuotesHelper.scala b/play-json/shared/src/main/scala-3/play/api/libs/json/QuotesHelper.scala new file mode 100644 index 000000000..26594b58b --- /dev/null +++ b/play-json/shared/src/main/scala-3/play/api/libs/json/QuotesHelper.scala @@ -0,0 +1,309 @@ +/* + * Copyright (C) 2009-2021 Lightbend Inc. + */ + +package play.api.libs.json + +import scala.util.{ Try => TryResult } +import scala.util.{ Success => TrySuccess } +import scala.util.{ Failure => TryFailure } + +import scala.deriving.Mirror.ProductOf + +import scala.quoted.Expr +import scala.quoted.Quotes +import scala.quoted.Type + +// TODO: Unit tests +private[json] trait QuotesHelper { + protected type Q <: Quotes + + protected val quotes: Q + + import quotes.reflect.* + + // format: off + private given q: Q = quotes + // format: on + + protected final lazy val anyValTpe: TypeRepr = TypeRepr.of[AnyVal] + + /** + * Recursively find the sub-classes of `tpr`. + * + * Sub-abstract types are not listed, but their own sub-types are examined; + * e.g. for trait `Foo` + * + * {{{ + * sealed trait Foo + * case class Bar(name: String) extends Foo + * sealed trait SubFoo extends Foo + * case class Lorem() extends SubFoo + * }}} + * + * Class `Lorem` is listed through `SubFoo`, + * but `SubFoo` itself is not returned. + */ + final def knownSubclasses(tpr: TypeRepr): Option[List[TypeRepr]] = + tpr.classSymbol.flatMap { cls => + @annotation.tailrec + def subclasses( + children: List[Tree], + out: List[TypeRepr] + ): List[TypeRepr] = { + val childTpr = children.headOption.collect { + case tpd: Typed => + tpd.tpt.tpe + + case vd: ValDef => + vd.tpt.tpe + + case cd: ClassDef => + cd.constructor.returnTpt.tpe + + } + + childTpr match { + case Some(child) => { + val tpeSym = child.typeSymbol + + if ( + (tpeSym.flags.is(Flags.Abstract) && + tpeSym.flags.is(Flags.Sealed) && + !(child <:< anyValTpe)) || + (tpeSym.flags.is(Flags.Sealed) && + tpeSym.flags.is(Flags.Trait)) + ) { + // Ignore sub-trait itself, but check the sub-sub-classes + subclasses(tpeSym.children.map(_.tree) ::: children.tail, out) + } else { + subclasses(children.tail, child :: out) + } + } + + case _ => + out.reverse + } + } + + val types = subclasses(cls.children.map(_.tree), Nil) + + if (types.isEmpty) None else Some(types) + } + + @annotation.tailrec + private def withElems[U <: Product]( + tupled: Expr[U], + fields: List[(Symbol, TypeRepr, Symbol)], + prepared: List[Tuple2[String, (Ref => Term) => Term]] + ): Map[String, (Ref => Term) => Term] = fields match { + case (sym, t, f) :: tail => { + val elem = ValDef.let( + Symbol.spliceOwner, + s"tuple${f.name}", + Typed(tupled.asTerm.select(f), Inferred(t)) + ) + + withElems(tupled, tail, (sym.name -> elem) :: prepared) + } + + case _ => prepared.reverse.toMap + } + + /** + * @param tupled the tupled term + * @param tupleTpe the tuple type + * @param decls the field declarations + */ + def withFields[U <: Product]( + tupled: Expr[U], + tupleTpe: TypeRepr, + decls: List[(Symbol, TypeRepr)], + debug: String => Unit + ): Map[String, (Term => Term) => Term] = { + val fields = decls.zipWithIndex.flatMap { case ((sym, t), i) => + val field = tupleTpe.typeSymbol.declaredMethod(s"_${i + 1}") + + field.map { meth => + debug( + s"// Field: ${sym.owner.owner.fullName}.${sym.name}, type = ${t.typeSymbol.fullName}, annotations = [${sym.annotations.map(_.show).mkString(", ")}]" + ) + + Tuple3(sym, t, meth) + } + } + + withElems[U](tupled, fields, List.empty) + } + + /** + * @tparam T the class type + * @tparam U the type of the product corresponding to class `T` + * @tparam R the result type (from the field operation) + * + * @param tpr the type for which a `ProductOf` is provided + * @param toProduct the function to convert the input value as product `U` + * @param types the types of the elements (fields) + * + * @return The tuple type + `{ v: Term => { tuple: Ref => ... } }` + * with `v` a term of type `tpe`, and `tuple` the product created from. + */ + def withTuple[T, U <: Product, R: Type]( + tpr: TypeRepr, + toProduct: Expr[T => U], + types: List[TypeRepr] + )(using + Type[T], + Type[U] + ): Tuple2[TypeRepr, Expr[T] => (Expr[U] => Expr[R]) => Expr[R]] = { + val unappliedTupleTpr: TypeRepr = { + if (types.isEmpty) { + TypeRepr.of[EmptyTuple] + } else { + TypeRepr.typeConstructorOf(Class.forName(s"scala.Tuple${types.size}")) + } + } + + val tupleTpr = unappliedTupleTpr.appliedTo(types) + + tupleTpr -> { + (in: Expr[T]) => + { (f: (Expr[U] => Expr[R])) => + '{ + val tuple: U = ${ toProduct }($in) + ${ f('{ tuple }) } + } + } + } + } + + /** + * Returns the elements type for `product`. + * + * @param owner the type representation for `T` + */ + def productElements[T, U <: Product]( + owner: TypeRepr, + pof: Expr[ProductOf[T]] + ): TryResult[List[(Symbol, TypeRepr)]] = { + + @annotation.tailrec + def elementTypes( + max: Int, + tpes: List[TypeRepr], + ts: List[TypeRepr] + ): List[TypeRepr] = tpes.headOption match { + case Some(AppliedType(ty, a :: b :: Nil)) if ty <:< TypeRepr.of[*:] && max > 0 => + elementTypes(max - 1, b :: tpes.tail, a :: ts) + + case Some(AppliedType(ty, as)) if ty <:< TypeRepr.of[Tuple] && as.size <= max => + elementTypes(max - as.size, as ::: tpes.tail, ts) + + case Some(t) if t =:= TypeRepr.of[EmptyTuple] => + elementTypes(max, tpes.tail, ts) + + case Some(TypeBounds(t, _)) => + elementTypes(max, t :: tpes.tail, ts) + + case Some(t) => + elementTypes(max, tpes.tail, t :: ts) + + case _ => + ts.reverse + } + + val ownerSym = owner.typeSymbol + val paramss = ownerSym.primaryConstructor.paramSymss.flatten.map { s => s.name -> s }.toMap + + def prepare( + elmLabels: TypeRepr, + elmTypes: TypeRepr + ): List[(Symbol, TypeRepr)] = { + val names = + elementTypes(Int.MaxValue, List(elmLabels), List.empty).collect { case ConstantType(StringConstant(n)) => n } + + names + .lazyZip(elementTypes(names.size, List(elmTypes), List.empty)) + .map { case (n, t) => + val csym = paramss.get(n) + def fsym = + Option(ownerSym.declaredField(n)).filterNot(_ == Symbol.noSymbol) + + val psym: Symbol = csym + .orElse(fsym) + .orElse { + ownerSym.declaredMethod(n).headOption + } + .getOrElse( + Symbol.newVal( + ownerSym, + n, + t, + Flags.EmptyFlags, + Symbol.noSymbol + ) + ) + + psym -> t + } + .toList + } + + val elements: Option[List[(Symbol, TypeRepr)]] = pof.asTerm.tpe match { + case Refinement( + Refinement(_, _, TypeBounds(t1 @ AppliedType(tycon1, _), _)), + _, + TypeBounds(t2 @ AppliedType(tycon2, _), _) + ) if tycon1 <:< TypeRepr.of[Product] && tycon2 <:< TypeRepr.of[Product] => + Option(prepare(t2, t1)) + + case Refinement( + ref @ Refinement(_, _, TypeBounds(t1 @ TermRef(_, _), _)), + _, + TypeBounds(t2 @ TermRef(_, _), _) + ) if { + val emptyTupTpe = TypeRepr.of[EmptyTuple] + (Ref.term(t1).tpe <:< emptyTupTpe && Ref.term(t2).tpe <:< emptyTupTpe) + } => + None + + case pofTpe => + pofTpe.dealias.typeSymbol.tree match { + case ClassDef(_, _, _, _, members) => + members + .collect { + case TypeDef( + n @ ("MirroredElemTypes" | "MirroredElemLabels"), + tt: TypeTree + ) if tt.tpe <:< TypeRepr.of[Product] => + n -> tt.tpe + } + .sortBy(_._1) match { + case (_, elmLabels) :: (_, elmTypes) :: Nil => + Option(prepare(elmLabels, elmTypes)) + + case _ => + Some(List.empty[(Symbol, TypeRepr)]) + } + + case _ => + Some(List.empty[(Symbol, TypeRepr)]) + } + } + + elements match { + case Some(ls) if elements.isEmpty => + TryFailure( + new IllegalArgumentException( + s"Ill-typed ProductOf[${owner.typeSymbol.fullName}]: Fails to resolve element types and labels (bad refinement?)" + ) + ) + + case Some(ls) => + TrySuccess(ls) + + case _ => + TrySuccess(List.empty[(Symbol, TypeRepr)]) + } + } +} diff --git a/play-json/shared/src/main/scala/play/api/libs/json/JsPath.scala b/play-json/shared/src/main/scala/play/api/libs/json/JsPath.scala index 88a002b91..450e51220 100644 --- a/play-json/shared/src/main/scala/play/api/libs/json/JsPath.scala +++ b/play-json/shared/src/main/scala/play/api/libs/json/JsPath.scala @@ -45,6 +45,7 @@ case class RecursiveSearch(key: String) extends PathNode { if (k == this.key) Right(this -> v) else Left(KeyPathNode(k) -> v) }.toList + case arr: JsArray => arr.value.toList.zipWithIndex.map { case (js, j) => Left(IdxPathNode(j) -> js) } @@ -74,7 +75,8 @@ case class KeyPathNode(key: String) extends PathNode { if (k == this.key) Right(this -> v) else Left(KeyPathNode(k) -> v) }.toList - case _ => List() + + case _ => List.empty } private[json] override def toJsonField(value: JsValue) = diff --git a/play-json/shared/src/main/scala/play/api/libs/json/Json.scala b/play-json/shared/src/main/scala/play/api/libs/json/Json.scala index 237167c05..7b07de236 100644 --- a/play-json/shared/src/main/scala/play/api/libs/json/Json.scala +++ b/play-json/shared/src/main/scala/play/api/libs/json/Json.scala @@ -190,7 +190,9 @@ object Json extends JsonFacade with JsMacros with JsValueMacros { def toJson[T](o: T)(implicit tjs: Writes[T]): JsValue = tjs.writes(o) - def toJsObject[T](o: T)(implicit tjs: OWrites[T]): JsObject = tjs.writes(o) + def toJsObject[T](o: T)(implicit + tjs: OWrites[T] + ): JsObject = tjs.writes(o) def fromJson[T](json: JsValue)(implicit fjs: Reads[T]): JsResult[T] = fjs.reads(json) @@ -272,7 +274,8 @@ object Json extends JsonFacade with JsMacros with JsValueMacros { */ final class WithOptions[Opts <: MacroOptions](val config: JsonConfiguration.Aux[Opts]) extends JsonFacade - with JsMacrosWithOptions { + with JsMacrosWithOptions[Opts] { + def this() = this(JsonConfiguration.default) @inline def parse(input: String): JsValue = Json.parse(input) @@ -285,6 +288,7 @@ object Json extends JsonFacade with JsMacros with JsValueMacros { Json.asciiStringify(json) @inline def prettyPrint(json: JsValue): String = Json.prettyPrint(json) + @inline def toJson[T](o: T)(implicit tjs: Writes[T]): JsValue = Json.toJson[T](o) diff --git a/play-json/shared/src/main/scala/play/api/libs/json/JsonConfiguration.scala b/play-json/shared/src/main/scala/play/api/libs/json/JsonConfiguration.scala index 7049594cb..519c275b4 100644 --- a/play-json/shared/src/main/scala/play/api/libs/json/JsonConfiguration.scala +++ b/play-json/shared/src/main/scala/play/api/libs/json/JsonConfiguration.scala @@ -153,10 +153,12 @@ trait OptionHandlers { jsPath.readNullableWithDefault(defaultValue) } + // Not used by Scala3 macros final def formatHandler[T](jsPath: JsPath)(implicit format: Format[T]): OFormat[Option[T]] = { OFormat(readHandler(jsPath), writeHandler(jsPath)) } + // Not used by Scala3 macros final def formatHandlerWithDefault[T](jsPath: JsPath, defaultValue: => Option[T])(implicit format: Format[T] ): OFormat[Option[T]] = { diff --git a/play-json/shared/src/test/scala-2/play/api/libs/json/MacroScala2Spec.scala b/play-json/shared/src/test/scala-2/play/api/libs/json/MacroScala2Spec.scala index c9b8e20a0..dadd63362 100644 --- a/play-json/shared/src/test/scala-2/play/api/libs/json/MacroScala2Spec.scala +++ b/play-json/shared/src/test/scala-2/play/api/libs/json/MacroScala2Spec.scala @@ -7,184 +7,11 @@ package play.api.libs.json import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec -import org.scalacheck.Gen - class MacroScala2Spec extends AnyWordSpec with Matchers with org.scalatestplus.scalacheck.ScalaCheckPropertyChecks { import MacroScala2Spec._ - "Reads" should { - "be generated for simple case class" in { - val json = Json.obj("bar" -> "lorem") - val expected = Simple("lorem") - - forAll( - Gen.oneOf( - Json.reads[Simple], - Json.configured.reads[Simple], - Json.using[Json.MacroOptions].reads[Simple] - ) - ) { _.reads(json).mustEqual(JsSuccess(expected)) } - } - - "be generated for a sealed family" when { - // lampepfl/dotty-feature-requests#161 No Mirror for sealed hierarchies that contain sealed trait children - "subtype is a sealed trait itself" in { - val expected = Leaf2(1) - val expectedJs = Json.obj("_type" -> "play.api.libs.json.MacroScala2Spec.Leaf2", "value" -> 1) - - Json.toJson[TreeValue](expected).mustEqual(expectedJs) - expectedJs.validate[TreeValue].mustEqual(JsSuccess(expected)) - } - } - - // lampepfl/dotty#7000 No Mirrors for value classes - "be generated for a ValueClass" in { - val expected = new TextId("foo") - implicit val r: Reads[TextId] = Json.valueReads - - JsString("foo").validate[TextId].mustEqual(JsSuccess(expected)) - } - } - - "Writes" should { - // lampepfl/dotty#7000 No Mirrors for value classes - "be generated for a ValueClass" in { - val js = Json.valueWrites[TextId].writes(new TextId("bar")) - - js.mustEqual(JsString("bar")) - } - } - "Macro" should { - // lampepfl/dotty#11054 Type aliasing breaks constValue - "handle case class with self type as nested type parameter" when { - import TestFormats._ - - val jsonNoValue = Json.obj("id" -> "A") - val jsonStrValue = Json.obj("id" -> "B", "value" -> "str") - val jsonFooValue = Json.obj("id" -> "C", "value" -> jsonStrValue) - - val fooStrValue = Foo(Foo.id("B"), Some(Left("str"))) - val fooFooValue = Foo(Foo.id("C"), Some(Right(fooStrValue))) - - def readSpec(r: Reads[Foo]) = { - r.reads(jsonNoValue).mustEqual(JsSuccess(Foo(Foo.id("A"), None))) - r.reads(jsonStrValue).mustEqual(JsSuccess(fooStrValue)) - r.reads(jsonFooValue).mustEqual(JsSuccess(fooFooValue)) - r.reads(Json.obj("id" -> "D", "value" -> jsonFooValue)) - .mustEqual(JsSuccess(Foo(Foo.id("D"), Some(Right(fooFooValue))))) - } - - def writeSpec(w: Writes[Foo]) = { - w.writes(Foo(Foo.id("A"), None)).mustEqual(jsonNoValue) - w.writes(fooStrValue).mustEqual(jsonStrValue) - w.writes(fooFooValue).mustEqual(jsonFooValue) - w.writes(Foo(Foo.id("D"), Some(Right(fooFooValue)))).mustEqual(Json.obj("id" -> "D", "value" -> jsonFooValue)) - } - - "to generate Reads" in readSpec(Json.reads[Foo]) - - "to generate Writes" in writeSpec(Json.writes[Foo]) - - "to generate Format" in { - val f: OFormat[Foo] = Json.format[Foo] - - readSpec(f) - writeSpec(f) - } - } - - // lampepfl/dotty-feature-requests#162 No support in Mirror for default arguments - "handle case class with default values" when { - val json01 = Json.obj("id" -> 15) - val json02 = Json.obj("id" -> 15, "a" -> "a") - val json03 = Json.obj("id" -> 15, "a" -> "a", "b" -> "b") - val fixture0 = WithDefault(15, "a", Some("b")) - - val json1 = Json.obj("id" -> 15, "b" -> JsNull) - val fixture1 = WithDefault(15, "a", None) - - val json2 = Json.obj("id" -> 15, "a" -> "aa") - val fixture2 = WithDefault(15, "aa", Some("b")) - - val json3 = Json.obj("id" -> 15, "a" -> "aa", "b" -> "bb") - val fixture3 = WithDefault(15, "aa", Some("bb")) - - val json4 = Json.obj("id" -> 18) - val fixture4 = WithDefault(18) - - def readSpec(r: Reads[WithDefault]) = { - r.reads(json01).mustEqual(JsSuccess(fixture0)) - r.reads(json02).mustEqual(JsSuccess(fixture0)) - r.reads(json03).mustEqual(JsSuccess(fixture0)) - r.reads(json1).mustEqual(JsSuccess(fixture1)) - r.reads(json2).mustEqual(JsSuccess(fixture2)) - r.reads(json3).mustEqual(JsSuccess(fixture3)) - r.reads(json4).mustEqual(JsSuccess(fixture4)) - } - - val jsWithDefaults = Json.using[Json.WithDefaultValues] - - "to generate Reads" in readSpec( - jsWithDefaults.reads[WithDefault] - ) - - "to generate Format" in readSpec( - jsWithDefaults.format[WithDefault] - ) - } - - // lampepfl/dotty-feature-requests#162 No support in Mirror for default arguments - "handle case class with default values, format defined in companion object" in { - val json = Json.obj("id" -> 15) - val expected = WithDefaultInCompanion(15, "a") - Json.fromJson[WithDefaultInCompanion](json).mustEqual(JsSuccess(expected)) - } - - // lampepfl/dotty-feature-requests#162 No support in Mirror for default arguments - "handle case class with default values inner optional case class containing default values" when { - implicit val withDefaultFormat: OFormat[WithDefault] = - Json.using[Json.MacroOptions with Json.DefaultValues].format[WithDefault] - - val json01 = Json.obj("id" -> 3) - val json02 = Json.obj( - "id" -> 3, - "ref" -> Json.obj( - "id" -> 1 - ) - ) - val json03 = Json.obj( - "id" -> 3, - "ref" -> Json.obj( - "id" -> 1, - "a" -> "a", - "b" -> "b" - ) - ) - val fixture0 = ComplexWithDefault(3) - - val json11 = Json.obj("id" -> 15, "ref" -> JsNull) - val fixture1 = ComplexWithDefault(15, None) - - def readSpec(r: Reads[ComplexWithDefault]) = { - r.reads(json01).mustEqual(JsSuccess(fixture0)) - r.reads(json02).mustEqual(JsSuccess(fixture0)) - r.reads(json03).mustEqual(JsSuccess(fixture0)) - r.reads(json11).mustEqual(JsSuccess(fixture1)) - } - - val jsWithDefaults = Json.using[Json.WithDefaultValues] - - "to generate Reads" in readSpec( - jsWithDefaults.reads[ComplexWithDefault] - ) - - "to generate Format" in readSpec( - jsWithDefaults.format[ComplexWithDefault] - ) - } - - // lampepfl/dotty-feature-requests#163 No Mirrors for case classes with implicits + // lampepfl/dotty-feature-requests#163 No Mirrors in Scala 3 for case classes with implicits "handle case class with implicits" when { val json1 = Json.obj("pos" -> 2, "text" -> "str") val json2 = Json.obj("ident" -> "id", "value" -> 23.456D) @@ -214,50 +41,6 @@ class MacroScala2Spec extends AnyWordSpec with Matchers with org.scalatestplus.s } } - // lampepfl/dotty-feature-requests#163 No Mirrors for case classes with implicits - "handle case class with collection types" when { - import TestFormats._ - - val json = Json.obj( - "id" -> "foo", - "ls" -> List(1.2D, 23.45D), - "set" -> List(1, 3, 4, 7), - "seq" -> List( - Json.obj("_1" -> 2, "_2" -> "bar"), - Json.obj("_1" -> 4, "_2" -> "lorem"), - Json.obj("_2" -> "ipsum", "_1" -> 5) - ), - "scores" -> Json.obj("A1" -> 0.1F, "EF" -> 12.3F) - ) - val withColl = WithColl( - id = "foo", - ls = List(1.2D, 23.45D), - set = Set(1, 3, 4, 7), - seq = Seq(2 -> "bar", 4 -> "lorem", 5 -> "ipsum"), - scores = Map("A1" -> 0.1F, "EF" -> 12.3F) - ) - - def readSpec(r: Reads[WithColl[Double, (Int, String)]]) = - r.reads(json).mustEqual(JsSuccess(withColl)) - - def writeSpec(w: Writes[WithColl[Double, (Int, String)]]) = - w.writes(withColl).mustEqual(json) - - "to generated Reads" in readSpec { - Json.reads[WithColl[Double, (Int, String)]] - } - - "to generated Writes".taggedAs(UnstableInScala213) in writeSpec { - Json.writes[WithColl[Double, (Int, String)]] - } - - "to generate Format".taggedAs(UnstableInScala213) in { - val f = Json.format[WithColl[Double, (Int, String)]] - readSpec(f) - writeSpec(f) - } - } - // lampepfl/dotty#7000 No Mirrors for value classes "handle ValueClass" in { val id = new TextId("foo") @@ -271,63 +54,9 @@ class MacroScala2Spec extends AnyWordSpec with Matchers with org.scalatestplus.s } object MacroScala2Spec { - sealed trait Family - case class Simple(bar: String) extends Family - case class Lorem[T](ipsum: T, age: Int) extends Family - case class Optional(prop: Option[String]) extends Family - - object FamilyCodec { - implicit val simpleWrites: OWrites[Simple] = Json.writes[Simple] - implicit val optionalWrites: OWrites[Optional] = Json.writes[Optional] - - implicit val familyWrites: OWrites[Family] = Json.writes[Family] // Failing: - /* java.lang.IllegalArgumentException: - requirement failed: familyWrites is not a valid identifier - */ - } - - sealed trait TreeValue - - sealed trait SubLevel extends TreeValue - - case class Leaf1(value: String) extends TreeValue - case class Leaf2(value: Int) extends SubLevel - object TreeValue { - private implicit val leaf1: OFormat[Leaf1] = Json.format - private implicit val leaf2: OFormat[Leaf2] = Json.format - private implicit val subLevel: OFormat[SubLevel] = Json.format - - implicit val format: OFormat[TreeValue] = Json.format - } - - object Foo { - import shapeless.tag.@@ - - type Id = String @@ Foo - def id(value: String): Id = value.asInstanceOf[Id] - - implicit val idReads: Reads[Id] = implicitly[Reads[String]].asInstanceOf[Reads[Id]] - } - case class Foo(id: Foo.Id, value: Option[Either[String, Foo]]) - - case class WithDefault(id: Int, a: String = "a", b: Option[String] = Some("b")) - case class ComplexWithDefault(id: Int, ref: Option[WithDefault] = Some(WithDefault(1))) - - case class WithImplicit1(pos: Int, text: String)(implicit x: Numeric[Int]) { def x1 = x.one } + case class WithImplicit1(pos: Int, text: String)(implicit + x: Numeric[Int] + ) { def x1 = x.one } case class WithImplicit2[N: Numeric](ident: String, value: N) - - case class WithColl[A: Numeric, B]( - id: String, - ls: List[A], - set: Set[Int], - seq: Seq[B], - scores: Map[String, Float] - ) - - case class WithDefaultInCompanion(id: Int, a: String = "a") - object WithDefaultInCompanion { - implicit val format: OFormat[WithDefaultInCompanion] = - Json.using[Json.WithDefaultValues].format[WithDefaultInCompanion] - } } diff --git a/play-json/shared/src/test/scala-2/play/api/libs/json/MacroSpecCompat.scala b/play-json/shared/src/test/scala-2/play/api/libs/json/MacroSpecCompat.scala new file mode 100644 index 000000000..92ee0e47e --- /dev/null +++ b/play-json/shared/src/test/scala-2/play/api/libs/json/MacroSpecCompat.scala @@ -0,0 +1,11 @@ +/* + * Copyright (C) 2009-2021 Lightbend Inc. + */ + +package play.api.libs.json + +private[json] trait MacroSpecCompat { + object UsingAliasImplicits { + // Empty for Scala2 - Using apply/unapply + } +} diff --git a/play-json/shared/src/test/scala-3/play/api/libs/json/MacroScala3Spec.scala b/play-json/shared/src/test/scala-3/play/api/libs/json/MacroScala3Spec.scala new file mode 100644 index 000000000..c5ec567fb --- /dev/null +++ b/play-json/shared/src/test/scala-3/play/api/libs/json/MacroScala3Spec.scala @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2009-2021 Lightbend Inc. + */ + +package play.api.libs.json + +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +final class MacroScala3Spec + extends AnyWordSpec + with Matchers + with org.scalatestplus.scalacheck.ScalaCheckPropertyChecks { + "Case class" should { + "not be handled" when { + "no Product Conversion" in { + import MacroSpec.UsingAlias + + "Macros.writer[UsingAlias]".mustNot(typeCheck) + } + + "no custom ProductOf" in { + "Macros.writer[CustomNoProductOf]".mustNot(typeCheck) + } + } + } +} + +final class CustomNoProductOf(val name: String, val age: Int) + +object CustomNoProductOf { + + given Conversion[CustomNoProductOf, Tuple2[String, Int]] = + (v: CustomNoProductOf) => v.name -> v.age +} diff --git a/play-json/shared/src/test/scala-3/play/api/libs/json/MacroSpecCompat.scala b/play-json/shared/src/test/scala-3/play/api/libs/json/MacroSpecCompat.scala new file mode 100644 index 000000000..3cb31cd48 --- /dev/null +++ b/play-json/shared/src/test/scala-3/play/api/libs/json/MacroSpecCompat.scala @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2009-2021 Lightbend Inc. + */ + +package play.api.libs.json + +import scala.deriving.Mirror + +import MacroSpec._ + +private[json] trait MacroSpecCompat { + object UsingAliasImplicits: // Required to support non-case class + implicit val conv: Conversion[UsingAlias, Tuple1[OptionalInt]] = + (v: UsingAlias) => Tuple1(v.v) + + implicit object ProductOfUsingAlias extends Mirror.Product { + type MirroredType = UsingAlias + type MirroredElemTypes = Tuple1[OptionalInt] + type MirroredMonoType = UsingAlias + type MirroredLabel = "UsingAlias" + type MirroredElemLabels = Tuple1["v"] + + def fromProduct(p: Product): MirroredMonoType = { + val v = p.productElement(0) + + new UsingAlias(p.productElement(0).asInstanceOf[OptionalInt]) + } + } + end UsingAliasImplicits +} diff --git a/play-json/shared/src/test/scala-3/play/api/libs/json/QuotesSpec.scala b/play-json/shared/src/test/scala-3/play/api/libs/json/QuotesSpec.scala new file mode 100644 index 000000000..0075df870 --- /dev/null +++ b/play-json/shared/src/test/scala-3/play/api/libs/json/QuotesSpec.scala @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2009-2021 Lightbend Inc. + */ + +package play.api.libs.json + +import scala.deriving.Mirror + +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +final class QuotesSpec extends AnyWordSpec with Matchers with org.scalatestplus.scalacheck.ScalaCheckPropertyChecks: + import TestMacros._ + + "Product" should { + "be inspected for elements" when { + "Foo" in { + testProductElements[Foo].mustEqual( + List( + "(val bar,List(),TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class scala)),object Predef),type String))", + "(val lorem,List(),TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class )),object scala),class Int))" + ) + ) + } + + "generic Bar" in { + testProductElements[Bar[Int]].mustEqual( + List( + "(val name,List(),TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class scala)),object Predef),type String))", + "(val opt,List(),AppliedType(TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class )),object scala),class Option),List(TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class )),object scala),class Int))))", + "(val scores,List(),AppliedType(TypeRef(ThisType(TypeRef(NoPrefix,module class immutable)),trait Seq),List(TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class )),object scala),class Double))))" + ) + ) + } + + "of non-case class" when { + "there is no ProductOf" in { + testProductElements[TestUnion.UC].size.mustEqual(0) + } + + "it's defined a ill-typed ProductOf" in { + // Bad refinement type, so element labels/types cannot be resolved + given pof: Mirror.ProductOf[TestUnion.UC] = TestUnion.ProductOfUC + + testProductElements[TestUnion.UC].size.mustEqual(0) + } + + "it's defined a well-typed ProductOf" when { + val expected = List( + "(val name,List(),TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class scala)),object Predef),type String))", + "(val age,List(),TypeRef(TermRef(ThisType(TypeRef(NoPrefix,module class )),object scala),class Int))" + ) + + "by import" in { + import TestUnion.Implicits.productOfUC + + testProductElements[TestUnion.UC].mustEqual(expected) + } + + "by local val" in { + implicit val pof = TestUnion.ProductOfUC + + testProductElements[TestUnion.UC].mustEqual(expected) + } + } + } + } + + "be created" when { + "from Foo" in { + testWithTuple( + Foo("1", 2) + ).mustEqual("scala.Tuple2[scala.Predef.String, scala.Int]/Foo(1,2)") + } + + "from generic Bar" in { + testWithTuple( + Bar[Double]("bar1", None, Seq(1.2D, 34.5D)) + ).mustEqual( + "scala.Tuple3[scala.Predef.String, scala.Option[scala.Double], scala.collection.immutable.Seq[scala.Double]]/Bar(bar1,None,List(1.2, 34.5))" + ) + + testWithTuple( + Bar[Int]("bar2", Some(2), Seq(3.45D)) + ).mustEqual( + "scala.Tuple3[scala.Predef.String, scala.Option[scala.Int], scala.collection.immutable.Seq[scala.Double]]/Bar(bar2,Some(2),List(3.45))" + ) + } + + "from non-case class" when { + "fail when there is no Conversion[T, _ <: Product]" in { + """testWithTuple(new TestUnion.UC("name", 2))""".mustNot(typeCheck) + } + + "fail when Conversion[T, _ <: Product] defined without ProductOf" in { + implicit val conv = TestUnion.ucAsProduct + + testWithTuple( + new TestUnion.UC("name", 2) + ).mustEqual("scala.Tuple$package.EmptyTuple/(name,2)") + } + + "be successful when conversion is provided" in { + import TestUnion.Implicits.productOfUC + implicit val conv = TestUnion.ucAsProduct + + testWithTuple( + new TestUnion.UC("name", 2) + ).mustEqual("scala.Tuple2[scala.Predef.String, scala.Int]/(name,2)") + } + } + } + + "be transformed" when { + "Foo" in { + testWithFields(Foo("3", 4)).mustEqual("bar=3,lorem=4") + } + + "generic Bar" in { + testWithFields( + Bar("bar3", Some("opt2"), Seq(3.1D, 4.5D)) + ).mustEqual("name=bar3,opt=Some(opt2),scores=List(3.1, 4.5)") + } + } + } + + "Direct known subtypes" should { + "be resolved for sealed trait" in { + testKnownSubtypes[TestUnion.UT].mustEqual( + List( + "play.api.libs.json.TestUnion.UA", + "play.api.libs.json.TestUnion.UB", + "play.api.libs.json.TestUnion.UC", + "play.api.libs.json.TestUnion.UD", + "play.api.libs.json.TestUnion.UE" // through UTT sub-trait + ) + ) + } + } +end QuotesSpec + +case class Foo(bar: String, lorem: Int) + +case class Bar[T](name: String, opt: Option[T], scores: Seq[Double]) + +object TestUnion: + sealed trait UT + case object UA extends UT + case class UB(name: String) extends UT + + class UC(val name: String, @CustomAnnot val age: Int) extends UT + + object UD extends UT + + sealed trait UTT extends UT + case class UE() extends UTT + + object ProductOfUC extends Mirror.Product { + type MirroredType = TestUnion.UC + type MirroredElemTypes = Tuple2[String, Int] + type MirroredMonoType = TestUnion.UC + type MirroredLabel = "UC" + type MirroredElemLabels = Tuple2["name", "age"] + + def fromProduct(p: Product): MirroredMonoType = + new TestUnion.UC( + p.productElement(0).asInstanceOf[String], + p.productElement(1).asInstanceOf[Int] + ) + } + + object Implicits: + implicit val productOfUC: ProductOfUC.type = ProductOfUC + end Implicits + + val ucAsProduct: Conversion[TestUnion.UC, Tuple2[String, Int]] = + (uc: TestUnion.UC) => uc.name -> uc.age + + import scala.annotation.{ meta, StaticAnnotation } + + @meta.field + final class CustomAnnot extends StaticAnnotation: + override def hashCode: Int = 1667526726 + + override def equals(that: Any): Boolean = that match { + case _: this.type => true + case _ => false + } + end CustomAnnot +end TestUnion diff --git a/play-json/shared/src/test/scala-3/play/api/libs/json/TestMacros.scala b/play-json/shared/src/test/scala-3/play/api/libs/json/TestMacros.scala new file mode 100644 index 000000000..a38113d85 --- /dev/null +++ b/play-json/shared/src/test/scala-3/play/api/libs/json/TestMacros.scala @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2009-2021 Lightbend Inc. + */ + +package play.api.libs.json + +import scala.deriving.Mirror.ProductOf +import scala.quoted.* + +object TestMacros: + + inline def testKnownSubtypes[T]: List[String] = + ${ testKnownSubtypesMacro[T] } + + def testKnownSubtypesMacro[T: Type](using q: Quotes): Expr[List[String]] = { + import q.reflect.* + + val helper = new QuotesHelper { + type Q = q.type + val quotes = q + } + + Expr( + helper.knownSubclasses(TypeRepr.of[T]).toList.flatten.map(_.show) + ) + } + + // --- + + inline def testProductElements[T]: List[String] = + ${ testProductElementsMacro[T] } + + def testProductElementsMacro[T: Type](using q: Quotes): Expr[List[String]] = { + import q.reflect.* + + val helper = new QuotesHelper { + type Q = q.type + val quotes = q + } + + val tpe = TypeRepr.of[T] + + val names = Expr.summon[ProductOf[T]] match { + case Some(expr) => + helper + .productElements(tpe, expr) + .map(_.map { case (sym, tpr) => + Tuple3(sym, sym.annotations, tpr).toString + }) + .getOrElse(List.empty[String]) + + case _ => + List.empty[String] + } + + Expr(names) + } + + // --- + + inline def testWithTuple[T <: Product]( + pure: T + ): String = + ${ testWithTupleMacro[T, T]('{ pure }, '{ identity[T] }) } + + inline def testWithTuple[T, P <: Product]( + pure: T + )(using + conv: Conversion[T, P] + ): String = + ${ testWithTupleMacro[T, P]('{ pure }, '{ conv(_: T) }) } + + def testWithTupleMacro[T: Type, P <: Product: Type]( + pure: Expr[T], + toProduct: Expr[T => P] + )(using + q: Quotes + ): Expr[String] = { + import q.reflect.* + + val helper = new QuotesHelper { + type Q = q.type + val quotes = q + } + + val tpe = TypeRepr.of[T] + val tpeElements = Expr + .summon[ProductOf[T]] + .map { + helper.productElements(tpe, _).get + } + .getOrElse(List.empty[(Symbol, TypeRepr)]) + + val types = tpeElements.map(_._2) + + val (tupleTpe, withTuple) = + helper.withTuple[T, P, String](tpe, toProduct, types) + + withTuple(pure) { (tupled: Expr[P]) => + val a = Expr(tupleTpe.show) + + '{ + $a + "/" + ${ tupled }.toString + } + } + } + + inline def testWithFields[T <: Product](pure: T): String = + ${ testWithFieldsMacro[T]('{ pure }) } + + def testWithFieldsMacro[T <: Product: Type]( + pure: Expr[T] + )(using + q: Quotes + ): Expr[String] = { + import q.reflect.* + + val helper = new QuotesHelper { + type Q = q.type + val quotes = q + } + + val tpe = TypeRepr.of[T] + val tpeElements = Expr + .summon[ProductOf[T]] + .map { + helper.productElements(tpe, _).get + } + .get + val types = tpeElements.map(_._2) + + val (tupleTpe, withTuple) = + helper.withTuple[T, T, String](tpe, '{ identity[T] }, types) + + withTuple(pure) { (tupled: Expr[T]) => + val fieldMap = + helper.withFields(tupled, tupleTpe, tpeElements, debug = _ => ()) + + val strs: List[Expr[String]] = fieldMap.map { case (nme, withField) => + withField { fi => + val n = Expr[String](nme) + + '{ $n + "=" + ${ fi.asExpr }.toString }.asTerm + }.asExprOf[String] + }.toList + + '{ ${ Expr.ofList(strs) }.mkString(",") } + } + } + +end TestMacros diff --git a/project/Dependencies.scala b/project/Dependencies.scala index ad69c8a52..7ba0cb5bc 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -4,5 +4,5 @@ object Dependencies { // - Mergify conditions (.mergify.yml) val Scala212 = "2.12.15" val Scala213 = "2.13.8" - val Scala3 = "3.0.2" + val Scala3 = "3.1.2-RC2" }