Skip to content

Commit

Permalink
Merge branch 'main' into consider-transform-annotations-for-inner-schema
Browse files Browse the repository at this point in the history
  • Loading branch information
vigoo authored Sep 6, 2023
2 parents 3f39a5f + 1a9ab2b commit 3ce84b4
Show file tree
Hide file tree
Showing 11 changed files with 249 additions and 59 deletions.
4 changes: 2 additions & 2 deletions tests/shared/src/test/scala-2/zio/schema/MetaSchemaSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -501,23 +531,53 @@ object DeriveSchema {
}
}

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

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

@nowarn
val typeAnnotations: 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)
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)
}

val selfRefName = c.freshName("ref")
val selfRefIdent = Ident(TermName(selfRefName))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -293,7 +324,14 @@ private case class DeriveSchema()(using val ctx: Quotes) extends ReflectionUtils

val cases = typesAndLabels.map { case (tpe, label) => deriveCase[T](tpe, label, newStack) }

val annotationExprs = TypeRepr.of[T].typeSymbol.annotations.filter(filterAnnotation).map(_.asExpr)
val isSimpleEnum: Boolean = !TypeRepr.of[T].typeSymbol.children.map(_.declaredFields.length).exists( _ > 0 )
val hasSimpleEnumAnn: Boolean = TypeRepr.of[T].typeSymbol.hasAnnotation(TypeRepr.of[_root_.zio.schema.annotation.simpleEnum].typeSymbol)

val annotationExprs = (isSimpleEnum, hasSimpleEnumAnn) match {
case (true, false) => TypeRepr.of[T].typeSymbol.annotations.filter(filterAnnotation).map(_.asExpr).+:('{_root_.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 typeInfo = '{TypeId.parse(${Expr(TypeRepr.of[T].show)})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package zio.schema
import scala.annotation.Annotation

import zio.Chunk
import zio.schema.annotation.{ fieldName, optionalField }
import zio.schema.annotation.{ fieldName, optionalField, simpleEnum }
import zio.test._

object DeriveSchemaSpec extends ZIOSpecDefault with VersionSpecificDeriveSchemaSpec {
Expand Down Expand Up @@ -243,6 +243,13 @@ object DeriveSchemaSpec extends ZIOSpecDefault with VersionSpecificDeriveSchemaS
implicit val schema: Schema[OptionalField] = DeriveSchema.gen[OptionalField]
}

@simpleEnum
sealed trait SimpleEnum1
case class SimpleClass1() extends SimpleEnum1

sealed trait SimpleEnum2
case class SimpleClass2() extends SimpleEnum2

override def spec: Spec[Environment, Any] = suite("DeriveSchemaSpec")(
suite("Derivation")(
test("correctly derives case class 0") {
Expand Down Expand Up @@ -449,6 +456,14 @@ object DeriveSchemaSpec extends ZIOSpecDefault with VersionSpecificDeriveSchemaS
)
}
assert(derived)(hasSameSchema(expected))
},
test("correctly derives simpleEnum with annotation") {
val derived = DeriveSchema.gen[SimpleEnum1]
assertTrue(derived.annotations == Chunk(simpleEnum(false)))
},
test("correctly derives simpleEnum without annotation") {
val derived = DeriveSchema.gen[SimpleEnum2]
assertTrue(derived.annotations == Chunk(simpleEnum(true)))
}
),
versionSpecificSuite
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down Expand Up @@ -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
)

Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ object JsonCodec {

private def enumEncoder[Z](parentSchema: Schema.Enum[Z], cases: Schema.Case[Z, _]*): ZJsonEncoder[Z] =
// if all cases are CaseClass0, encode as a String
if (cases.forall(_.schema.isInstanceOf[Schema.CaseClass0[_]])) {
if (parentSchema.annotations.exists(_.isInstanceOf[simpleEnum])) {
val caseMap: Map[Z, String] = cases
.filterNot(_.annotations.exists(_.isInstanceOf[transientCase]))
.map(
Expand Down
4 changes: 2 additions & 2 deletions zio-schema/shared/src/main/scala/zio/schema/Schema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@ object Schema extends SchemaEquality {

def defer[A](schema: => Schema[A]): Schema[A] = Lazy(() => schema)

def enumeration[A, C <: CaseSet.Aux[A]](id: TypeId, caseSet: C): Schema[A] =
EnumN(id, caseSet, Chunk.empty)
def enumeration[A, C <: CaseSet.Aux[A]](id: TypeId, caseSet: C, annotations: Chunk[Any] = Chunk.empty): Schema[A] =
EnumN(id, caseSet, annotations)

def fail[A](message: String): Schema[A] = Fail(message)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
package zio.schema.annotation

final case class recordName(name: String) extends scala.annotation.StaticAnnotation
import scala.annotation.StaticAnnotation

final case class recordName(name: String) extends StaticAnnotation
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package zio.schema.annotation

import scala.annotation.StaticAnnotation

/*
* Automatically added in sealed traits with only case objects or case class without parameters.
* Gives error if it used in other types of enumerations.
*/

final case class simpleEnum(automaticallyAdded: Boolean = false) extends StaticAnnotation
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,8 @@ object DecodeError {
def message: String = s"Value $values and $structure have incompatible shape"
}

final case class UnsupportedSchema(schema: Schema[_], decoderName: String) extends DecodeError {
def message: String = s"Schema $schema is not supported by $decoderName"
}

}
Loading

0 comments on commit 3ce84b4

Please sign in to comment.