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

zio.schema.DeriveSchema.scala Maven / Gradle / Ivy

There is a newer version: 1.5.0
Show newest version
package zio.schema

import scala.reflect.macros.whitebox

import zio.Chunk

object DeriveSchema {
  import scala.language.experimental.macros

  implicit def gen[T]: Schema[T] = macro genImpl[T]

  def genImpl[T: c.WeakTypeTag](c: whitebox.Context): c.Tree = {
    import c.universe._

    val JavaAnnotationTpe = typeOf[java.lang.annotation.Annotation]

    lazy val optionType = typeOf[Option[_]]
    lazy val listType   = typeOf[List[_]]
    lazy val setType    = typeOf[Set[_]]
    lazy val vectorType = typeOf[Vector[_]]
    lazy val chunkType  = typeOf[Chunk[_]]
    lazy val eitherType = typeOf[Either[_, _]]
    lazy val tuple2Type = typeOf[(_, _)]
    lazy val tuple3Type = typeOf[(_, _, _)]
    lazy val tuple4Type = typeOf[(_, _, _, _)]

    val tpe = weakTypeOf[T]

    def concreteType(seenFrom: Type, tpe: Type): Type =
      tpe.asSeenFrom(seenFrom, seenFrom.typeSymbol.asClass)

    def isCaseObject(tpe: Type): Boolean = tpe.typeSymbol.asClass.isModuleClass

    def isCaseClass(tpe: Type): Boolean = tpe.typeSymbol.asClass.isCaseClass

    def isSealedTrait(tpe: Type): Boolean = tpe.typeSymbol.asClass.isTrait && tpe.typeSymbol.asClass.isSealed

    def isSealedAbstractClass(tpe: Type): Boolean =
      tpe.typeSymbol.asClass.isClass && tpe.typeSymbol.asClass.isSealed && tpe.typeSymbol.asClass.isAbstract

    def isMap(tpe: Type): Boolean = tpe.typeSymbol.fullName == "scala.collection.immutable.Map"

    def collectTypeAnnotations(tpe: Type): List[Tree] =
      tpe.typeSymbol.annotations.collect {
        case annotation if !(annotation.tree.tpe <:< JavaAnnotationTpe) =>
          annotation.tree match {
            case q"new $annConstructor(..$annotationArgs)" =>
              q"new ${annConstructor.tpe.typeSymbol}(..$annotationArgs)"
            case q"new $annConstructor()" =>
              q"new ${annConstructor.tpe.typeSymbol}()"
            case tree =>
              c.warning(c.enclosingPosition, s"Unhandled annotation tree $tree")
              EmptyTree
          }
        case annotation =>
          c.warning(c.enclosingPosition, s"Unhandled annotation ${annotation.tree}")
          EmptyTree
      }.filter(_ != EmptyTree)

    def recurse(tpe: Type, stack: List[Frame[c.type]]): Tree =
      if (isCaseObject(tpe)) {
        val typeId          = q"_root_.zio.schema.TypeId.parse(${tpe.typeSymbol.asClass.fullName})"
        val typeAnnotations = collectTypeAnnotations(tpe)
        val annotations =
          if (typeAnnotations.isEmpty) q"_root_.zio.Chunk.empty"
          else q"_root_.zio.Chunk.apply(..$typeAnnotations)"
        q"_root_.zio.schema.Schema.CaseClass0($typeId, () => ${tpe.typeSymbol.asClass.module}, $annotations)"
      } else if (isCaseClass(tpe)) deriveRecord(tpe, stack)
      else if (isSealedTrait(tpe) || isSealedAbstractClass(tpe))
        deriveEnum(tpe, stack)
      else if (isMap(tpe)) deriveMap(tpe)
      else
        c.abort(
          c.enclosingPosition,
          s"Failed to derive schema for $tpe. Can only derive Schema for case class or sealed trait or sealed abstract class with case class children."
        )

    def directInferSchema(parentType: Type, schemaType: Type, stack: List[Frame[c.type]]): Tree = {
      stack
        .find(_.tpe =:= schemaType)
        .map {
          case Frame(_, ref, _) =>
            val refIdent = Ident(TermName(ref))
            q"_root_.zio.schema.Schema.defer($refIdent)"
        }
        .getOrElse {
          if (schemaType =:= parentType)
            c.abort(c.enclosingPosition, "Direct recursion is not supported")
          else {
            c.inferImplicitValue(
              c.typecheck(tq"_root_.zio.schema.Schema[$schemaType]", c.TYPEmode).tpe,
              withMacrosDisabled = true
            ) match {
              case EmptyTree =>
                schemaType.typeArgs match {
                  case Nil =>
                    recurse(schemaType, stack)
                  case typeArg1 :: Nil =>
                    if (schemaType <:< optionType)
                      q"_root_.zio.schema.Schema.option(_root_.zio.schema.Schema.defer(${directInferSchema(parentType, concreteType(parentType, typeArg1), stack)}))"
                    else if (schemaType <:< listType)
                      q"_root_.zio.schema.Schema.list(_root_.zio.schema.Schema.defer(${directInferSchema(parentType, concreteType(parentType, typeArg1), stack)}))"
                    else if (schemaType <:< setType)
                      q"_root_.zio.schema.Schema.set(_root_.zio.schema.Schema.defer(${directInferSchema(parentType, concreteType(parentType, typeArg1), stack)}))"
                    else if (schemaType <:< vectorType)
                      q"_root_.zio.schema.Schema.vector(_root_.zio.schema.Schema.defer(${directInferSchema(parentType, concreteType(parentType, typeArg1), stack)}))"
                    else if (schemaType <:< chunkType)
                      q"_root_.zio.schema.Schema.chunk(_root_.zio.schema.Schema.defer(${directInferSchema(parentType, concreteType(parentType, typeArg1), stack)}))"
                    else
                      recurse(schemaType, stack)
                  case typeArg1 :: typeArg2 :: Nil =>
                    if (schemaType <:< eitherType)
                      q"""_root_.zio.schema.Schema.either(
                        _root_.zio.schema.Schema.defer(${directInferSchema(
                        parentType,
                        concreteType(parentType, typeArg1),
                        stack
                      )}),
                        _root_.zio.schema.Schema.defer(${directInferSchema(
                        parentType,
                        concreteType(parentType, typeArg2),
                        stack
                      )})
                      )
                   """
                    else if (schemaType <:< tuple2Type)
                      q"""_root_.zio.schema.Schema.tuple2(
                        _root_.zio.schema.Schema.defer(${directInferSchema(
                        parentType,
                        concreteType(parentType, typeArg1),
                        stack
                      )}),
                        _root_.zio.schema.Schema.defer(${directInferSchema(
                        parentType,
                        concreteType(parentType, typeArg2),
                        stack
                      )})
                      )
                   """
                    else
                      recurse(schemaType, stack)
                  case typeArg1 :: typeArg2 :: typeArg3 :: Nil =>
                    if (schemaType <:< tuple3Type)
                      q"""_root_.zio.schema.Schema.tuple3(
                        _root_.zio.schema.Schema.defer(${directInferSchema(
                        parentType,
                        concreteType(parentType, typeArg1),
                        stack
                      )}),
                        _root_.zio.schema.Schema.defer(${directInferSchema(
                        parentType,
                        concreteType(parentType, typeArg2),
                        stack
                      )}),
                        _root_.zio.schema.Schema.defer(${directInferSchema(
                        parentType,
                        concreteType(parentType, typeArg3),
                        stack
                      )})

                      )
                   """
                    else
                      recurse(schemaType, stack)
                  case typeArg1 :: typeArg2 :: typeArg3 :: typeArg4 :: Nil =>
                    if (schemaType <:< tuple4Type)
                      q"""_root_.zio.schema.Schema.tuple4(
                        _root_.zio.schema.Schema.defer(${directInferSchema(
                        parentType,
                        concreteType(parentType, typeArg1),
                        stack
                      )}),
                        _root_.zio.schema.Schema.defer(${directInferSchema(
                        parentType,
                        concreteType(parentType, typeArg2),
                        stack
                      )}),
                        _root_.zio.schema.Schema.defer(${directInferSchema(
                        parentType,
                        concreteType(parentType, typeArg3),
                        stack
                      )}),
                        _root_.zio.schema.Schema.defer(${directInferSchema(
                        parentType,
                        concreteType(parentType, typeArg4),
                        stack
                      )})
                      )
                   """
                    else
                      recurse(schemaType, stack)
                  case args => c.abort(c.enclosingPosition, s"Unhandled type args $args")
                }
              case tree =>
                q"_root_.zio.schema.Schema.defer($tree)"
            }
          }
        }
    }

    def getFieldName(annotations: List[Tree]): Option[String] =
      annotations.collectFirst {
        case q"new fieldName($name1)" => name1.toString
      }.map(s => s.substring(1, s.length() - 1))

    def deriveRecord(tpe: Type, stack: List[Frame[c.type]]): Tree = stack.find(_.tpe =:= tpe) match {
      case Some(Frame(_, ref, _)) =>
        val refIdent = Ident(TermName(ref))
        q"$refIdent"
      case None =>
        val tpeCompanion = tpe.typeSymbol.companion

        val selfRefName = c.freshName("var")
        val selfRef     = Ident(TermName(selfRefName))

        val currentFrame = Frame[c.type](c, selfRefName, tpe)

        val sortedDecls = tpe.decls.sorted
        val fieldTypes: Iterable[TermSymbol] = sortedDecls.collect {
          case p: TermSymbol if p.isCaseAccessor && !p.isMethod => p
        }
        val arity = fieldTypes.size

        val typeArgs = fieldTypes.map { ft =>
          val fieldType = concreteType(tpe, tpe.decl(ft.name).typeSignature)
          q"$fieldType"
        } ++ Iterable(q"$tpe")

        val fieldAccessors = sortedDecls.collect {
          case p: TermSymbol if p.isCaseAccessor && p.isMethod => p.name
        }

        val typeId = q"_root_.zio.schema.TypeId.parse(${tpe.typeSymbol.fullName})"

        val genericAnnotations: List[Tree] =
          if (tpe.typeArgs.isEmpty) Nil
          else {
            val typeMembers = tpe.typeSymbol.asClass.typeParams.map(_.name.toString)
            val typeArgs = tpe.typeArgs
              .map(_.typeSymbol.fullName)
              .map(t => q"_root_.zio.schema.TypeId.parse(${t}).asInstanceOf[_root_.zio.schema.TypeId.Nominal]")
            val typeMembersWithArgs = typeMembers.zip(typeArgs).map { case (m, a) => q"($m, $a)" }
            List(
              q"new _root_.zio.schema.annotation.genericTypeInfo(_root_.scala.collection.immutable.ListMap(..$typeMembersWithArgs))"
            )
          }

        val typeAnnotations: List[Tree] = collectTypeAnnotations(tpe) ++ genericAnnotations

        val defaultConstructorValues =
          tpe.typeSymbol.asClass.primaryConstructor.asMethod.paramLists.head
            .map(_.asTerm)
            .zipWithIndex
            .flatMap {
              case (symbol, i) =>
                if (symbol.isParamWithDefault) {
                  val defaultInit  = tpe.companion.member(TermName(s"$$lessinit$$greater$$default$$${i + 1}"))
                  val defaultApply = tpe.companion.member(TermName(s"apply$$default$$${i + 1}"))
                  Some(i -> defaultInit)
                    .filter(_ => defaultInit != NoSymbol)
                    .orElse(Some(i -> defaultApply).filter(_ => defaultApply != NoSymbol))
                } else None
            }
            .toMap

        val fieldAnnotations: List[List[Tree]] = //List.fill(arity)(Nil)
          tpe.typeSymbol.asClass.primaryConstructor.asMethod.paramLists.headOption.map { symbols =>
            symbols.zipWithIndex.map {
              case (symbol, i) =>
                val annotations = symbol.annotations.collect {
                  case annotation if !(annotation.tree.tpe <:< JavaAnnotationTpe) =>
                    annotation.tree match {
                      case q"new $annConstructor(..$annotationArgs)" =>
                        q"new ${annConstructor.tpe.typeSymbol}(..$annotationArgs)"
                      case q"new $annConstructor()" =>
                        q"new ${annConstructor.tpe.typeSymbol}()"
                      case tree =>
                        c.warning(c.enclosingPosition, s"Unhandled annotation tree $tree")
                        EmptyTree
                    }
                  case annotation =>
                    c.warning(c.enclosingPosition, s"Unhandled annotation ${annotation.tree}")
                    EmptyTree
                }
                val hasDefaultAnnotation =
                  annotations.exists {
                    case ann if ann.toString.contains("new fieldDefaultValue") => true
                    case _                                                     => false
                  }
                val transientField =
                  annotations.exists {
                    case ann if ann.toString().endsWith("new transientField()") => true
                    case _                                                      => false
                  }
                if (transientField && !(hasDefaultAnnotation || defaultConstructorValues.contains(i))) {
                  throw new IllegalStateException(s"Field ${symbol.name} is transient and must have a default value.")
                }
                if (hasDefaultAnnotation || !defaultConstructorValues.contains(i)) {
                  annotations
                } else {
                  annotations :+
                    q"new _root_.zio.schema.annotation.fieldDefaultValue[${symbol.typeSignature}](${defaultConstructorValues(i)})"

                }

            }.filter(_ != EmptyTree)
          }.getOrElse(Nil)

        val fieldValidations: List[Tree] =
          tpe.typeSymbol.asClass.primaryConstructor.asMethod.paramLists.headOption.map { symbols =>
            symbols.map { symbol =>
              symbol.annotations.collect {
                case annotation if (annotation.tree.tpe.toString.startsWith("zio.schema.annotation.validate")) =>
                  annotation.tree match {
                    case q"new $_(..$annotationArgs)" =>
                      q"..$annotationArgs"
                    case tree =>
                      c.warning(c.enclosingPosition, s"Unhandled annotation tree $tree")
                      EmptyTree
                  }
              }
            }.filter(_ != EmptyTree)
              .map(_.foldLeft[c.universe.Tree](q"_root_.zio.schema.validation.Validation.succeed") {
                case (acc, t) => q"$acc && $t"
              })
          }.getOrElse(Nil)

        if (arity > 22) {
          val fields = fieldTypes.zip(fieldAnnotations).map {
            case (termSymbol, annotations) =>
              val fieldType = concreteType(tpe, termSymbol.typeSignature)
              val fieldSchema = directInferSchema(
                tpe,
                concreteType(tpe, termSymbol.typeSignature),
                currentFrame +: stack
              )
              val fieldLabel = termSymbol.name.toString.trim
              val getFunc =
                q" (z: _root_.scala.collection.immutable.ListMap[String, _]) => z.apply($fieldLabel).asInstanceOf[${termSymbol.typeSignature}]"

              val setFunc =
                q" (z: _root_.scala.collection.immutable.ListMap[String, _], v: ${termSymbol.typeSignature}) => z.updated($fieldLabel, v)"

              if (annotations.nonEmpty) {
                val newName       = getFieldName(annotations).getOrElse(fieldLabel)
                val singletonType = tq"${newName}.type"
                q"_root_.zio.schema.Schema.Field.apply(name0 = $newName, schema0 = $fieldSchema, annotations0 = _root_.zio.Chunk.apply[Any](..$annotations), get0 = $getFunc, set0 = $setFunc).asInstanceOf[_root_.zio.schema.Schema.Field.WithFieldName[scala.collection.immutable.ListMap[String, _], $singletonType, ${fieldType}]]"
              } else {
                val singletonType = tq"${fieldLabel}.type"
                q"_root_.zio.schema.Schema.Field.apply(name0 = $fieldLabel, schema0 = $fieldSchema, get0 = $getFunc, set0 = $setFunc).asInstanceOf[_root_.zio.schema.Schema.Field.WithFieldName[scala.collection.immutable.ListMap[String, _], $singletonType, ${fieldType}]]"
              }
          }
          val fromMap = {
            val casts = fieldTypes.zip(fieldAnnotations).map {
              case (termSymbol, annotations) =>
                val newName = getFieldName(annotations).getOrElse(termSymbol.name.toString.trim)
                q"""
                 try m.apply(${newName}).asInstanceOf[${termSymbol.typeSignature}]
                 catch {
                   case _: ClassCastException => throw new RuntimeException("Field " + ${termSymbol.name.toString.trim} + " has invalid type")
                   case _: Throwable  => throw new RuntimeException("Field " + ${termSymbol.name.toString.trim} + " is missing")
                 }
               """
            }
            q"""(m: scala.collection.immutable.ListMap[String, _]) => try { Right($tpeCompanion.apply(..$casts)) } catch { case e: Throwable => Left(e.getMessage) }"""
          }
          val toMap = {
            val tuples = fieldAccessors.zip(fieldAnnotations).map {
              case (fieldName, annotations) =>
                val newName = getFieldName(annotations).getOrElse(fieldName.toString)
                q"(${newName},b.$fieldName)"
            }
            q"""(b: $tpe) => Right(scala.collection.immutable.ListMap.apply(..$tuples))"""
          }

          val applyArgs =
            if (typeAnnotations.isEmpty) Iterable(q"annotations = _root_.zio.Chunk.empty")
            else Iterable(q"annotations = _root_.zio.Chunk.apply(..$typeAnnotations)")

          q"""_root_.zio.schema.Schema.GenericRecord($typeId, _root_.zio.schema.FieldSet.fromFields(..$fields), ..$applyArgs).transformOrFail[$tpe]($fromMap,$toMap)"""
        } else {
          val schemaType = arity match {
            case 0  => q"_root_.zio.schema.Schema.CaseClass0[..$typeArgs]"
            case 1  => q"_root_.zio.schema.Schema.CaseClass1[..$typeArgs]"
            case 2  => q"_root_.zio.schema.Schema.CaseClass2[..$typeArgs]"
            case 3  => q"_root_.zio.schema.Schema.CaseClass3[..$typeArgs]"
            case 4  => q"_root_.zio.schema.Schema.CaseClass4[..$typeArgs]"
            case 5  => q"_root_.zio.schema.Schema.CaseClass5[..$typeArgs]"
            case 6  => q"_root_.zio.schema.Schema.CaseClass6[..$typeArgs]"
            case 7  => q"_root_.zio.schema.Schema.CaseClass7[..$typeArgs]"
            case 8  => q"_root_.zio.schema.Schema.CaseClass8[..$typeArgs]"
            case 9  => q"_root_.zio.schema.Schema.CaseClass9[..$typeArgs]"
            case 10 => q"_root_.zio.schema.Schema.CaseClass10[..$typeArgs]"
            case 11 => q"_root_.zio.schema.Schema.CaseClass11[..$typeArgs]"
            case 12 => q"_root_.zio.schema.Schema.CaseClass12[..$typeArgs]"
            case 13 => q"_root_.zio.schema.Schema.CaseClass13[..$typeArgs]"
            case 14 => q"_root_.zio.schema.Schema.CaseClass14[..$typeArgs]"
            case 15 => q"_root_.zio.schema.Schema.CaseClass15[..$typeArgs]"
            case 16 => q"_root_.zio.schema.Schema.CaseClass16[..$typeArgs]"
            case 17 => q"_root_.zio.schema.Schema.CaseClass17[..$typeArgs]"
            case 18 => q"_root_.zio.schema.Schema.CaseClass18[..$typeArgs]"
            case 19 => q"_root_.zio.schema.Schema.CaseClass19[..$typeArgs]"
            case 20 => q"_root_.zio.schema.Schema.CaseClass20[..$typeArgs]"
            case 21 => q"_root_.zio.schema.Schema.CaseClass21[..$typeArgs]"
            case 22 => q"_root_.zio.schema.Schema.CaseClass22[..$typeArgs]"
          }

          val fieldDefs = fieldTypes.zip(fieldAnnotations).zip(fieldValidations).zipWithIndex.map {
            case (((termSymbol, annotations), validation), idx) =>
              val fieldType = concreteType(tpe, termSymbol.typeSignature)
              val fieldSchema = directInferSchema(
                tpe,
                fieldType,
                currentFrame +: stack
              )
              val fieldArg   = if (fieldTypes.size > 1) TermName(s"field0${idx + 1}") else TermName("field0")
              val fieldLabel = termSymbol.name.toString.trim
              val getArg     = TermName(fieldLabel)

              val getFunc = q" (z: $tpe) => z.$getArg"
              val setFunc = q" (z: $tpe, v: $fieldType) => z.copy[..${tpe.typeArgs}]($getArg = v)"

              if (annotations.nonEmpty) {
                val newName       = getFieldName(annotations).getOrElse(fieldLabel)
                val singletonType = tq"${newName}.type"
                q"""$fieldArg = _root_.zio.schema.Schema.Field.apply(name0 = $newName, schema0 = $fieldSchema, annotations0 = _root_.zio.Chunk.apply[Any](..$annotations), validation0 = $validation, get0 = $getFunc, set0 = $setFunc).asInstanceOf[_root_.zio.schema.Schema.Field.WithFieldName[$tpe, $singletonType, $fieldType]]"""
              } else {
                val singletonType = tq"${fieldLabel}.type"
                q"""$fieldArg = _root_.zio.schema.Schema.Field.apply(name0 = $fieldLabel, schema0 = $fieldSchema, validation0 = $validation, get0 = $getFunc, set0 = $setFunc).asInstanceOf[_root_.zio.schema.Schema.Field.WithFieldName[$tpe, $singletonType, $fieldType]]"""
              }
          }

          val constructArgs = fieldTypes.zipWithIndex.map {
            case (term, idx) =>
              val arg = TermName(s"_$idx")
              q"$arg: ${term.typeSignature.asSeenFrom(tpe, tpe.typeSymbol.asClass)}"
          }
          val constructApplyArgs = fieldTypes.zipWithIndex.map {
            case (_, idx) =>
              val arg = TermName(s"_$idx")
              q"$arg"
          }

          val constructExpr =
            if (arity < 2)
              q"defaultConstruct0 = (..$constructArgs) => $tpeCompanion[..${tpe.typeArgs}](..$constructApplyArgs)"
            else
              q"construct0 = (..$constructArgs) => $tpeCompanion[..${tpe.typeArgs}](..$constructApplyArgs)"

          val applyArgs =
            if (typeAnnotations.isEmpty)
              Iterable(q"annotations0 = _root_.zio.Chunk.empty") ++ Iterable(q"id0 = ${typeId}") ++ fieldDefs ++ Iterable(
                constructExpr
              )
            else
              Iterable(q"annotations0 = _root_.zio.Chunk.apply(..$typeAnnotations)") ++ Iterable(q"id0 = ${typeId}") ++ fieldDefs ++ Iterable(
                constructExpr
              )

          val typeArgsWithFields = fieldTypes.zip(fieldAnnotations).map {
            case (termSymbol, annotations) if annotations.nonEmpty =>
              tq"${getFieldName(annotations).getOrElse(termSymbol.name.toString.trim)}.type"
            case (termSymbol, _) =>
              tq"${termSymbol.name.toString.trim}.type"
          } ++ typeArgs

          fieldTypes.size match {
            case 0 =>
              q"{lazy val $selfRef: _root_.zio.schema.Schema.CaseClass0[..$typeArgsWithFields] = $schemaType(..$applyArgs); $selfRef}"
            case 1 =>
              q"{lazy val $selfRef: _root_.zio.schema.Schema.CaseClass1.WithFields[..$typeArgsWithFields] = $schemaType(..$applyArgs).asInstanceOf[_root_.zio.schema.Schema.CaseClass1.WithFields[..$typeArgsWithFields]]; $selfRef}"
            case 2 =>
              q"{lazy val $selfRef: _root_.zio.schema.Schema.CaseClass2.WithFields[..$typeArgsWithFields] = $schemaType(..$applyArgs).asInstanceOf[_root_.zio.schema.Schema.CaseClass2.WithFields[..$typeArgsWithFields]]; $selfRef}"
            case 3 =>
              q"{lazy val $selfRef: _root_.zio.schema.Schema.CaseClass3.WithFields[..$typeArgsWithFields] = $schemaType(..$applyArgs).asInstanceOf[_root_.zio.schema.Schema.CaseClass3.WithFields[..$typeArgsWithFields]]; $selfRef}"
            case 4 =>
              q"{lazy val $selfRef: _root_.zio.schema.Schema.CaseClass4.WithFields[..$typeArgsWithFields] = $schemaType(..$applyArgs).asInstanceOf[_root_.zio.schema.Schema.CaseClass4.WithFields[..$typeArgsWithFields]]; $selfRef}"
            case 5 =>
              q"{lazy val $selfRef: _root_.zio.schema.Schema.CaseClass5.WithFields[..$typeArgsWithFields] = $schemaType(..$applyArgs).asInstanceOf[_root_.zio.schema.Schema.CaseClass5.WithFields[..$typeArgsWithFields]]; $selfRef}"
            case 6 =>
              q"{lazy val $selfRef: _root_.zio.schema.Schema.CaseClass6.WithFields[..$typeArgsWithFields] = $schemaType(..$applyArgs).asInstanceOf[_root_.zio.schema.Schema.CaseClass6.WithFields[..$typeArgsWithFields]]; $selfRef}"
            case 7 =>
              q"{lazy val $selfRef: _root_.zio.schema.Schema.CaseClass7.WithFields[..$typeArgsWithFields] = $schemaType(..$applyArgs).asInstanceOf[_root_.zio.schema.Schema.CaseClass7.WithFields[..$typeArgsWithFields]]; $selfRef}"
            case 8 =>
              q"{lazy val $selfRef: _root_.zio.schema.Schema.CaseClass8.WithFields[..$typeArgsWithFields] = $schemaType(..$applyArgs).asInstanceOf[_root_.zio.schema.Schema.CaseClass8.WithFields[..$typeArgsWithFields]]; $selfRef}"
            case 9 =>
              q"{lazy val $selfRef: _root_.zio.schema.Schema.CaseClass9.WithFields[..$typeArgsWithFields] = $schemaType(..$applyArgs).asInstanceOf[_root_.zio.schema.Schema.CaseClass9.WithFields[..$typeArgsWithFields]]; $selfRef}"
            case 10 =>
              q"{lazy val $selfRef: _root_.zio.schema.Schema.CaseClass10.WithFields[..$typeArgsWithFields] = $schemaType(..$applyArgs).asInstanceOf[_root_.zio.schema.Schema.CaseClass10.WithFields[..$typeArgsWithFields]]; $selfRef}"
            case 11 =>
              q"{lazy val $selfRef: _root_.zio.schema.Schema.CaseClass11.WithFields[..$typeArgsWithFields] = $schemaType(..$applyArgs).asInstanceOf[_root_.zio.schema.Schema.CaseClass11.WithFields[..$typeArgsWithFields]]; $selfRef}"
            case 12 =>
              q"{lazy val $selfRef: _root_.zio.schema.Schema.CaseClass12.WithFields[..$typeArgsWithFields] = $schemaType(..$applyArgs).asInstanceOf[_root_.zio.schema.Schema.CaseClass12.WithFields[..$typeArgsWithFields]]; $selfRef}"
            case 13 =>
              q"{lazy val $selfRef: _root_.zio.schema.Schema.CaseClass13.WithFields[..$typeArgsWithFields] = $schemaType(..$applyArgs).asInstanceOf[_root_.zio.schema.Schema.CaseClass13.WithFields[..$typeArgsWithFields]]; $selfRef}"
            case 14 =>
              q"{lazy val $selfRef: _root_.zio.schema.Schema.CaseClass14.WithFields[..$typeArgsWithFields] = $schemaType(..$applyArgs).asInstanceOf[_root_.zio.schema.Schema.CaseClass14.WithFields[..$typeArgsWithFields]]; $selfRef}"
            case 15 =>
              q"{lazy val $selfRef: _root_.zio.schema.Schema.CaseClass15.WithFields[..$typeArgsWithFields] = $schemaType(..$applyArgs).asInstanceOf[_root_.zio.schema.Schema.CaseClass15.WithFields[..$typeArgsWithFields]]; $selfRef}"
            case 16 =>
              q"{lazy val $selfRef: _root_.zio.schema.Schema.CaseClass16.WithFields[..$typeArgsWithFields] = $schemaType(..$applyArgs).asInstanceOf[_root_.zio.schema.Schema.CaseClass16.WithFields[..$typeArgsWithFields]]; $selfRef}"
            case 17 =>
              q"{lazy val $selfRef: _root_.zio.schema.Schema.CaseClass17.WithFields[..$typeArgsWithFields] = $schemaType(..$applyArgs).asInstanceOf[_root_.zio.schema.Schema.CaseClass17.WithFields[..$typeArgsWithFields]]; $selfRef}"
            case 18 =>
              q"{lazy val $selfRef: _root_.zio.schema.Schema.CaseClass18.WithFields[..$typeArgsWithFields] = $schemaType(..$applyArgs).asInstanceOf[_root_.zio.schema.Schema.CaseClass18.WithFields[..$typeArgsWithFields]]; $selfRef}"
            case 19 =>
              q"{lazy val $selfRef: _root_.zio.schema.Schema.CaseClass19.WithFields[..$typeArgsWithFields] = $schemaType(..$applyArgs).asInstanceOf[_root_.zio.schema.Schema.CaseClass19.WithFields[..$typeArgsWithFields]]; $selfRef}"
            case 20 =>
              q"{lazy val $selfRef: _root_.zio.schema.Schema.CaseClass20.WithFields[..$typeArgsWithFields] = $schemaType(..$applyArgs).asInstanceOf[_root_.zio.schema.Schema.CaseClass20.WithFields[..$typeArgsWithFields]]; $selfRef}"
            case 21 =>
              q"{lazy val $selfRef: _root_.zio.schema.Schema.CaseClass21.WithFields[..$typeArgsWithFields] = $schemaType(..$applyArgs).asInstanceOf[_root_.zio.schema.Schema.CaseClass21.WithFields[..$typeArgsWithFields]]; $selfRef}"
            case 22 =>
              q"{lazy val $selfRef: _root_.zio.schema.Schema.CaseClass22.WithFields[..$typeArgsWithFields] = $schemaType(..$applyArgs).asInstanceOf[_root_.zio.schema.Schema.CaseClass22.WithFields[..$typeArgsWithFields]]; $selfRef}"
            case s =>
              c.abort(
                tpe.termSymbol.pos,
                s"Unhandled product arity $s for ${show(tpe)}, ${show(tpeCompanion)}. This should never happen. If you see this error message please report a bug to https://github.com/zio/zio-schema/issues"
              )
          }
        }
    }

    def deriveEnum(tpe: Type, stack: List[Frame[c.type]]): Tree = {
      val typeId = q"_root_.zio.schema.TypeId.parse(${tpe.toString()})"

      val appliedTypeArgs: Map[String, Type] =
        tpe.typeConstructor.typeParams.map(_.name.toString).zip(tpe.typeArgs).toMap

      def appliedSubtype(subtype: Type): Type =
        if (subtype.typeArgs.size == 0) subtype
        else {
          val appliedTypes = subtype.typeConstructor.typeParams.map(_.name.toString).map { typeParam =>
            appliedTypeArgs.get(typeParam) match {
              case None =>
                c.abort(
                  c.enclosingPosition,
                  s"Unable to find applied type param  $typeParam for subtype $subtype of $tpe"
                )
              case Some(applyType) => applyType
            }
          }
          appliedType(subtype, appliedTypes: _*)
        }

      def knownSubclassesOf(parent: ClassSymbol): List[Type] = {

        val sortedKnownSubclasses =
          parent.knownDirectSubclasses.toList.sortBy(subclass => (subclass.pos.line, subclass.pos.column))

        sortedKnownSubclasses
          .filter(s => !s.isAbstract)
          .foreach { child =>
            child.typeSignature
            if (!child.isFinal && !child.asClass.isCaseClass)
              c.abort(c.enclosingPosition, s"child $child of $parent is neither final nor a case class")
          }

        sortedKnownSubclasses.flatMap { child =>
          child.typeSignature
          val childClass = child.asClass
          if (childClass.isSealed && childClass.isTrait)
            knownSubclassesOf(childClass)
          else if (childClass.isCaseClass || (childClass.isClass && childClass.isAbstract)) {
            val st = concreteType(concreteType(tpe, parent.asType.toType), child.asType.toType)
            Set(appliedSubtype(st))
          } else c.abort(c.enclosingPosition, s"child $child of $parent is not a sealed trait or case class")
        }
      }

      val subtypes = knownSubclassesOf(tpe.typeSymbol.asClass)
        .map(concreteType(tpe, _))

      val isSimpleEnum: Boolean =
        !subtypes.map { subtype =>
          subtype.typeSymbol.typeSignature.decls.sorted.collect {
            case p: TermSymbol if p.isCaseAccessor && !p.isMethod => p
          }.size
        }.exists(_ > 0) && subtypes.forall(subtype => subtype.typeSymbol.asClass.isCaseClass)

      val hasSimpleEnum: Boolean =
        tpe.typeSymbol.annotations.exists(_.tree.tpe =:= typeOf[_root_.zio.schema.annotation.simpleEnum])

      val genericAnnotations: List[Tree] =
        if (tpe.typeArgs.isEmpty) Nil
        else {
          val typeMembers = tpe.typeSymbol.asClass.typeParams.map(_.name.toString)
          val typeArgs = tpe.typeArgs
            .map(_.typeSymbol.fullName)
            .map(t => q"_root_.zio.schema.TypeId.parse(${t}).asInstanceOf[_root_.zio.schema.TypeId.Nominal]")
          val typeMembersWithArgs = typeMembers.zip(typeArgs).map { case (m, a) => q"($m, $a)" }
          List(
            q"new _root_.zio.schema.annotation.genericTypeInfo(_root_.scala.collection.immutable.ListMap(..$typeMembersWithArgs))"
          )
        }

      val typeAnnotations: List[Tree] = ((isSimpleEnum, hasSimpleEnum) match {
        case (true, false) =>
          tpe.typeSymbol.annotations.collect {
            case annotation if !(annotation.tree.tpe <:< JavaAnnotationTpe) =>
              annotation.tree match {
                case q"new $annConstructor(..$annotationArgs)" =>
                  q"new ${annConstructor.tpe.typeSymbol}(..$annotationArgs)"
                case q"new $annConstructor()" =>
                  q"new ${annConstructor.tpe.typeSymbol}()"
                case tree =>
                  c.warning(c.enclosingPosition, s"Unhandled annotation tree $tree")
                  EmptyTree
              }
            case annotation =>
              c.warning(c.enclosingPosition, s"Unhandled annotation ${annotation.tree}")
              EmptyTree
          }.filter(_ != EmptyTree).+:(q"new _root_.zio.schema.annotation.simpleEnum(true)")
        case (false, true) =>
          c.abort(c.enclosingPosition, s"${show(tpe)} must be a simple Enum")
        case _ =>
          tpe.typeSymbol.annotations.collect {
            case annotation if !(annotation.tree.tpe <:< JavaAnnotationTpe) =>
              annotation.tree match {
                case q"new $annConstructor(..$annotationArgs)" =>
                  q"new ${annConstructor.tpe.typeSymbol}(..$annotationArgs)"
                case q"new $annConstructor()" =>
                  q"new ${annConstructor.tpe.typeSymbol}()"
                case tree =>
                  c.warning(c.enclosingPosition, s"Unhandled annotation tree $tree")
                  EmptyTree
              }
            case annotation =>
              c.warning(c.enclosingPosition, s"Unhandled annotation ${annotation.tree}")
              EmptyTree
          }.filter(_ != EmptyTree)
      }) ++ genericAnnotations

      val selfRefName  = c.freshName("ref")
      val selfRefIdent = Ident(TermName(selfRefName))

      val currentFrame = Frame[c.type](c, selfRefName, tpe)

      val typeArgs = subtypes ++ Iterable(tpe)

      val cases = subtypes.map { (subtype: Type) =>
        val genericAnnotations: List[Tree] =
          if (subtype.typeArgs.isEmpty) Nil
          else {
            val typeMembers = subtype.typeSymbol.asClass.typeParams.map(_.name.toString)
            val typeArgs = subtype.typeArgs
              .map(_.typeSymbol.fullName)
              .map(t => q"_root_.zio.schema.TypeId.parse(${t}).asInstanceOf[_root_.zio.schema.TypeId.Nominal]")
            val typeMembersWithArgs = typeMembers.zip(typeArgs).map { case (m, a) => q"($m, $a)" }
            List(
              q"new _root_.zio.schema.annotation.genericTypeInfo(_root_.scala.collection.immutable.ListMap(..$typeMembersWithArgs))"
            )
          }

        val typeAnnotations: List[Tree] =
          subtype.typeSymbol.annotations.collect {
            case annotation if !(annotation.tree.tpe <:< JavaAnnotationTpe) =>
              annotation.tree match {
                case q"new $annConstructor(..$annotationArgs)" =>
                  q"new ${annConstructor.tpe.typeSymbol}(..$annotationArgs)"
                case q"new $annConstructor()" =>
                  q"new ${annConstructor.tpe.typeSymbol}()"
                case tree =>
                  c.warning(c.enclosingPosition, s"Unhandled annotation tree $tree")
                  EmptyTree
              }
            case annotation =>
              c.warning(c.enclosingPosition, s"Unhandled annotation ${annotation.tree}")
              EmptyTree
          }.filter(_ != EmptyTree) ++ genericAnnotations

        val caseLabel     = subtype.typeSymbol.name.toString.trim
        val caseSchema    = directInferSchema(tpe, concreteType(tpe, subtype), currentFrame +: stack)
        val deconstructFn = q"(z: $tpe) => z.asInstanceOf[$subtype]"
        val constructFn   = q"(z: $subtype) => z.asInstanceOf[$tpe]"
        val isCaseFn      = q"(z: $tpe) => z.isInstanceOf[$subtype]"
        if (typeAnnotations.isEmpty)
          q"_root_.zio.schema.Schema.Case.apply[$tpe, $subtype]($caseLabel,$caseSchema,$deconstructFn, $constructFn, $isCaseFn)"
        else {
          val annotationArgs = q"zio.Chunk.apply(..$typeAnnotations)"
          q"_root_.zio.schema.Schema.Case.apply[$tpe, $subtype]($caseLabel,$caseSchema,$deconstructFn, $constructFn, $isCaseFn, $annotationArgs)"
        }
      }

      val applyArgs =
        if (typeAnnotations.isEmpty)
          Iterable(q"id = ${typeId}") ++ cases ++ Iterable(q"annotations = zio.Chunk.empty")
        else
          Iterable(q"id = ${typeId}") ++ cases ++ Iterable(q"annotations = zio.Chunk.apply(..$typeAnnotations)")

      cases.size match {
        case 0 => c.abort(c.enclosingPosition, s"No subtypes found for ${show(tpe)}")
        case 1 =>
          q"""{lazy val $selfRefIdent: _root_.zio.schema.Schema.Enum1[..$typeArgs] = _root_.zio.schema.Schema.Enum1[..$typeArgs](..$applyArgs); $selfRefIdent}"""
        case 2 =>
          q"""{lazy val $selfRefIdent: _root_.zio.schema.Schema.Enum2[..$typeArgs] = _root_.zio.schema.Schema.Enum2[..$typeArgs](..$applyArgs); $selfRefIdent}"""
        case 3 =>
          q"""{lazy val $selfRefIdent: _root_.zio.schema.Schema.Enum3[..$typeArgs] = _root_.zio.schema.Schema.Enum3[..$typeArgs](..$applyArgs); $selfRefIdent}"""
        case 4 =>
          q"""{lazy val $selfRefIdent: _root_.zio.schema.Schema.Enum4[..$typeArgs] = _root_.zio.schema.Schema.Enum4[..$typeArgs](..$applyArgs); $selfRefIdent}"""
        case 5 =>
          q"""{lazy val $selfRefIdent: _root_.zio.schema.Schema.Enum5[..$typeArgs] = _root_.zio.schema.Schema.Enum5[..$typeArgs](..$applyArgs); $selfRefIdent}"""
        case 6 =>
          q"""{lazy val $selfRefIdent: _root_.zio.schema.Schema.Enum6[..$typeArgs] = _root_.zio.schema.Schema.Enum6[..$typeArgs](..$applyArgs); $selfRefIdent}"""
        case 7 =>
          q"""{lazy val $selfRefIdent: _root_.zio.schema.Schema.Enum7[..$typeArgs] = _root_.zio.schema.Schema.Enum7[..$typeArgs](..$applyArgs); $selfRefIdent}"""
        case 8 =>
          q"""{lazy val $selfRefIdent: _root_.zio.schema.Schema.Enum8[..$typeArgs] = _root_.zio.schema.Schema.Enum8[..$typeArgs](..$applyArgs); $selfRefIdent}"""
        case 9 =>
          q"""{lazy val $selfRefIdent: _root_.zio.schema.Schema.Enum9[..$typeArgs] = _root_.zio.schema.Schema.Enum9[..$typeArgs](..$applyArgs); $selfRefIdent}"""
        case 10 =>
          q"""{lazy val $selfRefIdent: _root_.zio.schema.Schema.Enum10[..$typeArgs] = _root_.zio.schema.Schema.Enum10[..$typeArgs](..$applyArgs); $selfRefIdent}"""
        case 11 =>
          q"""{lazy val $selfRefIdent: _root_.zio.schema.Schema.Enum11[..$typeArgs] = _root_.zio.schema.Schema.Enum11[..$typeArgs](..$applyArgs); $selfRefIdent}"""
        case 12 =>
          q"""{lazy val $selfRefIdent: _root_.zio.schema.Schema.Enum12[..$typeArgs] = _root_.zio.schema.Schema.Enum12[..$typeArgs](..$applyArgs); $selfRefIdent}"""
        case 13 =>
          q"""{lazy val $selfRefIdent: _root_.zio.schema.Schema.Enum13[..$typeArgs] = _root_.zio.schema.Schema.Enum13[..$typeArgs](..$applyArgs); $selfRefIdent}"""
        case 14 =>
          q"""{lazy val $selfRefIdent: _root_.zio.schema.Schema.Enum14[..$typeArgs] = _root_.zio.schema.Schema.Enum14[..$typeArgs](..$applyArgs); $selfRefIdent}"""
        case 15 =>
          q"""{lazy val $selfRefIdent: _root_.zio.schema.Schema.Enum15[..$typeArgs] = _root_.zio.schema.Schema.Enum15[..$typeArgs](..$applyArgs); $selfRefIdent}"""
        case 16 =>
          q"""{lazy val $selfRefIdent: _root_.zio.schema.Schema.Enum16[..$typeArgs] = _root_.zio.schema.Schema.Enum16[..$typeArgs](..$applyArgs); $selfRefIdent}"""
        case 17 =>
          q"""{lazy val $selfRefIdent: _root_.zio.schema.Schema.Enum17[..$typeArgs] = _root_.zio.schema.Schema.Enum17[..$typeArgs](..$applyArgs); $selfRefIdent}"""
        case 18 =>
          q"""{lazy val $selfRefIdent: _root_.zio.schema.Schema.Enum18[..$typeArgs] = _root_.zio.schema.Schema.Enum18[..$typeArgs](..$applyArgs); $selfRefIdent}"""
        case 19 =>
          q"""{lazy val $selfRefIdent: _root_.zio.schema.Schema.Enum19[..$typeArgs] = _root_.zio.schema.Schema.Enum19[..$typeArgs](..$applyArgs); $selfRefIdent}"""
        case 20 =>
          q"""{lazy val $selfRefIdent: _root_.zio.schema.Schema.Enum20[..$typeArgs] = _root_.zio.schema.Schema.Enum20[..$typeArgs](..$applyArgs); $selfRefIdent}"""
        case 21 =>
          q"""{lazy val $selfRefIdent: _root_.zio.schema.Schema.Enum21[..$typeArgs] = _root_.zio.schema.Schema.Enum21[..$typeArgs](..$applyArgs); $selfRefIdent}"""
        case 22 =>
          q"""{lazy val $selfRefIdent: _root_.zio.schema.Schema.Enum22[..$typeArgs] = _root_.zio.schema.Schema.Enum22[..$typeArgs](..$applyArgs); $selfRefIdent}"""
        case _ =>
          val caseSet = q"_root_.zio.schema.CaseSet.apply(..$cases).asInstanceOf[_root_.zio.schema.CaseSet.Aux[$tpe]]"

          if (typeAnnotations.isEmpty)
            q"""{lazy val $selfRefIdent: _root_.zio.schema.Schema.EnumN[$tpe,_root_.zio.schema.CaseSet.Aux[$tpe]] = _root_.zio.schema.Schema.EnumN.apply[$tpe,_root_.zio.schema.CaseSet.Aux[$tpe]]($typeId, $caseSet); $selfRefIdent}"""
          else {
            val annotationArg = q"zio.Chunk.apply(..$typeAnnotations)"

            q"""{lazy val $selfRefIdent: _root_.zio.schema.Schema.EnumN[$tpe,_root_.zio.schema.CaseSet.Aux[$tpe]] = _root_.zio.schema.Schema.EnumN.apply[$tpe,_root_.zio.schema.CaseSet.Aux[$tpe]]($typeId, $caseSet, $annotationArg); $selfRefIdent}"""
          }
      }
    }

    def deriveMap(tpe: Type): Tree = {
      val selfRefName  = c.freshName("ref")
      val selfRefIdent = Ident(TermName(selfRefName))

      val (keyType, valueType) = tpe match {
        case TypeRef(_, _, List(keyType, valueType)) => (keyType, valueType)
        case _                                       => c.abort(c.enclosingPosition, (s"Invalid type $tpe for @deriveSchema"))
      }

      val keySchema   = q"""_root_.zio.schema.Schema[$keyType]"""
      val valueSchema = q"""_root_.zio.schema.Schema[$valueType]"""

      q"""{lazy val $selfRefIdent: _root_.zio.schema.Schema.Map[$keyType, $valueType] = _root_.zio.schema.Schema.Map.apply[$keyType, $valueType]($keySchema, $valueSchema, zio.Chunk.empty); $selfRefIdent}"""
    }

    recurse(tpe, List.empty[Frame[c.type]])
  }

  case class Frame[C <: whitebox.Context](ctx: C, ref: String, tpe: C#Type)

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy