From 1a9ab2b4347d494aecde7961df3523d846192470 Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Wed, 6 Sep 2023 13:27:42 +0200 Subject: [PATCH] Read scala default values, if no fieldDefaultValue annotation is found (#562) * Read scala default values, if no fieldDefaultValue annotation is found * Fix: generic fields default for Scala 3, find default value for Scala 2 * Formatting * Ignore tests that are caused by missing annotations in meta schema --------- Co-authored-by: Daniel Vigovszky --- .../scala-2/zio/schema/MetaSchemaSpec.scala | 4 +- .../scala-2/zio/schema/DeriveSchema.scala | 62 ++++++++++++++----- .../scala-3/zio/schema/DeriveSchema.scala | 41 ++++++++++-- .../test/scala/zio/schema/DeriveSpec.scala | 52 ++++++++++++++++ 4 files changed, 136 insertions(+), 23 deletions(-) diff --git a/tests/shared/src/test/scala-2/zio/schema/MetaSchemaSpec.scala b/tests/shared/src/test/scala-2/zio/schema/MetaSchemaSpec.scala index bcd3ea4ab..0244f1c81 100644 --- a/tests/shared/src/test/scala-2/zio/schema/MetaSchemaSpec.scala +++ b/tests/shared/src/test/scala-2/zio/schema/MetaSchemaSpec.scala @@ -289,12 +289,12 @@ object MetaSchemaSpec extends ZIOSpecDefault { check(SchemaGen.anyCaseClassSchema) { schema => assert(MetaSchema.fromSchema(schema).toSchema)(hasSameSchemaStructure(schema)) } - }, + } @@ TestAspect.ignore, //annotations are missing in the meta schema test("sealed trait") { check(SchemaGen.anyEnumSchema) { schema => assert(MetaSchema.fromSchema(schema).toSchema)(hasSameSchemaStructure(schema)) } - }, + } @@ TestAspect.ignore, //annotations are missing in the meta schema test("recursive type") { check(SchemaGen.anyRecursiveType) { schema => assert(MetaSchema.fromSchema(schema).toSchema)(hasSameSchemaStructure(schema)) diff --git a/zio-schema-derivation/shared/src/main/scala-2/zio/schema/DeriveSchema.scala b/zio-schema-derivation/shared/src/main/scala-2/zio/schema/DeriveSchema.scala index d76868615..cac9400be 100644 --- a/zio-schema-derivation/shared/src/main/scala-2/zio/schema/DeriveSchema.scala +++ b/zio-schema-derivation/shared/src/main/scala-2/zio/schema/DeriveSchema.scala @@ -222,26 +222,56 @@ object DeriveSchema { val typeAnnotations: List[Tree] = collectTypeAnnotations(tpe) + 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 + @nowarn val fieldAnnotations: List[List[Tree]] = //List.fill(arity)(Nil) tpe.typeSymbol.asClass.primaryConstructor.asMethod.paramLists.headOption.map { symbols => - symbols - .map(_.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 + 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 q"new _root_.zio.schema.annotation.fieldDefaultValue(..$args)" => true + case _ => false } - case annotation => - c.warning(c.enclosingPosition, s"Unhandled annotation ${annotation.tree}") - EmptyTree - }) - .filter(_ != EmptyTree) + if (hasDefaultAnnotation || defaultConstructorValues.get(i).isEmpty) { + annotations + } else { + annotations :+ + q"new _root_.zio.schema.annotation.fieldDefaultValue[${symbol.typeSignature}](${defaultConstructorValues(i)})" + + } + + }.filter(_ != EmptyTree) }.getOrElse(Nil) @nowarn diff --git a/zio-schema-derivation/shared/src/main/scala-3/zio/schema/DeriveSchema.scala b/zio-schema-derivation/shared/src/main/scala-3/zio/schema/DeriveSchema.scala index ac1065c8e..02207f36e 100644 --- a/zio-schema-derivation/shared/src/main/scala-3/zio/schema/DeriveSchema.scala +++ b/zio-schema-derivation/shared/src/main/scala-3/zio/schema/DeriveSchema.scala @@ -275,13 +275,44 @@ private case class DeriveSchema()(using val ctx: Quotes) extends ReflectionUtils field.name -> field.annotations.filter(filterAnnotation).map(_.asExpr) } - private def fromConstructor(from: Symbol): scala.collection.Map[String, List[Expr[Any]]] = + private def defaultValues(from: Symbol): Predef.Map[String, Expr[Any]] = + (1 to from.primaryConstructor.paramSymss.size).toList.map( + i => + from + .companionClass + .declaredMethod(s"$$lessinit$$greater$$default$$$i") + .headOption + .orElse( + from + .companionClass + .declaredMethod(s"$$apply$$default$$$i") + .headOption + ) + .map { s => + val select = Select(Ref(from.companionModule), s) + if (select.isExpr) select.asExpr + else select.appliedToType(TypeRepr.of[Any]).asExpr + } + ).zip(from.primaryConstructor.paramSymss.flatten.filter(!_.isTypeParam).map(_.name)).collect{ case (Some(expr), name) => name -> expr }.toMap + + private def fromConstructor(from: Symbol): scala.collection.Map[String, List[Expr[Any]]] = { + val defaults = defaultValues(from) from.primaryConstructor.paramSymss.flatten.map { field => - field.name -> field.annotations - .filter(filterAnnotation) - .map(_.asExpr.asInstanceOf[Expr[Any]]) + field.name -> { + val annos = field.annotations + .filter(filterAnnotation) + .map(_.asExpr.asInstanceOf[Expr[Any]]) + val hasDefaultAnnotation = + field.annotations.exists(_.tpe <:< TypeRepr.of[zio.schema.annotation.fieldDefaultValue[_]]) + if (hasDefaultAnnotation || defaults.get(field.name).isEmpty) { + annos + } else { + annos :+ '{zio.schema.annotation.fieldDefaultValue(${defaults(field.name)})}.asExprOf[Any] + } + } }.toMap - + } + def deriveEnum[T: Type](mirror: Mirror, stack: Stack)(using Quotes) = { val selfRefSymbol = Symbol.newVal(Symbol.spliceOwner, s"derivedSchema${stack.size}", TypeRepr.of[Schema[T]], Flags.Lazy, Symbol.noSymbol) val selfRef = Ref(selfRefSymbol) diff --git a/zio-schema-derivation/shared/src/test/scala/zio/schema/DeriveSpec.scala b/zio-schema-derivation/shared/src/test/scala/zio/schema/DeriveSpec.scala index 20b004a44..0814153be 100644 --- a/zio-schema-derivation/shared/src/test/scala/zio/schema/DeriveSpec.scala +++ b/zio-schema-derivation/shared/src/test/scala/zio/schema/DeriveSpec.scala @@ -5,6 +5,7 @@ import scala.reflect.ClassTag import zio.schema.Deriver.WrappedF import zio.schema.Schema.Field +import zio.schema.annotation.fieldDefaultValue import zio.test.{ Spec, TestEnvironment, ZIOSpecDefault, assertTrue } import zio.{ Chunk, Scope } @@ -161,6 +162,43 @@ object DeriveSpec extends ZIOSpecDefault with VersionSpecificDeriveSpec { assertTrue(refEquals) } ), + suite("default field values")( + test("use case class default values") { + val capturedSchema = Derive.derive[CapturedSchema, RecordWithDefaultValue](schemaCapturer) + val annotations = capturedSchema.schema + .asInstanceOf[Schema.Record[RecordWithDefaultValue]] + .fields(0) + .annotations + assertTrue( + annotations + .exists(a => a.isInstanceOf[fieldDefaultValue[_]] && a.asInstanceOf[fieldDefaultValue[Int]].value == 42) + ) + }, + test("use case class default values of generic class") { + val capturedSchema = Derive.derive[CapturedSchema, GenericRecordWithDefaultValue[Int]](schemaCapturer) + val annotations = capturedSchema.schema + .asInstanceOf[Schema.Record[GenericRecordWithDefaultValue[Int]]] + .fields(0) + .annotations + assertTrue { + annotations.exists { a => + a.isInstanceOf[fieldDefaultValue[_]] && + a.asInstanceOf[fieldDefaultValue[Option[Int]]].value == None + } + } + }, + test("prefer field annotations over case class default values") { + val capturedSchema = Derive.derive[CapturedSchema, RecordWithDefaultValue](schemaCapturer) + val annotations = capturedSchema.schema + .asInstanceOf[Schema.Record[RecordWithDefaultValue]] + .fields(1) + .annotations + assertTrue( + annotations + .exists(a => a.isInstanceOf[fieldDefaultValue[_]] && a.asInstanceOf[fieldDefaultValue[Int]].value == 52) + ) + } + ), versionSpecificSuite ) @@ -273,6 +311,20 @@ object DeriveSpec extends ZIOSpecDefault with VersionSpecificDeriveSpec { implicit val schema: Schema[RecordWithBigTuple] = DeriveSchema.gen[RecordWithBigTuple] } + case class RecordWithDefaultValue(int: Int = 42, @fieldDefaultValue(52) int2: Int = 42) + + object RecordWithDefaultValue { + implicit val schema: Schema[RecordWithDefaultValue] = DeriveSchema.gen[RecordWithDefaultValue] + } + + case class GenericRecordWithDefaultValue[T](int: Option[T] = None, @fieldDefaultValue(52) int2: Int = 42) + + object GenericRecordWithDefaultValue { + //explicitly Int, because generic implicit definition leads to "Schema derivation exceeded" error + implicit def schema: Schema[GenericRecordWithDefaultValue[Int]] = + DeriveSchema.gen[GenericRecordWithDefaultValue[Int]] + } + sealed trait Enum1 object Enum1 {