From bb10cbde171c04ae64f6ca9fb3bdaa7cf9591304 Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Tue, 30 Jan 2024 09:54:35 +0100 Subject: [PATCH] Derive description from scaladoc for Scala 3 (#646) (#652) --- .../scala-3/zio/schema/DeriveSchema.scala | 66 ++++++++++++------- .../VersionSpecificDeriveSchemaSpec.scala | 33 +++++++++- 2 files changed, 71 insertions(+), 28 deletions(-) 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 9c541ddc6..7119d4911 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 @@ -22,7 +22,7 @@ private case class DeriveSchema()(using val ctx: Quotes) { val reflectionUtils = ReflectionUtils(ctx) import reflectionUtils.{MirrorType, Mirror, summonOptional} import ctx.reflect._ - + case class Frame(ref: Term, tpe: TypeRepr) case class Stack(frames: List[Frame]) { def find(tpe: TypeRepr): Option[Term] = frames.find(_.tpe =:= tpe).map(_.ref) @@ -33,7 +33,7 @@ private case class DeriveSchema()(using val ctx: Quotes) { def size = frames.size - override def toString = + override def toString = frames.map(f => s"${f.ref.show} : ${f.tpe.show}").mkString("Stack(", ", ", ")") } @@ -52,7 +52,7 @@ private case class DeriveSchema()(using val ctx: Quotes) { val result = stack.find(typeRepr) match { case Some(ref) => '{ Schema.defer(${ref.asExprOf[Schema[T]]}) } - case None => + case None => val summoned = if (!top) Expr.summon[Schema[T]] else None if (!top && summoned.isDefined) { '{ @@ -121,9 +121,13 @@ private case class DeriveSchema()(using val ctx: Quotes) { val selfRefSymbol = Symbol.newVal(Symbol.spliceOwner, s"derivedSchema${stack.size}", TypeRepr.of[Schema[T]], Flags.Lazy, Symbol.noSymbol) val selfRef = Ref(selfRefSymbol) + val docAnnotationExpr = TypeRepr.of[T].typeSymbol.docstring.map { docstring => + val docstringExpr = Expr(docstring) + '{zio.schema.annotation.description(${docstringExpr})} + } val typeInfo = '{TypeId.parse(${Expr(TypeRepr.of[T].show)})} val annotationExprs = TypeRepr.of[T].typeSymbol.annotations.filter (filterAnnotation).map (_.asExpr) - val annotations = '{zio.Chunk.fromIterable (${Expr.ofSeq (annotationExprs)})} + val annotations = '{zio.Chunk.fromIterable (${Expr.ofSeq (annotationExprs)}) ++ zio.Chunk.fromIterable(${Expr.ofSeq(docAnnotationExpr.toList)}) } val constructor = '{() => ${Ref(TypeRepr.of[T].typeSymbol.companionModule).asExprOf[T]}} val ctor = typeRprOf[T](0).typeSymbol.companionModule @@ -163,8 +167,12 @@ private case class DeriveSchema()(using val ctx: Quotes) { val paramAnns = fromConstructor(TypeRepr.of[T].typeSymbol) val constructor = caseClassConstructor[T](mirror).asExpr + val docAnnotationExpr = TypeRepr.of[T].typeSymbol.docstring.map { docstring => + val docstringExpr = Expr(docstring) + '{zio.schema.annotation.description(${docstringExpr})} + } val annotationExprs = TypeRepr.of[T].typeSymbol.annotations.filter(filterAnnotation).map(_.asExpr) - val annotations = '{ zio.Chunk.fromIterable(${Expr.ofSeq(annotationExprs)}) } + val annotations = '{ zio.Chunk.fromIterable(${Expr.ofSeq(annotationExprs)}) ++ zio.Chunk.fromIterable(${Expr.ofSeq(docAnnotationExpr.toList)}) } val typeInfo = '{TypeId.parse(${Expr(TypeRepr.of[T].show)})} val applied = if (labels.length <= 22) { @@ -182,7 +190,7 @@ private case class DeriveSchema()(using val ctx: Quotes) { ) val fieldsAndFieldTypes = typesAndLabels.map { case (tpe, label) => deriveField[T](tpe, label, paramAnns.getOrElse(label, List.empty), newStack) } - val (fields, fieldTypes) = fieldsAndFieldTypes.unzip + val (fields, fieldTypes) = fieldsAndFieldTypes.unzip val args = List(typeInfo) ++ fields ++ Seq(constructor) ++ Seq(annotations) val terms = Expr.ofTupleFromSeq(args) @@ -271,7 +279,7 @@ private case class DeriveSchema()(using val ctx: Quotes) { } - private def fromDeclarations(from: Symbol): List[(String, List[Expr[Any]])] = + private def fromDeclarations(from: Symbol): List[(String, List[Expr[Any]])] = from.declaredFields.map { field => field.name -> field.annotations.filter(filterAnnotation).map(_.asExpr) @@ -332,12 +340,16 @@ private case class DeriveSchema()(using val ctx: Quotes) { val isSimpleEnum: Boolean = !TypeRepr.of[T].typeSymbol.children.map(_.declaredFields.length).exists( _ > numParentFields ) val hasSimpleEnumAnn: Boolean = TypeRepr.of[T].typeSymbol.hasAnnotation(TypeRepr.of[_root_.zio.schema.annotation.simpleEnum].typeSymbol) + val docAnnotationExpr = TypeRepr.of[T].typeSymbol.docstring.map { docstring => + val docstringExpr = Expr(docstring) + '{zio.schema.annotation.description(${docstringExpr})} + } val annotationExprs = (isSimpleEnum, hasSimpleEnumAnn) match { case (true, false) => TypeRepr.of[T].typeSymbol.annotations.filter(filterAnnotation).map(_.asExpr).+:('{zio.schema.annotation.simpleEnum(true)}) 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)}) } + val annotations = '{ zio.Chunk.fromIterable(${Expr.ofSeq(annotationExprs)}) ++ zio.Chunk.fromIterable(${Expr.ofSeq(docAnnotationExpr.toList)}) } val typeInfo = '{TypeId.parse(${Expr(TypeRepr.of[T].show)})} @@ -346,12 +358,12 @@ private case class DeriveSchema()(using val ctx: Quotes) { val terms = Expr.ofTupleFromSeq(args) val ctor = TypeRepr.of[Enum2[_, _, _]].typeSymbol.primaryConstructor - val typeArgs = + val typeArgs = (types.appended(TypeRepr.of[T])).map { tpe => tpe.asType match case '[tt] => TypeTree.of[tt] } - + val typeTree = enumTypeTree[T](labels.length) Apply( @@ -371,7 +383,7 @@ private case class DeriveSchema()(using val ctx: Quotes) { case '{ type tt <: Schema[T]; $ex : `tt` } => '{ ${Block( - List(lazyValDef), + List(lazyValDef), selfRef ).asExpr}.asInstanceOf[tt] } @@ -416,16 +428,16 @@ private case class DeriveSchema()(using val ctx: Quotes) { if (anns.nonEmpty) { val (newName, newNameValue) = anns.collectFirst { - case ann if ann.isExprOf[fieldName] => + case ann if ann.isExprOf[fieldName] => val fieldNameAnn = ann.asExprOf[fieldName] ('{${fieldNameAnn}.name}, extractFieldNameValue(fieldNameAnn)) }.getOrElse((Expr(name), name)) - + val f = '{ Field($newName, $schema, $chunk, $validator, $get, $set)} addFieldName(newNameValue)(f) // TODO: we need to pass the evaluated annotation value instead of name } else { - val f = '{ Field(${Expr(name)}, $schema, $chunk, $validator, $get, $set) } - addFieldName(name)(f) + val f = '{ Field(${Expr(name)}, $schema, $chunk, $validator, $get, $set) } + addFieldName(name)(f) } } } @@ -484,21 +496,25 @@ private case class DeriveSchema()(using val ctx: Quotes) { val r = TypeRepr.of[R] val t = TypeRepr.of[T] val nameT = ConstantType(StringConstant(name)) - val fieldWithName = withFieldName.appliedTo(List(r, nameT, t)) + val fieldWithName = withFieldName.appliedTo(List(r, nameT, t)) (Select.unique(f.asTerm, "asInstanceOf").appliedToType(fieldWithName).asExprOf[F], nameT) } // sealed case class Case[A, Z](id: String, codec: Schema[A], unsafeDeconstruct: Z => A, annotations: Chunk[Any] = Chunk.empty) { def deriveCase[T: Type](repr: TypeRepr, label: String, stack: Stack)(using Quotes) = { - repr.asType match { case '[t] => + repr.asType match { case '[t] => val schema = deriveSchema[t](stack) val stringExpr = Expr(label) + val docAnnotationExpr = TypeRepr.of[t].typeSymbol.docstring.map { docstring => + val docstringExpr = Expr(docstring) + '{zio.schema.annotation.description(${docstringExpr})} + } val annotationExprs = TypeRepr.of[t].typeSymbol.annotations.filter(filterAnnotation).map(_.asExpr) - val annotations = '{ zio.Chunk.fromIterable(${Expr.ofSeq(annotationExprs)}) } + val annotations = '{ zio.Chunk.fromIterable(${Expr.ofSeq(annotationExprs)}) ++ zio.Chunk.fromIterable(${Expr.ofSeq(docAnnotationExpr.toList)}) } - val unsafeDeconstruct = '{ + val unsafeDeconstruct = '{ (z: T) => z.asInstanceOf[t] } val construct = '{ @@ -525,14 +541,14 @@ private case class DeriveSchema()(using val ctx: Quotes) { a.tpe.typeSymbol.maybeOwner.isNoSymbol || a.tpe.typeSymbol.owner.fullName != "scala.annotation.internal" - def extractFieldNameValue(attribute: Expr[fieldName]): String = + def extractFieldNameValue(attribute: Expr[fieldName]): String = attribute.asTerm match { // Apply(Select(New(Ident(fieldName)),),List(Literal(Constant(renamed)))) case Apply(_, List(Literal(StringConstant(name)))) => name - } + } - def caseClassTypeTree[T: Type](arity: Int): TypeTree = + def caseClassTypeTree[T: Type](arity: Int): TypeTree = arity match { case 0 => TypeTree.of[CaseClass0[T]] case 1 => TypeTree.of[CaseClass1[_, T]] @@ -559,7 +575,7 @@ private case class DeriveSchema()(using val ctx: Quotes) { case 22 => TypeTree.of[CaseClass22[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, T]] } - def typeRprOf[T: Type](arity: Int): TypeRepr = + def typeRprOf[T: Type](arity: Int): TypeRepr = arity match { case 0 => TypeRepr.of[CaseClass0[T]] case 1 => TypeRepr.of[CaseClass1[_, T]] @@ -586,7 +602,7 @@ private case class DeriveSchema()(using val ctx: Quotes) { case 22 => TypeRepr.of[CaseClass22[_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, T]] } - def caseClassWithFieldsType(arity: Int): TypeRepr = + def caseClassWithFieldsType(arity: Int): TypeRepr = arity match { case 1 => TypeRepr.of[CaseClass1.WithFields] case 2 => TypeRepr.of[CaseClass2.WithFields] @@ -612,7 +628,7 @@ private case class DeriveSchema()(using val ctx: Quotes) { case 22 => TypeRepr.of[CaseClass22.WithFields] } - def enumTypeTree[T: Type](arity: Int): TypeTree = + def enumTypeTree[T: Type](arity: Int): TypeTree = arity match { case 0 => TypeTree.of[CaseClass0[T]] case 1 => TypeTree.of[Enum1[_, T]] diff --git a/zio-schema-derivation/shared/src/test/scala-3/zio/schema/VersionSpecificDeriveSchemaSpec.scala b/zio-schema-derivation/shared/src/test/scala-3/zio/schema/VersionSpecificDeriveSchemaSpec.scala index 1c1bce328..80dcdb7c2 100644 --- a/zio-schema-derivation/shared/src/test/scala-3/zio/schema/VersionSpecificDeriveSchemaSpec.scala +++ b/zio-schema-derivation/shared/src/test/scala-3/zio/schema/VersionSpecificDeriveSchemaSpec.scala @@ -2,9 +2,12 @@ package zio.schema import zio.Chunk import zio.test.* -import zio.schema.annotation.simpleEnum +import zio.schema.annotation.* trait VersionSpecificDeriveSchemaSpec extends ZIOSpecDefault { + /** ObjectWithDoc doc */ + object ObjectWithDoc + case class ContainerFields(field1: Option[String]) object ContainerFields { @@ -22,14 +25,26 @@ trait VersionSpecificDeriveSchemaSpec extends ZIOSpecDefault { final case class AutoDerives(i: Int) derives Schema + /** AutoDerives scaladoc */ + final case class AutoDerivesWithDoc(i: Int) derives Schema + enum Colour(val rgb: Int) { case Red extends Colour(0xff0000) case Green extends Colour(0x00ff00) case Blue extends Colour(0x0000ff) } + /** Colour scaladoc */ + @caseName("Red") + enum ColourWithDoc(val rgb: Int) { + /** Red scaladoc */ + case Red extends ColourWithDoc(0xff0000) + case Green extends ColourWithDoc(0x00ff00) + case Blue extends ColourWithDoc(0x0000ff) + } + def versionSpecificSuite = Spec.labeled( - "Scala 3 specific tests", + "Scala 3 specific tests", suite("Derivation")( test("correctly derives case class with `derives` keyword") { val expected: Schema[AutoDerives] = Schema.CaseClass1( @@ -47,7 +62,19 @@ trait VersionSpecificDeriveSchemaSpec extends ZIOSpecDefault { test("correctly assigns simpleEnum to enum") { val derived: Schema[Colour] = DeriveSchema.gen[Colour] assertTrue(derived.annotations == Chunk(simpleEnum(true))) - } + }, + test("correctly adds scaladoc as description"){ + val colourWithDoc: Schema[ColourWithDoc] = DeriveSchema.gen[ColourWithDoc] + val autoDerivesWithDoc: Schema[AutoDerivesWithDoc] = Schema[AutoDerivesWithDoc] + val objectWithDoc: Schema[ObjectWithDoc.type] = DeriveSchema.gen[ObjectWithDoc.type] + val redAnnotations = colourWithDoc.asInstanceOf[Schema.Enum[ColourWithDoc]].cases.find(_.id == "Red").get.schema.annotations.find(_.isInstanceOf[description]) + assertTrue( + colourWithDoc.annotations.find(_.isInstanceOf[description]) == Some(description("/** Colour scaladoc */")), + //redAnnotations == Some(description("/** Red scaladoc */")), fix #651 to make this work + autoDerivesWithDoc.annotations.find(_.isInstanceOf[description]) == Some(description("/** AutoDerives scaladoc */")), + objectWithDoc.annotations.find(_.isInstanceOf[description]) == Some(description("/** ObjectWithDoc doc */")), + ) + }, ) ) }