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

son-extensions_2.13.14.1.0.1.source-code.play-json.scala Maven / Gradle / Ivy

package com.gu.ai.x.play.json
import scala.reflect.macros.blackbox
import _root_.play.api.libs.json._
import collection.immutable.ListMap
import scala.annotation.implicitNotFound
import scala.reflect.NameTransformer

package object internals {
  /*
  // this would allow implicitlyOption for primitives. move to scala-extensions
  final case class FetchedFormat[T](format: Option[Format[T]])
  object FetchedFormat{
    implicit def fetch[T](implicit format: Format[T] = null): FetchedFormat[T] = FetchedFormat(Option(format))
  }
  def implicitlyOption[T](implicit ev: FetchedFormat[T]) = ev.format
  */
  /** does not work for primitive types */
  def implicitlyOption[T]( implicit ev: T = null ): Option[T] = Option( ev )

  /** Type class for case classes
   */
  final class CaseClass[T]
  object CaseClass {
    def checkCaseClassMacro[T: c.WeakTypeTag]( c: blackbox.Context ) = {
      import c.universe._
      val T = c.weakTypeOf[T]
      if ( !T.typeSymbol.isClass || !T.typeSymbol.asClass.isCaseClass ) c.error( c.enclosingPosition, s"$T does not have case modifier" )
      q"new _root_.com.gu.ai.x.play.json.internals.CaseClass[$T]"
    }
    /** fails compilation if T is not a case class
     *  meaning this can be used as an implicit to check
     */
    implicit def checkCaseClass[T]: CaseClass[T] = macro checkCaseClassMacro[T]
  }

  final class SingletonObject[T]
  object SingletonObject {
    def checkSingletonObjectMacro[T: c.WeakTypeTag]( c: blackbox.Context ) = {
      import c.universe._
      val T = c.weakTypeOf[T]
      if ( !T.typeSymbol.isClass || !T.typeSymbol.asClass.isModuleClass ) c.error( c.enclosingPosition, s"$T is not an object" )
      q"new _root_.com.gu.ai.x.play.json.internals.SingletonObject[$T]"
    }
    /** fails compilation if T is not a singleton object class
     *  meaning this can be used as an implicit to check
     */
    implicit def checkSingletonObject[T]: SingletonObject[T] = macro checkSingletonObjectMacro[T]
  }

  import scala.collection._
  private[json] implicit class IterableExtensions[A, Repr]( val coll: Iterable[A] ) extends AnyVal {
    /** Eliminates duplicates based on the given equivalence function.
     *  There is no guarantee which elements stay in case element two elements are considered equivalent.
     *  this has runtime O(n^2)
     *
     *  @param equivalent comparison function which tests whether the two arguments are considered equivalent.
     */
    def distinctWith[That]( equivalent: ( A, A ) => Boolean ): Iterable[A] = {
      var l = List[A]()
      val b = Iterable.newBuilder[A]
      b.sizeHint( coll )
      for ( elem <- coll ) {
        l.find {
          case first => equivalent( elem, first )
        }.getOrElse {
          l = elem +: l
          b += elem
        }
      }
      b.result
    }
  }
}

import internals._

@implicitNotFound( """could not find implicit value for parameter helper: play.api.libs.json.Reads[${T}]
TRIGGERED BY: could not find implicit value for parameter helper: com.gu.ai.x.play.json.OptionValidationDispatcher[${T}]
TO SOLVE THIS
1. Make sure there is a Reads[${T}] or Format[${T}] in the implicit scope
2. In case of Reads[Option[...]] you need to either
   import com.gu.ai.x.play.json.implicits.optionWithNull // suggested
   or
   import com.gu.ai.x.play.json.implicits.optionNoError // buggy play-json 2.3 behavior
3. In case of Reads[... .type]
   import com.gu.ai.x.play.json.SingletonEncoder.simpleName
   import com.gu.ai.x.play.json.implicits.formatSingleton
""" )
final class OptionValidationDispatcher[T] private[json] ( val validate: JsLookupResult => JsResult[T] ) extends AnyVal

object OptionValidationDispatcher {
  // these methods allow to dispatch via overloading
  // this is required to dispatch when not usign implicit search such as in the implementation of formatAuto
  def dispatch[T]( reads: Reads[T] )( disambiguate: AnyRef = null ): OptionValidationDispatcher[T] = {
    new OptionValidationDispatcher[T]( _.validate[T]( reads ) )
  }
  def dispatch[T]( reads: Reads[T] )(): OptionValidationDispatcher[Option[T]] = {
    new OptionValidationDispatcher[Option[T]]( _.validateOpt[T]( reads ) )
  }

  // these methods allow dispatch via implicit search
  implicit def dispatchNonOption[T: Reads]: OptionValidationDispatcher[T] = {
    new OptionValidationDispatcher[T]( _.validate[T] )
  }
  implicit def dispatchOption[T: Reads]: OptionValidationDispatcher[Option[T]] = {
    new OptionValidationDispatcher[Option[T]]( _.validateOpt[T] )
  }
}

object debugMacro {
  def apply[T]( tree: T ): T = macro Macros.debugMacro
}

object `package` {
  implicit class JsLookupResultExtensions( res: JsLookupResult ) {
    /** properly validate Option and non-Option fields alike */
    def validateAuto[T]( implicit helper: OptionValidationDispatcher[T] ): JsResult[T] = helper.validate( res )
  }
  implicit class JsValueExtensions( res: JsValue ) {
    /** properly validate Option and non-Option fields alike */
    def validateAuto[T]( implicit helper: OptionValidationDispatcher[T] ): JsResult[T] = JsDefined( res ).validateAuto[T]
  }
}

private[json] class Macros( val c: blackbox.Context ) {
  import c.universe._
  val pkg = q"_root_.com.gu.ai.x.play.json"
  val pjson = q"_root_.play.api.libs.json"

  /** like identity but prints desugared code and tree */
  def debugMacro( tree: Tree ): Tree = {
    println( "code:\n  " + tree )
    println( "Tree:\n  " + showRaw( tree ) )
    tree
  }

  private def primaryConstructor( tpe: Type ): MethodSymbol = {
    tpe.decls.collectFirst {
      case m: MethodSymbol if m.isPrimaryConstructor =>
        if ( !m.isPublic )
          c.error( c.enclosingPosition, s"Only classes with public primary constructor are supported. Found: $tpe" )
        m
    }.get
  }
  private def caseClassFieldsTypes( tpe: Type ): ListMap[String, Type] = {
    val paramLists = primaryConstructor( tpe ).paramLists
    val params = paramLists.head

    if ( paramLists.size > 1 )
      c.error( c.enclosingPosition, s"Only one parameter list classes are supported. Found: $tpe" )

    params.foreach {
      p =>
        if ( !p.isPublic )
          c.error( c.enclosingPosition, s"Only classes with all public constructor arguments are supported. Found: $tpe" )
    }

    ListMap( params.map { field =>
      (
        field.name.toTermName.decodedName.toString,
        field.infoIn( tpe ) )
    }: _* )
  }
  private def caseClassFieldsDefaults( tpe: Type ): ListMap[String, Option[Tree]] = {
    if ( tpe.companion == NoType ) {
      ListMap()
    } else {
      ListMap( tpe.companion.member( TermName( "apply" ) ).asTerm.alternatives.find( _.isSynthetic ).get.asMethod.paramLists.flatten.zipWithIndex.map {
        case ( field, i ) =>
          (
            field.name.toTermName.decodedName.toString,
            {
              val method = TermName( s"apply$$default$$${i + 1}" )
              tpe.companion.member( method ) match {
                case NoSymbol => None
                case _        => Some( q"${tpe.typeSymbol.companion}.$method" )
              }
            }
          )
      }: _* )
    }
  }

  def formatAuto[T: c.WeakTypeTag]( encoder: Tree ): Tree = formatAutoInternal( c.weakTypeOf[T], encoder )
  def formatAutoInternal( T: Type, encoder: Tree ): Tree = {
    import internals.IterableExtensions
    def defaultFormatter =
      if ( T <:< typeOf[Option[_]] ) {
        val s = T.typeArgs.head
        q"""
          Format.optionWithNull(${formatAutoInternal( s, encoder )})
        """
      } else if ( isModuleClass( T ) ) {
        q"""
          implicit def simpleName = SingletonEncoder.simpleName
          implicits.formatSingleton
        """
      } else if ( isCaseClass( T ) && caseClassFieldsTypes( T ).size == 1 ) {
        val ArgType = caseClassFieldsTypes( T ).head._2
        val name = TermName( c.freshName )
        q"""
        implicit def $name = $pkg.Jsonx.formatAuto[$ArgType]
        $pkg.Jsonx.formatInline[$T]
        """
      } else if ( isCaseClass( T ) ) {
        val fieldFormatters = caseClassFieldsTypes( T ).map {
          case ( _, t ) => t
        }.toVector.distinctWith( _ =:= _ ).map { t =>
          val name = TermName( c.freshName )
          q"implicit def $name = $pkg.Jsonx.formatAuto[$t]"
        }
        val t = q"""
        ..$fieldFormatters
        $pkg.Jsonx.formatCaseClass[$T]
        """
        t
      } else if ( T.typeSymbol.isClass && T.typeSymbol.asClass.isSealed && T.typeSymbol.asClass.isAbstract ) {
        val fieldFormatters = T.typeSymbol.asClass.knownDirectSubclasses.map { t =>
          val name = TermName( c.freshName )
          q"implicit def $name = Jsonx.formatAuto[$t]"
        }
        q"""
        ..$fieldFormatters
        $pkg.Jsonx.formatSealed[$T]
        """
      } else {
        q"implicitly[Format[$T]]" // produces error message if no formatter defined
      }

    q"""
      {
        import $pjson.{ Format }
        implicit val encoder: NameEncoder = $encoder
        $pkg.internals.implicitlyOption[Format[$T]].getOrElse{
          $defaultFormatter
        }
      }
    """
  }

  private def illegalArgExceptionToJsError( inner: Tree ): Tree = {
    q"""
      {
        import $pjson.{ JsSuccess, JsError, JsonValidationError }
        try { JsSuccess($inner) }
        catch {
          case e: _root_.java.lang.IllegalArgumentException =>
            val sw = new _root_.java.io.StringWriter()
            val pw = new _root_.java.io.PrintWriter(sw)
            e.printStackTrace(pw)
            JsError(JsonValidationError(sw.toString,e))
        }
      }
     """
  }

  def formatInline[T: c.WeakTypeTag]: Tree = {
    val T = c.weakTypeOf[T]
    val fields = caseClassFieldsTypes( T )
    if ( fields.size != 1 )
      c.error( c.enclosingPosition, s"class with exactly one argument required, but found: $T" )
    val ( field, tpe ) = fields.head
    q"""
      {
        import $pjson.{ Json, Format, JsValue }
        new Format[$T]{
          def reads(json: JsValue) = json.validate[$tpe].flatMap(field => ${illegalArgExceptionToJsError( q"new $T(field)" )})
          def writes(obj: $T) = Json.toJson(obj.${TermName( field )})
        }
      }
    """
  }

  def formatCaseClassUseDefaults[T: c.WeakTypeTag]( ev: Tree, encoder: Tree ): Tree = formatCaseClassInternal[T]( ev, encoder, useDefaults = true )

  def formatCaseClass[T: c.WeakTypeTag]( ev: Tree, encoder: Tree ): Tree = formatCaseClassInternal[T]( ev, encoder, useDefaults = false )

  private def formatCaseClassInternal[T: c.WeakTypeTag]( ev: Tree, encoder: Tree, useDefaults: Boolean ): Tree = {
    val T = c.weakTypeOf[T]
    if ( !isCaseClass( T ) )
      c.error( c.enclosingPosition, s"not a case class: $T" )
    val defaults = caseClassFieldsDefaults( T )
    def orDefault( t: Tree, name: String ) = {
      val default = defaults.get( name ).flatten
      default.filter( _ => useDefaults ).map( d => q"$t orElse $pjson.JsSuccess($d)" ).getOrElse( t )
    }
    val ( results, mkResults ) = caseClassFieldsTypes( T ).map {
      case ( k, t ) =>
        val name = TermName( c.freshName )
        val path = q"""(json \ (encoder.encode($k)))"""
        val result = q"""{
          import $pkg._
          bpath.validateAuto[$t].repath(path)
        }"""
        // FIXME: the below needs cleanup
        ( name, q"""val $name: JsResult[$t] = {
            val bpath = $path
            val path = ($pjson.JsPath() \ $k)
            val resolved = path.asSingleJsResult(json)
            val result = if(bpath.isInstanceOf[$pjson.JsDefined]) $result else ${orDefault( result, k )}
            (resolved,result) match {
              case (_,result:$pjson.JsSuccess[_]) => result
              case _ => resolved.flatMap(_ => result)
            }
          }
          """ )
    }.unzip
    val jsonFields = caseClassFieldsTypes( T ).map {
      case ( k, t ) => q"""${Constant( NameTransformer.decode( k ) )} -> $pjson.Json.toJson[$t](obj.${TermName( NameTransformer.encode( k ) )})(implicitly[$pjson.Writes[$t]])"""
    }

    q"""
      {
        import $pjson.{ OFormat, JsValue, JsResult, JsError, JsObject, JsNull }
        new OFormat[$T]{
          def reads(json: JsValue) = {
            ..$mkResults
            val errors = Seq[JsResult[_]](..$results).collect{
              case JsError(values) => values
            }.flatten
            if(errors.isEmpty){
              ${illegalArgExceptionToJsError( q"""new $T(..${results.map( r => q"$r.get" )})""" )}
            } else JsError(errors)
          }
          def writes(obj: $T) = JsObject(Seq[(String,JsValue)](..$jsonFields)
            .filterNot(_._2 == JsNull)
            .map({case (name, value) => ((encoder.encode(name)), value)}))
        }
      }
      """
  }

  private def assertClass[T: c.WeakTypeTag]( msg: String = s"required class or trait" ) = {
    val T = c.weakTypeOf[T].typeSymbol
    if ( !T.isClass ) {
      c.error( c.enclosingPosition, msg + ", found " + T )
    }
  }

  private def assertSealedAbstract[T: c.WeakTypeTag] = {
    assertClass[T]()
    val T = c.weakTypeOf[T].typeSymbol.asClass
    if ( !T.isSealed || !T.isAbstract ) {
      lazy val modifiers = T.toString.split( " " ).dropRight( 1 ).mkString
      c.error( c.enclosingPosition, s"required sealed trait or sealed abstract class, found $modifiers ${T.fullName}" )
    }
  }

  def formatSingletonImplicit[T: c.WeakTypeTag]( encodeSingleton: Tree, ev: Tree ): Tree = formatSingleton[T]( encodeSingleton )

  def formatSingleton[T: c.WeakTypeTag]( encodeSingleton: Tree ): Tree = {
    SingletonObject.checkSingletonObjectMacro[T]( c )
    val T = c.weakTypeOf[T].typeSymbol.asClass
    val t = q"""
      {
        import $pjson.{ Format, JsError, JsSuccess, JsValue }
        val encoded = $encodeSingleton.apply(classOf[$T])
        new Format[$T]{
          def reads(json: JsValue) = {
            if(json == encoded)
              JsSuccess(${T.module})
            else JsError(s"not " + ${T.fullName})
          }
          def writes(obj: $T) = encoded
        }
      }
      """
    //println(t)
    t
  }

  def formatSealed[T: c.WeakTypeTag, FormatT <: Format[T]: c.WeakTypeTag]: Tree = formatSealedInternal[T, FormatT]( None )
  def formatSealedWithFallback[T: c.WeakTypeTag, Fallback <: T: c.WeakTypeTag, FormatT <: Format[T]: c.WeakTypeTag]: Tree = formatSealedInternal[T, FormatT]( Some( c.weakTypeOf[Fallback].typeSymbol.asType ) )
  def formatSealedInternal[T: c.WeakTypeTag, FormatT <: Format[T]: c.WeakTypeTag]( fallback: Option[TypeSymbol] ): Tree = {
    assertSealedAbstract[T]

    val formatClass = c.weakTypeOf[FormatT].typeSymbol

    val T = c.weakTypeOf[T]
    val subs =
      T.typeSymbol
        .asClass
        .knownDirectSubclasses
        .toVector

    if ( subs.isEmpty )
      c.error( c.enclosingPosition, s"""
No child classes found for $T. If there clearly are child classes,
try moving the call into a separate file, a sibbling package, a separate sbt sub project or else.
This can be caused by https://issues.scala-lang.org/browse/SI-7046 which can only be avoided by manually moving the call.
      """ )

    val writes = subs.map {
      sym => cq"""obj: $sym => implicitly[$pjson.$formatClass[$sym]].writes(obj)"""
    }

    val reads = subs
      // don't include fallback
      .filterNot( t => fallback.map( _.toType =:= t.asType.toType ).getOrElse( false ) )
      .map { sym =>
        q"""{
        import $pkg._
        json.validateAuto[$sym]
      }"""
      }
      .reduce( ( l, r ) => q"$l orElse $r" )

    // add fallback last
    val readsWithFallback = fallback.map( f => q"""{
      import $pkg._
      $reads orElse json.validateAuto[$f]
    }""" ) getOrElse reads

    val rootName = Literal( Constant( T.toString ) )
    val subNames = Literal( Constant( subs.map( _.fullName ).mkString( ", " ) ) )

    q"""
      {
        new $pjson.$formatClass[$T]{
          def reads(json: $pjson.JsValue) = $readsWithFallback orElse $pjson.JsError("Could not deserialize to any of the subtypes of "+ $rootName +". Tried: "+ $subNames)
          def writes(obj: $T) = {
            obj match {
              case ..$writes
              case _ => throw new Exception("formatSealed found unexpected object of type "+${Literal( Constant( T.toString ) )}+s": $${obj.getClass}$$obj")
            }
          }
        }
      }
      """
  }

  protected def isCaseClass( tpe: Type ) = tpe.typeSymbol.isClass && tpe.typeSymbol.asClass.isCaseClass

  protected def isModuleClass( tpe: Type ) = tpe.typeSymbol.isClass && tpe.typeSymbol.asClass.isModuleClass
}

object implicits {
  /** very simple optional field Reads that maps "null" to None */
  implicit def optionWithNull[T]( implicit rds: Reads[T] ): Reads[Option[T]] = Reads.optionWithNull[T]

  /** Stupidly reads a field as an Option mapping any error (format or missing field) to None */
  implicit def optionNoError[A]( implicit reads: Reads[A] ): Reads[Option[A]] = Reads.optionNoError[A]

  /** Stupidly reads a field as an Option mapping any error (format or missing field) to None */
  implicit def formatSingleton[T](
    implicit
    encodeSingleton: SingletonEncoder, ev: SingletonObject[T]
  ): Format[T] = macro Macros.formatSingletonImplicit[T]
}

final case class SingletonEncoder( apply: java.lang.Class[_] => JsValue )
object SingletonEncoder {
  import scala.reflect.NameTransformer
  def camel2underscore( str: String ) = (
    str.take( 1 )
    ++
    "[0-9A-Z]".r.replaceAllIn(
      str.drop( 1 ),
      "_" + _.group( 0 ).toLowerCase
    )
  )
  def decodeName( name: String ) = NameTransformer.decode( name.dropRight( 1 ) )
  implicit def simpleName = SingletonEncoder( cls => JsString( decodeName( cls.getSimpleName ) ) )
  implicit def simpleNameLowerCase = SingletonEncoder( cls => JsString( camel2underscore( decodeName( cls.getSimpleName ) ) ) )
  implicit def simpleNameUpperCase = SingletonEncoder( cls => JsString( camel2underscore( decodeName( cls.getSimpleName ) ).toUpperCase ) )
}

sealed trait NameEncoder {
  def encode( str: String ): String
}

case class BaseNameEncoder() extends NameEncoder {
  override def encode( str: String ): String = str
}

case class CamelToSnakeNameEncoder() extends NameEncoder {
  override def encode( str: String ): String = (
    str.take( 1 ).toLowerCase
    ++
    "[0-9A-Z]".r.replaceAllIn(
      str.drop( 1 ),
      "_" + _.group( 0 ).toLowerCase
    )
  )
}

object Encoders {
  implicit val encoder: NameEncoder = BaseNameEncoder()
}

object Jsonx {
  /** Generates a PlayJson Format[T] for a case class T with any number of fields (>22 included)
   */
  def formatCaseClass[T]( implicit ev: CaseClass[T], encoder: NameEncoder ): OFormat[T] = macro Macros.formatCaseClass[T]

  /** Generates a PlayJson Format[T] for a case class T with any number of fields (>22 included)
   *  Uses default values when fields are not found
   */
  def formatCaseClassUseDefaults[T]( implicit ev: CaseClass[T], encoder: NameEncoder ): OFormat[T] = macro Macros.formatCaseClassUseDefaults[T]

  /** Serialize one member classes such as value classes as their single contained value instead of a wrapping js object.
   */
  def formatInline[T]: Format[T] = macro Macros.formatInline[T]

  /** Generates a PlayJson Format[T] for a sealed trait that dispatches to Writes of it's concrete subclasses.
   *  CAREFUL: It uses orElse for Reads in an unspecified order, which can produce wrong results
   *  in case of ambiguities.
   */
  def formatSealed[T]: Format[T] = macro Macros.formatSealed[T, Format[T]]

  /** Generates a PlayJson Format[T] for a sealed trait that dispatches to Writes of it's concrete subclasses.
   *  Uses provided type Fallback as the last resort. Fallback needs to be a subtype of T
   *  and ideally: case class Fallback(json: JsValue) extend T
   *  and using formatInline[Fallback] as the serializer
   *  CAREFUL: It uses orElse for Reads in an unspecified order, which can produce wrong results
   *  in case of ambiguities.
   */
  def formatSealedWithFallback[T, Fallback <: T]: Format[T] = macro Macros.formatSealedWithFallback[T, Fallback, Format[T]]

  /** Generates a PlayJson OFormat[T] for a sealed trait that dispatches to Writes of it's concrete subclasses.
   *  CAREFUL: It uses orElse for Reads in an unspecified order, which can produce wrong results
   *  in case of ambiguities.
   */
  def oFormatSealed[T]: OFormat[T] = macro Macros.formatSealed[T, OFormat[T]]

  /** Generates a PlayJson OFormat[T] for a sealed trait that dispatches to Writes of it's concrete subclasses.
   *  Uses provided type Fallback as the last resort. Fallback needs to be a subtype of T
   *  and ideally: case class Fallback(json: JsValue) extend T
   *  and using formatInline[Fallback] as the serializer
   *  CAREFUL: It uses orElse for Reads in an unspecified order, which can produce wrong results
   *  in case of ambiguities.
   */
  def oFormatSealedWithFallback[T, Fallback <: T]: OFormat[T] = macro Macros.formatSealedWithFallback[T, Fallback, OFormat[T]]

  /** serializes a singleton object of given type with the given encoder */
  def formatSingleton[T](
    implicit
    encodeSingleton: SingletonEncoder
  ): Format[T] = macro Macros.formatSingleton[T]

  /** Fully automatic, recursive formatter generator.
   *  Recognizes overridden formatters from companion objects or implicit scope
   *  Does currently only for for case classes, sealed traits, objects and manually defined formatters.
   *  Automatically, recursively delegates to formatCaseClass, formatSealed, formatInline, formatSingleton, implicitly[ Format[...] ]
   *  Note: defaults to inline single-value case classes. Override if required.
   *  Currently not supported: classes with type arguments including tuples
   */
  def formatAuto[T]( implicit encoder: NameEncoder ): Format[T] = macro Macros.formatAuto[T]
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy