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 018079141..c171cf5b1 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 @@ -232,7 +232,20 @@ object DeriveSchema { val typeId = q"_root_.zio.schema.TypeId.parse(${tpe.typeSymbol.fullName})" - val typeAnnotations: List[Tree] = collectTypeAnnotations(tpe) + 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 @@ -557,7 +570,20 @@ object DeriveSchema { val hasSimpleEnum: Boolean = tpe.typeSymbol.annotations.exists(_.tree.tpe =:= typeOf[_root_.zio.schema.annotation.simpleEnum]) - val typeAnnotations: List[Tree] = (isSimpleEnum, hasSimpleEnum) match { + 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) => @@ -592,7 +618,7 @@ object DeriveSchema { c.warning(c.enclosingPosition, s"Unhandled annotation ${annotation.tree}") EmptyTree }.filter(_ != EmptyTree) - } + }) ++ genericAnnotations val selfRefName = c.freshName("ref") val selfRefIdent = Ident(TermName(selfRefName)) @@ -602,6 +628,19 @@ object DeriveSchema { 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) => @@ -617,7 +656,7 @@ object DeriveSchema { case annotation => c.warning(c.enclosingPosition, s"Unhandled annotation ${annotation.tree}") EmptyTree - }.filter(_ != EmptyTree) + }.filter(_ != EmptyTree) ++ genericAnnotations val caseLabel = subtype.typeSymbol.name.toString.trim val caseSchema = directInferSchema(tpe, concreteType(tpe, subtype), currentFrame +: stack) 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 57df11ebc..235c07bb6 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 @@ -195,7 +195,12 @@ private case class DeriveSchema()(using val ctx: Quotes) { then TypeRepr.of[T].typeSymbol.children.map(_.annotations).flatten.filter (filterAnnotation).map (_.asExpr) else TypeRepr.of[T].typeSymbol.annotations.filter (filterAnnotation).map (_.asExpr) - val annotations = '{ zio.Chunk.fromIterable(${Expr.ofSeq(annotationExprs)}) ++ zio.Chunk.fromIterable(${Expr.ofSeq(docAnnotationExpr)}) } + val genericAnnotations = if (TypeRepr.of[T].classSymbol.exists(_.typeMembers.nonEmpty)){ + val typeMembersExpr = Expr.ofSeq(TypeRepr.of[T].classSymbol.get.typeMembers.map { t => Expr(t.name) }) + val typeArgsExpr = Expr.ofSeq(TypeRepr.of[T].typeArgs.map { t => Expr(t.typeSymbol.fullName) }) + List('{zio.schema.annotation.genericTypeInfo(ListMap.from(${typeMembersExpr}.zip(${typeArgsExpr}.map(name => TypeId.parse(name).asInstanceOf[TypeId.Nominal]))))}) + } else List.empty + val annotations = '{ zio.Chunk.fromIterable(${Expr.ofSeq(annotationExprs)}) ++ zio.Chunk.fromIterable(${Expr.ofSeq(docAnnotationExpr)}) ++ zio.Chunk.fromIterable(${Expr.ofSeq(genericAnnotations)}) } val typeInfo = '{TypeId.parse(${Expr(TypeRepr.of[T].show)})} val applied = if (labels.length <= 22) { @@ -372,7 +377,12 @@ private case class DeriveSchema()(using val ctx: Quotes) { case (false, true) => throw new Exception(s"${TypeRepr.of[T].typeSymbol.name} must be a simple Enum") case _ => TypeRepr.of[T].typeSymbol.annotations.filter(filterAnnotation).map(_.asExpr) } - val annotations = '{ zio.Chunk.fromIterable(${Expr.ofSeq(annotationExprs)}) ++ zio.Chunk.fromIterable(${Expr.ofSeq(docAnnotationExpr.toList)}) } + val genericAnnotations = if (TypeRepr.of[T].classSymbol.exists(_.typeMembers.nonEmpty)){ + val typeMembersExpr = Expr.ofSeq(TypeRepr.of[T].classSymbol.get.typeMembers.map { t => Expr(t.name) }) + val typeArgsExpr = Expr.ofSeq(TypeRepr.of[T].typeArgs.map { t => Expr(t.typeSymbol.fullName) }) + List('{zio.schema.annotation.genericTypeInfo(ListMap.from(${typeMembersExpr}.zip(${typeArgsExpr}.map(name => TypeId.parse(name).asInstanceOf[TypeId.Nominal]))))}) + } else List.empty + val annotations = '{ zio.Chunk.fromIterable(${Expr.ofSeq(annotationExprs)}) ++ zio.Chunk.fromIterable(${Expr.ofSeq(docAnnotationExpr.toList)}) ++ zio.Chunk.fromIterable(${Expr.ofSeq(genericAnnotations)}) } val typeInfo = '{TypeId.parse(${Expr(TypeRepr.of[T].show)})} 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 e82bac1f3..51a222e08 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 @@ -1,11 +1,12 @@ package zio.schema import scala.annotation.nowarn +import scala.collection.immutable.ListMap import scala.reflect.ClassTag import zio.schema.Deriver.WrappedF import zio.schema.Schema.Field -import zio.schema.annotation.fieldDefaultValue +import zio.schema.annotation.{ fieldDefaultValue, genericTypeInfo } import zio.test.{ Spec, TestEnvironment, ZIOSpecDefault, assertTrue } import zio.{ Chunk, Scope } @@ -183,7 +184,11 @@ import zio.{ Chunk, Scope } .asInstanceOf[Schema.Record[GenericRecordWithDefaultValue[Int]]] .fields(0) .annotations + val maybeTypeInfo = capturedSchema.schema.annotations.collectFirst { case gt @ genericTypeInfo(_) => gt } assertTrue( + maybeTypeInfo.contains( + genericTypeInfo(ListMap("T" -> TypeId.parse("scala.Int").asInstanceOf[TypeId.Nominal])) + ), annotations.exists { a => a.isInstanceOf[fieldDefaultValue[_]] && a.asInstanceOf[fieldDefaultValue[Option[Int]]].value == None diff --git a/zio-schema-json/shared/src/test/scala-3/zio/schema/codec/DefaultValueSpec.scala b/zio-schema-json/shared/src/test/scala-3/zio/schema/codec/DefaultValueSpec.scala index cd65b6a00..4b2dd94bc 100644 --- a/zio-schema-json/shared/src/test/scala-3/zio/schema/codec/DefaultValueSpec.scala +++ b/zio-schema-json/shared/src/test/scala-3/zio/schema/codec/DefaultValueSpec.scala @@ -12,7 +12,7 @@ object DefaultValueSpec extends ZIOSpecDefault { def spec: Spec[TestEnvironment, Any] = suite("Custom Spec")( - customSuite, + customSuite ) @@ timeout(90.seconds) private val customSuite = suite("custom")( @@ -24,7 +24,7 @@ object DefaultValueSpec extends ZIOSpecDefault { ) ) - case class WithDefaultValue(orderId:Int, description: String = "desc") + case class WithDefaultValue(orderId: Int, description: String = "desc") object WithDefaultValue { implicit lazy val schema: Schema[WithDefaultValue] = DeriveSchema.gen[WithDefaultValue] diff --git a/zio-schema/shared/src/main/scala/zio/schema/annotation/genericTypeInfo.scala b/zio-schema/shared/src/main/scala/zio/schema/annotation/genericTypeInfo.scala new file mode 100644 index 000000000..0c4a32505 --- /dev/null +++ b/zio-schema/shared/src/main/scala/zio/schema/annotation/genericTypeInfo.scala @@ -0,0 +1,8 @@ +package zio.schema.annotation + +import scala.collection.immutable.ListMap + +import zio.schema.TypeId + +final case class genericTypeInfo(appliedTypes: ListMap[String, TypeId.Nominal]) + extends scala.annotation.StaticAnnotation