All Downloads are FREE. Search and download functionalities are using the official Maven repository.

play.api.libs.json.ops.v4.PlayJsonMacros.scala Maven / Gradle / Ivy

The newest version!
package play.api.libs.json.ops.v4

import play.api.libs.json.{Json, Reads}

import scala.collection.Factory
import scala.language.experimental.macros
import scala.language.reflectiveCalls
import scala.reflect.macros.whitebox

/**
 * Provides macros similar to those in [[Json]] but with modified functionality to better handle our common json
 * parsing use cases, for example missing fields (instead of empty arrays).
 */
object PlayJsonMacros extends TolerantContainerFormats {

  /**
   * Same as [[Json.reads]] but when reading containers such as [[List]], [[Seq]], [[Set]], etc., if the field
   * is missing will return an empty container instead of failing to validate the model.
   *
   * NOTE: this doesn't seem to work with Array, but does work with [[scala.collection.mutable.ArraySeq]] and
   *       other collections which extend [[Traversable]]
   *
   * @see [[TolerantContainerPath.readNullableContainer]]
   * @tparam A The model you're generating a [[Reads]] for
   * @return a [[Reads]] that will deserialize [[A]], with empty containers for fields that are missing from the json.
   */
  def nullableReads[A]: Reads[A] = macro nullableReadsImpl[A]

  /**
   * copied from [[play.api.libs.json.JsMacroImpl.readsImpl]] implementation and modified to use the readNullableContainer helper
   * @see https://github.com/playframework/playframework/blob/2.3.x/framework/src/play-json/src/main/scala/play/api/libs/json/JsMacroImpl.scala#L11
   */
  def nullableReadsImpl[A: c.WeakTypeTag](c: whitebox.Context): c.Expr[Reads[A]] = {
    import c.universe.Flag._
    import c.universe._

    val companioned = weakTypeOf[A].typeSymbol
    val companionSymbol = companioned.companion
    val companionType = companionSymbol.typeSignature

    val libsPkg = Select(Select(Ident(TermName("play")), TermName("api")), TermName("libs"))
    val jsonPkg = Select(libsPkg, TermName("json"))
    val functionalSyntaxPkg = Select(Select(libsPkg, TermName("functional")), TermName("syntax"))
    val utilPkg = Select(jsonPkg, TermName("util"))

    val jsPathSelect = Select(jsonPkg, TermName("JsPath"))
    val readsSelect = Select(jsonPkg, TermName("Reads"))
    val lazyHelperSelect = Select(utilPkg, TypeName("LazyHelper"))

    val importFunctionalSyntax = Import(functionalSyntaxPkg, List(ImportSelector(termNames.WILDCARD, -1, null, -1)))

    companionType.decl(TermName("unapply")) match {
      case NoSymbol => c.abort(c.enclosingPosition, "No unapply function found")
      case s =>
        val unapply = s.asMethod
        val unapplyReturnTypes = unapply.returnType match {
          case TypeRef(_, _, Nil) =>
            c.abort(c.enclosingPosition, s"Apply of ${companionSymbol} has no parameters. Are you using an empty case class?")
          case TypeRef(_, _, args) =>
            args.head match {
              case t @ TypeRef(_, _, Nil) => Some(List(t))
              case t @ TypeRef(_, _, args) =>
                if (t <:< typeOf[Option[_]]) Some(List(t))
                else if (t <:< typeOf[Seq[_]]) Some(List(t))
                else if (t <:< typeOf[Set[_]]) Some(List(t))
                else if (t <:< typeOf[Map[_, _]]) Some(List(t))
                else if (t <:< typeOf[Product]) Some(args)
              case _ => None
            }
          case _ => None
        }

        companionType.decl(TermName("apply")) match {
          case NoSymbol => c.abort(c.enclosingPosition, "No apply function found")
          case s =>
            // searches apply method corresponding to unapply
            val applies = s.asMethod.alternatives
            val apply = applies.collectFirst {
              case apply: MethodSymbol
                if apply.paramLists.headOption.map(_.map(_.asTerm.typeSignature)) == unapplyReturnTypes => apply
            }
            apply match {
              case Some(apply) =>
                val params = apply.paramLists.head // verify there is a single parameter group

                val inferedImplicits = params.map(_.typeSignature).map { implType =>

                  // innerType is only used if we're working with a container and using readNullableContainer below
                  val (isRecursive, tpe, innerType) = implType match {
                    case TypeRef(_, _, args) =>
                      // Option[_] needs special treatment because we need to use XXXOpt
                      if (implType.typeConstructor <:< typeOf[Option[_]].typeConstructor)
                        (args.exists(_.typeSymbol == companioned), args.head, args.head)
                      else if (implType.typeConstructor <:< typeOf[Iterable[_]].typeConstructor)
                        (args.exists(_.typeSymbol == companioned), implType, args.head)
                      else
                        (args.exists(_.typeSymbol == companioned), implType, implType)
                    case _ =>
                      (false, implType, implType)
                  }

                  // builds reads implicit from expected type
                  val neededReadsImplicitType = appliedType(weakTypeOf[Reads[_]].typeConstructor, tpe :: Nil)
                  // infers implicit
                  val neededReadsImplicit = c.inferImplicitValue(neededReadsImplicitType)

                  // summons implicit Factory if type is Iterable
                  val neededFactoryImplicit = if (tpe != innerType) {
                    val neededFactoryImplicitType =
                      appliedType(weakTypeOf[Factory[_, _]].typeConstructor, List(innerType, tpe))
                    c.inferImplicitValue(neededFactoryImplicitType)
                  } else
                    neededReadsImplicit

                  (implType, neededReadsImplicit, neededFactoryImplicit, isRecursive, tpe, innerType)
                }

                // if any implicit is missing, abort
                // else goes on
                inferedImplicits.collect {
                  case (t, readsImplicit, _, rec, _, _) if readsImplicit == EmptyTree && !rec => t
                } match {
                  case List() =>
                    val namedImplicits = params.map(_.name).zip(inferedImplicits)

                    val helperMember = Select(This(typeNames.EMPTY), TermName("lazyStuff"))

                    var hasRec = false

                    // combines all reads into CanBuildX
                    val canBuild = namedImplicits.map {
                      case (name, (t, readsImplicit, bfImplicit, rec, tpe, innerType)) =>
                        // inception of (__ \ name).read(readsImplicit)
                        val jspathTree = Apply(
                          Select(jsPathSelect, TermName(scala.reflect.NameTransformer.encode("\\"))),
                          List(Literal(Constant(name.decodedName.toString)))
                        )

                        if (!rec) {
                          val readTree =
                            if (t.typeConstructor <:< typeOf[Option[_]].typeConstructor)
                              Apply(
                                Select(jspathTree, TermName("readNullable")),
                                List(readsImplicit)
                              )
                            // If Traversable, then apply readNullableContainer helper instead
                            else if (t.typeConstructor <:< typeOf[Iterable[_]].typeConstructor) {
                              val justContainer = tpe.typeConstructor
                              val app = Apply(
                                c.Expr[Any](
                                  q"${Select(jspathTree, TermName("readNullableContainer"))}[$justContainer,$innerType]"
                                ).tree,
                                List(readsImplicit, bfImplicit)
                              )
                              app
                            } else Apply(
                              Select(jspathTree, TermName("read")),
                              List(readsImplicit)
                            )

                          readTree
                        } else {
                          hasRec = true
                          val readTree =
                            if (t.typeConstructor <:< typeOf[Option[_]].typeConstructor)
                              Apply(
                                Select(jspathTree, TermName("readNullable")),
                                List(
                                  Apply(
                                    Select(Apply(jsPathSelect, List()), TermName("lazyRead")),
                                    List(helperMember)
                                  )
                                )
                              )
                            // If Traversable, then apply readNullableContainer helper instead
                            else if (t.typeConstructor <:< typeOf[Iterable[_]].typeConstructor)
                              Apply(
                                Select(jspathTree, TermName("readNullableContainer")),
                                if (tpe.typeConstructor <:< typeOf[List[_]].typeConstructor)
                                  List(
                                    Apply(
                                      Select(readsSelect, TermName("list")),
                                      List(helperMember)
                                    )
                                  )
                                else if (tpe.typeConstructor <:< typeOf[Set[_]].typeConstructor)
                                  List(
                                    Apply(
                                      Select(readsSelect, TermName("set")),
                                      List(helperMember)
                                    )
                                  )
                                else if (tpe.typeConstructor <:< typeOf[Seq[_]].typeConstructor)
                                  List(
                                    Apply(
                                      Select(readsSelect, TermName("seq")),
                                      List(helperMember)
                                    )
                                  )
                                else if (tpe.typeConstructor <:< typeOf[Map[_, _]].typeConstructor)
                                  List(
                                    Apply(
                                      Select(readsSelect, TermName("map")),
                                      List(helperMember)
                                    )
                                  )
                                else List(helperMember)
                              )

                            else {
                              Apply(
                                Select(jspathTree, TermName("lazyRead")),
                                if (tpe.typeConstructor <:< typeOf[List[_]].typeConstructor)
                                  List(
                                    Apply(
                                      Select(readsSelect, TermName("list")),
                                      List(helperMember)
                                    )
                                  )
                                else if (tpe.typeConstructor <:< typeOf[Set[_]].typeConstructor)
                                  List(
                                    Apply(
                                      Select(readsSelect, TermName("set")),
                                      List(helperMember)
                                    )
                                  )
                                else if (tpe.typeConstructor <:< typeOf[Seq[_]].typeConstructor)
                                  List(
                                    Apply(
                                      Select(readsSelect, TermName("seq")),
                                      List(helperMember)
                                    )
                                  )
                                else if (tpe.typeConstructor <:< typeOf[Map[_, _]].typeConstructor)
                                  List(
                                    Apply(
                                      Select(readsSelect, TermName("map")),
                                      List(helperMember)
                                    )
                                  )
                                else List(helperMember)
                              )
                            }

                          readTree
                        }
                    }.reduceLeft { (acc, r) =>
                      Apply(
                        Select(acc, TermName("and")),
                        List(r)
                      )
                    }

                    // builds the final Reads using apply method
                    val applyMethod =
                      Function(
                        params.foldLeft(List[ValDef]())((l, e) =>
                          l :+ ValDef(Modifiers(PARAM), TermName(e.name.encodedName.toString), TypeTree(), EmptyTree)
                        ),
                        Apply(
                          Select(Ident(companionSymbol.name), TermName("apply")),
                          params.foldLeft(List[Tree]())((l, e) =>
                            l :+ Ident(TermName(e.name.encodedName.toString))
                          )
                        )
                      )

                    // if case class has one single field, needs to use inmap instead of canbuild.apply
                    val finalTree = if (params.length > 1) {
                      Apply(
                        Select(canBuild, TermName("apply")),
                        List(applyMethod)
                      )
                    } else {
                      Apply(
                        Select(canBuild, TermName("map")),
                        List(applyMethod)
                      )
                    }

                    if (!hasRec) {
                      c.Expr[Reads[A]](
                        q"""{
                          $importFunctionalSyntax
                          $finalTree
                        }"""
                      )
                    } else {
                      val defineClassWithLazyStuff = ClassDef(
                        Modifiers(Flag.FINAL),
                        TypeName("$anon"),
                        List(),
                        Template(
                          List(
                            AppliedTypeTree(
                              lazyHelperSelect,
                              List(
                                Ident(weakTypeOf[Reads[A]].typeSymbol),
                                Ident(weakTypeOf[A].typeSymbol)
                              )
                            )
                          ),
                          noSelfType,
                          List(
                            DefDef(
                              Modifiers(),
                              termNames.CONSTRUCTOR,
                              List(),
                              List(List()),
                              TypeTree(),
                              Apply(
                                Select(Super(This(typeNames.EMPTY), typeNames.EMPTY), termNames.CONSTRUCTOR),
                                List()
                              )
                            ),
                            ValDef(
                              Modifiers(Flag.OVERRIDE | Flag.LAZY),
                              TermName("lazyStuff"),
                              AppliedTypeTree(Ident(weakTypeOf[Reads[A]].typeSymbol), List(TypeTree(weakTypeOf[A]))),
                              finalTree
                            )
                          )
                        )
                      )
                      val constructInstance = Apply(Select(New(Ident(TypeName("$anon"))), termNames.CONSTRUCTOR), List())
                      val lazyStuff = TermName("lazyStuff")

                      c.Expr[Reads[A]](
                        q"""{
                          $importFunctionalSyntax
                          $defineClassWithLazyStuff
                          $constructInstance
                        }.$lazyStuff"""
                      )
                    }
                  case l => c.abort(c.enclosingPosition, s"No implicit Reads for ${l.mkString(", ")} available.")
                }

              case None => c.abort(c.enclosingPosition, "No apply function found matching unapply return types")
            }

        }
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy