diff --git a/build.sbt b/build.sbt index 9fab85b5..eb440363 100644 --- a/build.sbt +++ b/build.sbt @@ -79,9 +79,14 @@ lazy val enumeratum = project ), scalatestSettings, publishSettings, - mimaSettings(), - scalaSettings, - testSettings + mimaSettings(excludeScala3 = true), + scalaSettings ++ Seq( + crossScalaVersions += scala3 + ), + testSettings, + Test / scalacOptions ++= { + if (scalaVersion.value.startsWith("3")) List("-Yretain-trees") else Nil + } ) .dependsOn(core, generic) diff --git a/modules/enumeratum/src/main/scala/enumeratum/Vulcan.scala b/modules/enumeratum/src/main/scala-2/enumeratum/Vulcan.scala similarity index 100% rename from modules/enumeratum/src/main/scala/enumeratum/Vulcan.scala rename to modules/enumeratum/src/main/scala-2/enumeratum/Vulcan.scala diff --git a/modules/enumeratum/src/main/scala/enumeratum/VulcanEnum.scala b/modules/enumeratum/src/main/scala-2/enumeratum/VulcanEnum.scala similarity index 100% rename from modules/enumeratum/src/main/scala/enumeratum/VulcanEnum.scala rename to modules/enumeratum/src/main/scala-2/enumeratum/VulcanEnum.scala diff --git a/modules/enumeratum/src/main/scala/enumeratum/values/Vulcan.scala b/modules/enumeratum/src/main/scala-2/enumeratum/values/Vulcan.scala similarity index 100% rename from modules/enumeratum/src/main/scala/enumeratum/values/Vulcan.scala rename to modules/enumeratum/src/main/scala-2/enumeratum/values/Vulcan.scala diff --git a/modules/enumeratum/src/main/scala/enumeratum/values/VulcanValueEnum.scala b/modules/enumeratum/src/main/scala-2/enumeratum/values/VulcanValueEnum.scala similarity index 100% rename from modules/enumeratum/src/main/scala/enumeratum/values/VulcanValueEnum.scala rename to modules/enumeratum/src/main/scala-2/enumeratum/values/VulcanValueEnum.scala diff --git a/modules/enumeratum/src/main/scala-3/enumeratum/Vulcan.scala b/modules/enumeratum/src/main/scala-3/enumeratum/Vulcan.scala new file mode 100644 index 00000000..fb5f537b --- /dev/null +++ b/modules/enumeratum/src/main/scala-3/enumeratum/Vulcan.scala @@ -0,0 +1,27 @@ +/* + * Copyright 2019 OVO Energy Limited + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package enumeratum + +import vulcan.{Avro, AvroError, Codec} +import vulcan.internal.Names +import vulcan.generic.deriveEnum + +object Vulcan { + def enumCodec[A <: EnumEntry]( + `enum`: Enum[A] + )(implicit names: Names[A]): Codec.Aux[Avro.EnumSymbol, A] = { + lazy val entries = `enum`.values.map(_.entryName).mkString(", ") + val notFound = (name: String) => + AvroError(s"$name is not a member of ${names.typeName} ($entries)") + + deriveEnum( + symbols = `enum`.values.map(_.entryName), + encode = _.entryName, + decode = name => `enum`.withNameOption(name).toRight(notFound(name)) + ) + } +} diff --git a/modules/enumeratum/src/main/scala-3/enumeratum/VulcanEnum.scala b/modules/enumeratum/src/main/scala-3/enumeratum/VulcanEnum.scala new file mode 100644 index 00000000..cffcbefd --- /dev/null +++ b/modules/enumeratum/src/main/scala-3/enumeratum/VulcanEnum.scala @@ -0,0 +1,14 @@ +/* + * Copyright 2019 OVO Energy Limited + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package enumeratum + +import vulcan.Codec +import vulcan.internal.Names + +trait VulcanEnum[A <: EnumEntry] { this: Enum[A] => + given (using names: Names[A]): Codec[A] = Vulcan.enumCodec(this) +} diff --git a/modules/enumeratum/src/main/scala-3/enumeratum/values/Vulcan.scala b/modules/enumeratum/src/main/scala-3/enumeratum/values/Vulcan.scala new file mode 100644 index 00000000..dc568983 --- /dev/null +++ b/modules/enumeratum/src/main/scala-3/enumeratum/values/Vulcan.scala @@ -0,0 +1,40 @@ +/* + * Copyright 2019 OVO Energy Limited + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package enumeratum.values + +import vulcan.{Avro, AvroError, Codec} +import vulcan.internal.Names +import vulcan.generic.deriveEnum + +object Vulcan { + def codec[ValueType, EntryType <: ValueEnumEntry[ValueType]]( + `enum`: ValueEnum[ValueType, EntryType] + )(implicit + codec: Codec[ValueType], + names: Names[EntryType] + ): Codec.Aux[codec.AvroType, EntryType] = { + lazy val entries = `enum`.values.map(_.value).mkString(", ") + val notFound = (value: ValueType) => + AvroError(s"$value is not a member of ${names.typeName} ($entries)") + + codec.imapError(value => `enum`.withValueOpt(value).toRight(notFound(value)))(_.value) + } + + def enumCodec[EntryType <: ValueEnumEntry[String]]( + `enum`: ValueEnum[String, EntryType] + )(implicit names: Names[EntryType]): Codec.Aux[Avro.EnumSymbol, EntryType] = { + lazy val entries = `enum`.values.map(_.value).mkString(", ") + val notFound = (value: String) => + AvroError(s"$value is not a member of ${names.typeName} ($entries)") + + deriveEnum( + symbols = `enum`.values.map(_.value), + encode = _.value, + decode = value => `enum`.withValueOpt(value).toRight(notFound(value)) + ) + } +} diff --git a/modules/enumeratum/src/main/scala-3/enumeratum/values/VulcanValueEnum.scala b/modules/enumeratum/src/main/scala-3/enumeratum/values/VulcanValueEnum.scala new file mode 100644 index 00000000..0c15586e --- /dev/null +++ b/modules/enumeratum/src/main/scala-3/enumeratum/values/VulcanValueEnum.scala @@ -0,0 +1,58 @@ +/* + * Copyright 2019 OVO Energy Limited + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package enumeratum.values + +import vulcan.{Avro, Codec} +import vulcan.internal.Names + +sealed trait VulcanValueEnum[ValueType, EntryType <: ValueEnumEntry[ValueType]] { + this: ValueEnum[ValueType, EntryType] => + + given vulcanCodec(using names: Names[EntryType]): Codec[EntryType] +} + +trait ByteVulcanEnum[EntryType <: ByteEnumEntry] extends VulcanValueEnum[Byte, EntryType] { + this: ValueEnum[Byte, EntryType] => + + given vulcanCodec(using names: Names[EntryType]): Codec.Aux[Avro.Int, EntryType] = + Vulcan.codec(this) +} + +trait CharVulcanEnum[EntryType <: CharEnumEntry] extends VulcanValueEnum[Char, EntryType] { + this: ValueEnum[Char, EntryType] => + + given vulcanCodec(using names: Names[EntryType]): Codec.Aux[Avro.String, EntryType] = + Vulcan.codec(this) +} + +trait IntVulcanEnum[EntryType <: IntEnumEntry] extends VulcanValueEnum[Int, EntryType] { + this: ValueEnum[Int, EntryType] => + + given vulcanCodec(using names: Names[EntryType]): Codec.Aux[Avro.Int, EntryType] = + Vulcan.codec(this) +} + +trait LongVulcanEnum[EntryType <: LongEnumEntry] extends VulcanValueEnum[Long, EntryType] { + this: ValueEnum[Long, EntryType] => + + given vulcanCodec(using names: Names[EntryType]): Codec.Aux[Avro.Long, EntryType] = + Vulcan.codec(this) +} + +trait ShortVulcanEnum[EntryType <: ShortEnumEntry] extends VulcanValueEnum[Short, EntryType] { + this: ValueEnum[Short, EntryType] => + + given vulcanCodec(using names: Names[EntryType]): Codec.Aux[Avro.Int, EntryType] = + Vulcan.codec(this) +} + +trait StringVulcanEnum[EntryType <: StringEnumEntry] extends VulcanValueEnum[String, EntryType] { + this: ValueEnum[String, EntryType] => + + given vulcanCodec(using names: Names[EntryType]): Codec.Aux[Avro.EnumSymbol, EntryType] = + Vulcan.enumCodec(this) +} diff --git a/modules/enumeratum/src/test/scala/enumeratum/values/TestEnums.scala b/modules/enumeratum/src/test/scala/enumeratum/values/TestEnums.scala new file mode 100644 index 00000000..1620996b --- /dev/null +++ b/modules/enumeratum/src/test/scala/enumeratum/values/TestEnums.scala @@ -0,0 +1,91 @@ +package enumeratum.values + +import vulcan.generic.{AvroDoc, AvroNamespace} + +sealed abstract class CustomByteEnum(val value: Byte) extends ByteEnumEntry + +object CustomByteEnum extends ByteEnum[CustomByteEnum] with ByteVulcanEnum[CustomByteEnum] { + case object First extends CustomByteEnum(1) + case object Second extends CustomByteEnum(2) + case object Third extends CustomByteEnum(3) + + val values = findValues + + override def withValueOpt(i: Byte): Option[CustomByteEnum] = + if (i == 3) None + else super.withValueOpt(i) +} + +sealed abstract class CustomCharEnum(val value: Char) extends CharEnumEntry + +object CustomCharEnum extends CharEnum[CustomCharEnum] with CharVulcanEnum[CustomCharEnum] { + case object First extends CustomCharEnum('1') + case object Second extends CustomCharEnum('2') + case object Third extends CustomCharEnum('3') + + val values = findValues + + override def withValueOpt(c: Char): Option[CustomCharEnum] = + if (c == '3') None + else super.withValueOpt(c) +} + +sealed abstract class CustomIntEnum(val value: Int) extends IntEnumEntry + +object CustomIntEnum extends IntEnum[CustomIntEnum] with IntVulcanEnum[CustomIntEnum] { + case object First extends CustomIntEnum(1) + case object Second extends CustomIntEnum(2) + case object Third extends CustomIntEnum(3) + + val values = findValues + + override def withValueOpt(i: Int): Option[CustomIntEnum] = + if (i == 3) None + else super.withValueOpt(i) +} + +sealed abstract class CustomLongEnum(val value: Long) extends LongEnumEntry + +object CustomLongEnum extends LongEnum[CustomLongEnum] with LongVulcanEnum[CustomLongEnum] { + case object First extends CustomLongEnum(1L) + case object Second extends CustomLongEnum(2L) + case object Third extends CustomLongEnum(3L) + + val values = findValues + + override def withValueOpt(l: Long): Option[CustomLongEnum] = + if (l == 3L) None + else super.withValueOpt(l) +} + +sealed abstract class CustomShortEnum(val value: Short) extends ShortEnumEntry + +object CustomShortEnum extends ShortEnum[CustomShortEnum] with ShortVulcanEnum[CustomShortEnum] { + case object First extends CustomShortEnum(1) + case object Second extends CustomShortEnum(2) + case object Third extends CustomShortEnum(3) + + val values = findValues + + override def withValueOpt(s: Short): Option[CustomShortEnum] = + if (s == 3) None + else super.withValueOpt(s) +} + +@AvroNamespace("com.example") +@AvroDoc("Custom enumeration") +sealed abstract class CustomStringEnum(val value: String) extends StringEnumEntry + +object CustomStringEnum + extends StringEnum[CustomStringEnum] + with StringVulcanEnum[CustomStringEnum] { + case object First extends CustomStringEnum("first") + case object Second extends CustomStringEnum("second") + case object Third extends CustomStringEnum("third") + + val values = findValues + + override def withValueOpt(s: String): Option[CustomStringEnum] = + if (s == "third") None + else super.withValueOpt(s) +} diff --git a/modules/enumeratum/src/test/scala/enumeratum/values/VulcanValueEnumSpec.scala b/modules/enumeratum/src/test/scala/enumeratum/values/VulcanValueEnumSpec.scala index a026460e..73e073e0 100644 --- a/modules/enumeratum/src/test/scala/enumeratum/values/VulcanValueEnumSpec.scala +++ b/modules/enumeratum/src/test/scala/enumeratum/values/VulcanValueEnumSpec.scala @@ -11,242 +11,168 @@ import org.scalacheck.Gen import org.scalatest.funspec.AnyFunSpec import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks import vulcan.Codec -import vulcan.generic.{AvroDoc, AvroNamespace} final class VulcanValueEnumSpec extends AnyFunSpec with ScalaCheckPropertyChecks with EitherValues { - describe("ByteVulcanEnum") { - sealed abstract class CustomEnum(val value: Byte) extends ByteEnumEntry - - object CustomEnum extends ByteEnum[CustomEnum] with ByteVulcanEnum[CustomEnum] { - case object First extends CustomEnum(1) - case object Second extends CustomEnum(2) - case object Third extends CustomEnum(3) - val values = findValues - - override def withValueOpt(i: Byte): Option[CustomEnum] = - if (i == 3) None - else super.withValueOpt(i) - } + describe("ByteVulcanEnum") { it("schema should be same as for underlying type") { assert { - Codec[CustomEnum].schema.value.toString === + Codec[CustomByteEnum].schema.value.toString === Codec[Byte].schema.value.toString } } it("should roundtrip enumeration values") { - val gen = Gen.oneOf[CustomEnum](CustomEnum.First, CustomEnum.Second) + val gen = Gen.oneOf[CustomByteEnum](CustomByteEnum.First, CustomByteEnum.Second) forAll(gen) { customEnum => - val roundtrip = Codec.encode(customEnum).flatMap(Codec.decode[CustomEnum]) + val roundtrip = Codec.encode(customEnum).flatMap(Codec.decode[CustomByteEnum]) assert(roundtrip.value === customEnum) } } it("should error if withValueOpt does not handle schema value") { - val roundtrip = Codec.encode[CustomEnum](CustomEnum.Third).flatMap(Codec.decode[CustomEnum]) + val roundtrip = + Codec.encode[CustomByteEnum](CustomByteEnum.Third).flatMap(Codec.decode[CustomByteEnum]) assert { roundtrip.swap.value.message === - """3 is not a member of CustomEnum (1, 2, 3)""" + """3 is not a member of CustomByteEnum (1, 2, 3)""" } } } describe("CharVulcanEnum") { - sealed abstract class CustomEnum(val value: Char) extends CharEnumEntry - - object CustomEnum extends CharEnum[CustomEnum] with CharVulcanEnum[CustomEnum] { - case object First extends CustomEnum('1') - case object Second extends CustomEnum('2') - case object Third extends CustomEnum('3') - - val values = findValues - - override def withValueOpt(c: Char): Option[CustomEnum] = - if (c == '3') None - else super.withValueOpt(c) - } it("schema should be same as for underlying type") { assert { - Codec[CustomEnum].schema.value.toString === + Codec[CustomCharEnum].schema.value.toString === Codec[Char].schema.value.toString } } it("should roundtrip enumeration values") { - val gen = Gen.oneOf[CustomEnum](CustomEnum.First, CustomEnum.Second) + val gen = Gen.oneOf[CustomCharEnum](CustomCharEnum.First, CustomCharEnum.Second) forAll(gen) { customEnum => - val roundtrip = Codec.encode(customEnum).flatMap(Codec.decode[CustomEnum]) + val roundtrip = Codec.encode(customEnum).flatMap(Codec.decode[CustomCharEnum]) assert(roundtrip.value === customEnum) } } it("should error if withValueOpt does not handle schema value") { - val roundtrip = Codec.encode[CustomEnum](CustomEnum.Third).flatMap(Codec.decode[CustomEnum]) + val roundtrip = + Codec.encode[CustomCharEnum](CustomCharEnum.Third).flatMap(Codec.decode[CustomCharEnum]) assert { roundtrip.swap.value.message === - """3 is not a member of CustomEnum (1, 2, 3)""" + """3 is not a member of CustomCharEnum (1, 2, 3)""" } } } describe("IntVulcanEnum") { - sealed abstract class CustomEnum(val value: Int) extends IntEnumEntry - - object CustomEnum extends IntEnum[CustomEnum] with IntVulcanEnum[CustomEnum] { - case object First extends CustomEnum(1) - case object Second extends CustomEnum(2) - case object Third extends CustomEnum(3) - - val values = findValues - - override def withValueOpt(i: Int): Option[CustomEnum] = - if (i == 3) None - else super.withValueOpt(i) - } it("schema should be same as for underlying type") { assert { - Codec[CustomEnum].schema.value.toString === + Codec[CustomIntEnum].schema.value.toString === Codec[Int].schema.value.toString } } it("should roundtrip enumeration values") { - val gen = Gen.oneOf[CustomEnum](CustomEnum.First, CustomEnum.Second) + val gen = Gen.oneOf[CustomIntEnum](CustomIntEnum.First, CustomIntEnum.Second) forAll(gen) { customEnum => - val roundtrip = Codec.encode(customEnum).flatMap(Codec.decode[CustomEnum]) + val roundtrip = Codec.encode(customEnum).flatMap(Codec.decode[CustomIntEnum]) assert(roundtrip.value === customEnum) } } it("should error if withValueOpt does not handle schema value") { - val roundtrip = Codec.encode[CustomEnum](CustomEnum.Third).flatMap(Codec.decode[CustomEnum]) + val roundtrip = + Codec.encode[CustomIntEnum](CustomIntEnum.Third).flatMap(Codec.decode[CustomIntEnum]) assert { roundtrip.swap.value.message === - """3 is not a member of CustomEnum (1, 2, 3)""" + """3 is not a member of CustomIntEnum (1, 2, 3)""" } } } describe("LongVulcanEnum") { - sealed abstract class CustomEnum(val value: Long) extends LongEnumEntry - - object CustomEnum extends LongEnum[CustomEnum] with LongVulcanEnum[CustomEnum] { - case object First extends CustomEnum(1L) - case object Second extends CustomEnum(2L) - case object Third extends CustomEnum(3L) - - val values = findValues - - override def withValueOpt(l: Long): Option[CustomEnum] = - if (l == 3L) None - else super.withValueOpt(l) - } it("schema should be same as for underlying type") { assert { - Codec[CustomEnum].schema.value.toString === + Codec[CustomLongEnum].schema.value.toString === Codec[Long].schema.value.toString } } it("should roundtrip enumeration values") { - val gen = Gen.oneOf[CustomEnum](CustomEnum.First, CustomEnum.Second) + val gen = Gen.oneOf[CustomLongEnum](CustomLongEnum.First, CustomLongEnum.Second) forAll(gen) { customEnum => - val roundtrip = Codec.encode(customEnum).flatMap(Codec.decode[CustomEnum]) + val roundtrip = Codec.encode(customEnum).flatMap(Codec.decode[CustomLongEnum]) assert(roundtrip.value === customEnum) } } it("should error if withValueOpt does not handle schema value") { - val roundtrip = Codec.encode[CustomEnum](CustomEnum.Third).flatMap(Codec.decode[CustomEnum]) + val roundtrip = + Codec.encode[CustomLongEnum](CustomLongEnum.Third).flatMap(Codec.decode[CustomLongEnum]) assert { roundtrip.swap.value.message === - """3 is not a member of CustomEnum (1, 2, 3)""" + """3 is not a member of CustomLongEnum (1, 2, 3)""" } } } describe("ShortVulcanEnum") { - sealed abstract class CustomEnum(val value: Short) extends ShortEnumEntry - - object CustomEnum extends ShortEnum[CustomEnum] with ShortVulcanEnum[CustomEnum] { - case object First extends CustomEnum(1) - case object Second extends CustomEnum(2) - case object Third extends CustomEnum(3) - - val values = findValues - - override def withValueOpt(s: Short): Option[CustomEnum] = - if (s == 3) None - else super.withValueOpt(s) - } it("schema should be same as for underlying type") { assert { - Codec[CustomEnum].schema.value.toString === + Codec[CustomShortEnum].schema.value.toString === Codec[Short].schema.value.toString } } it("should roundtrip enumeration values") { - val gen = Gen.oneOf[CustomEnum](CustomEnum.First, CustomEnum.Second) + val gen = Gen.oneOf[CustomShortEnum](CustomShortEnum.First, CustomShortEnum.Second) forAll(gen) { customEnum => - val roundtrip = Codec.encode(customEnum).flatMap(Codec.decode[CustomEnum]) + val roundtrip = Codec.encode(customEnum).flatMap(Codec.decode[CustomShortEnum]) assert(roundtrip.value === customEnum) } } it("should error if withValueOpt does not handle schema value") { - val roundtrip = Codec.encode[CustomEnum](CustomEnum.Third).flatMap(Codec.decode[CustomEnum]) + val roundtrip = + Codec.encode[CustomShortEnum](CustomShortEnum.Third).flatMap(Codec.decode[CustomShortEnum]) assert { roundtrip.swap.value.message === - """3 is not a member of CustomEnum (1, 2, 3)""" + """3 is not a member of CustomShortEnum (1, 2, 3)""" } } } describe("StringVulcanEnum") { - @AvroNamespace("com.example") - @AvroDoc("Custom enumeration") - sealed abstract class CustomEnum(val value: String) extends StringEnumEntry - - object CustomEnum extends StringEnum[CustomEnum] with StringVulcanEnum[CustomEnum] { - case object First extends CustomEnum("first") - case object Second extends CustomEnum("second") - case object Third extends CustomEnum("third") - - val values = findValues - - override def withValueOpt(s: String): Option[CustomEnum] = - if (s == "third") None - else super.withValueOpt(s) - } - it("schema should be enum") { assert { - Codec[CustomEnum].schema.value.toString === - """{"type":"enum","name":"CustomEnum","namespace":"com.example","doc":"Custom enumeration","symbols":["first","second","third"]}""" + Codec[CustomStringEnum].schema.value.toString === + """{"type":"enum","name":"CustomStringEnum","namespace":"com.example","doc":"Custom enumeration","symbols":["first","second","third"]}""" } } it("should roundtrip enumeration values") { - val gen = Gen.oneOf[CustomEnum](CustomEnum.First, CustomEnum.Second) + val gen = Gen.oneOf[CustomStringEnum](CustomStringEnum.First, CustomStringEnum.Second) forAll(gen) { customEnum => - val roundtrip = Codec.encode(customEnum).flatMap(Codec.decode[CustomEnum]) + val roundtrip = Codec.encode(customEnum).flatMap(Codec.decode[CustomStringEnum]) assert(roundtrip.value === customEnum) } } it("should error if withValueOpt does not handle schema value") { - val roundtrip = Codec.encode[CustomEnum](CustomEnum.Third).flatMap(Codec.decode[CustomEnum]) + val roundtrip = Codec + .encode[CustomStringEnum](CustomStringEnum.Third) + .flatMap(Codec.decode[CustomStringEnum]) assert { roundtrip.swap.value.message === - """Error decoding com.example.CustomEnum: third is not a member of CustomEnum (first, second, third)""" + """Error decoding com.example.CustomStringEnum: third is not a member of CustomStringEnum (first, second, third)""" } } } diff --git a/modules/generic/src/main/scala-3/vulcan/generic/package.scala b/modules/generic/src/main/scala-3/vulcan/generic/package.scala index 2b44ec50..cfa75455 100644 --- a/modules/generic/src/main/scala-3/vulcan/generic/package.scala +++ b/modules/generic/src/main/scala-3/vulcan/generic/package.scala @@ -19,6 +19,7 @@ import magnolia1._ import org.apache.avro.generic._ import org.apache.avro.Schema import vulcan.internal.converters.collection._ +import vulcan.internal.Names package object generic { @@ -114,15 +115,15 @@ package object generic { symbols: Seq[String], encode: A => String, decode: String => Either[AvroError, A] - ): Codec.Aux[Avro.EnumSymbol, A] = + )(using names: Names[A]): Codec.Aux[Avro.EnumSymbol, A] = Codec.enumeration( - name = nameOf[A], + name = names.typeName, symbols = symbols, encode = encode, decode = decode, - namespace = namespaceOf[A], - doc = docOf[A], - aliases = aliasOf[A] + namespace = names.namespace, + doc = names.doc, + aliases = names.aliasOf.toSeq ) /** Returns a fixed `Codec` for type `A`, deriving details like the name, namespace, and @@ -134,34 +135,15 @@ package object generic { size: Int, encode: A => Array[Byte], decode: Array[Byte] => Either[AvroError, A] - ): Codec.Aux[Avro.Fixed, A] = + )(using names: Names[A]): Codec.Aux[Avro.Fixed, A] = Codec.fixed( - name = nameOf[A], + name = names.typeName, size = size, encode = encode, decode = decode, - namespace = namespaceOf[A], - doc = docOf[A], - aliases = aliasOf[A] + namespace = names.namespace, + doc = names.doc, + aliases = names.aliasOf.toSeq ) - private inline def nameOf[A]: String = summonFrom { - case a: Annotation[AvroName, A] => a().name - case ct: ClassTag[A] => ct.runtimeClass.getSimpleName - } - - private inline def namespaceOf[A]: String = summonFrom { - case a: Annotation[AvroNamespace, A] => a().namespace - case ct: ClassTag[A] => ct.runtimeClass.getPackage.getName - } - - private inline def docOf[A]: Option[String] = summonFrom { - case a: Annotation[AvroDoc, A] => Some(a().doc) - case _ => None - } - - private inline def aliasOf[A]: Seq[String] = summonFrom { - case a: Annotation[AvroAlias, A] => Seq(a().alias) - case _ => Seq() - } } diff --git a/modules/generic/src/main/scala-3/vulcan/internal/Names.scala b/modules/generic/src/main/scala-3/vulcan/internal/Names.scala new file mode 100644 index 00000000..7cf84587 --- /dev/null +++ b/modules/generic/src/main/scala-3/vulcan/internal/Names.scala @@ -0,0 +1,20 @@ +/* + * Copyright 2019 OVO Energy Limited + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package vulcan.internal +import vulcan.internal.NamesMacro + +case class Names[A]( + typeName: String, + namespace: String, + doc: Option[String], + aliasOf: Option[String] +) + +object Names { + + inline given [A]: Names[A] = NamesMacro.names[A] +} diff --git a/modules/generic/src/main/scala-3/vulcan/internal/NamesMacro.scala b/modules/generic/src/main/scala-3/vulcan/internal/NamesMacro.scala new file mode 100644 index 00000000..4036c880 --- /dev/null +++ b/modules/generic/src/main/scala-3/vulcan/internal/NamesMacro.scala @@ -0,0 +1,43 @@ +/* + * Copyright 2019 OVO Energy Limited + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package vulcan.internal + +import scala.quoted.* +import scala.compiletime.* +import vulcan.internal.Names +import vulcan.generic.AvroDoc +import vulcan.generic.AvroName +import vulcan.generic.AvroNamespace +import vulcan.generic.AvroAlias + +object NamesMacro { + inline def names[A]: Names[A] = ${ namesImpl[A] } + + def namesImpl[A: Type](using Quotes): Expr[Names[A]] = + import quotes.reflect.* + val symbol = TypeTree.of[A].symbol + + def getAnnotValue[Ann: Type]: Option[Expr[Ann]] = + symbol.getAnnotation(TypeRepr.of[Ann].typeSymbol).map(p => p.asExprOf[Ann]) + + def toExprOpt(e: Option[Expr[String]]): Expr[Option[String]] = + e match { + case Some(stringExp) => '{ Some($stringExp) } + case None => '{ Option.empty[String] } + } + + val typeName = getAnnotValue[AvroName].map(e => '{ $e.name }).getOrElse(Expr(symbol.name)) + val namespace = getAnnotValue[AvroNamespace] + .map(e => '{ $e.namespace }) + .getOrElse(Expr(symbol.owner.fullName)) + + val doc = toExprOpt(getAnnotValue[AvroDoc].map(e => '{ $e.doc })) + val alias = toExprOpt(getAnnotValue[AvroAlias].map(e => '{ $e.alias })) + + '{ Names[A]($typeName, $namespace, $doc, $alias) } + +}