From 0e80ab8836831169d53237210fe65c41e2682837 Mon Sep 17 00:00:00 2001 From: charlibot Date: Thu, 28 Apr 2022 21:28:36 +0200 Subject: [PATCH 1/4] introduce extractName method to generic derviations --- .../main/scala-2/vulcan/generic/package.scala | 6 +++++- .../main/scala-3/vulcan/generic/package.scala | 6 +++++- .../generic/GenericDerivationCodecSpec.scala | 18 ++++++++++++++++ .../CaseClassTypeParameterField.scala | 21 +++++++++++++++++++ 4 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 modules/generic/src/test/scala/vulcan/generic/examples/CaseClassTypeParameterField.scala diff --git a/modules/generic/src/main/scala-2/vulcan/generic/package.scala b/modules/generic/src/main/scala-2/vulcan/generic/package.scala index 34c6f166..6a3ddedb 100644 --- a/modules/generic/src/main/scala-2/vulcan/generic/package.scala +++ b/modules/generic/src/main/scala-2/vulcan/generic/package.scala @@ -61,7 +61,7 @@ package object generic { .record[A]( name = caseClass.annotations .collectFirst { case AvroName(namespace) => namespace } - .getOrElse(caseClass.typeName.short), + .getOrElse(extractName(caseClass.typeName)), namespace = caseClass.annotations .collectFirst { case AvroNamespace(namespace) => namespace } .getOrElse(caseClass.typeName.owner), @@ -98,6 +98,10 @@ package object generic { } } + private def extractName(typeName: TypeName): String = + if (typeName.typeArguments.isEmpty) typeName.short + else typeName.short + "__" + typeName.typeArguments.map(extractName).mkString("_") + /** * Returns a `Codec` instance for the specified type, * deriving details from the type, as long as the 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 90b538b5..b272f1e7 100644 --- a/modules/generic/src/main/scala-3/vulcan/generic/package.scala +++ b/modules/generic/src/main/scala-3/vulcan/generic/package.scala @@ -33,7 +33,7 @@ package object generic { .record[A]( name = caseClass.annotations .collectFirst { case AvroName(namespace) => namespace } - .getOrElse(caseClass.typeInfo.short), + .getOrElse(extractName(caseClass.typeInfo)), namespace = caseClass.annotations .collectFirst { case AvroNamespace(namespace) => namespace } .getOrElse(caseClass.typeInfo.owner), @@ -69,6 +69,10 @@ package object generic { .map(caseClass.rawConstruct(_)) } + private def extractName(typeInfo: TypeInfo): String = + if (typeInfo.typeParams.isEmpty) typeInfo.short + else typeInfo.short + "__" + typeInfo.typeParams.map(extractName).mkString("_") + final def split[A](sealedTrait: SealedTrait[Codec, A]): Codec.Aux[Any, A] = { Codec .union[A]( diff --git a/modules/generic/src/test/scala/vulcan/generic/GenericDerivationCodecSpec.scala b/modules/generic/src/test/scala/vulcan/generic/GenericDerivationCodecSpec.scala index b0505e24..e311c359 100644 --- a/modules/generic/src/test/scala/vulcan/generic/GenericDerivationCodecSpec.scala +++ b/modules/generic/src/test/scala/vulcan/generic/GenericDerivationCodecSpec.scala @@ -69,6 +69,24 @@ final class GenericDerivationCodecSpec extends CodecBase { } } + it("should support generic case classes and include the type name in the schema name (Int)") { + assertSchemaIs[CaseClassTypeParameterField[Int]] { + """{"type":"record","name":"CaseClassTypeParameterField__Int","namespace":"vulcan.generic.examples","fields":[{"name":"s","type":"string"},{"name":"value","type":"int"}]}""" + } + } + + it("should support generic case classes and include the type name in the schema name (Long)") { + assertSchemaIs[CaseClassTypeParameterField[Long]] { + """{"type":"record","name":"CaseClassTypeParameterField__Long","namespace":"vulcan.generic.examples","fields":[{"name":"s","type":"string"},{"name":"value","type":"long"}]}""" + } + } + + it("should support case classes with nested generic case classes and include the types in the schema name") { + assertSchemaIs[CaseClassTypeParameterField[CaseClassInner[Int]]] { + """{"type":"record","name":"CaseClassTypeParameterField__CaseClassInner__Int","namespace":"vulcan.generic.examples","fields":[{"name":"s","type":"string"},{"name":"value","type":{"type":"record","name":"CaseClassInner__Int","fields":[{"name":"inner","type":"int"}]}}]}""" + } + } + } describe("encode") { diff --git a/modules/generic/src/test/scala/vulcan/generic/examples/CaseClassTypeParameterField.scala b/modules/generic/src/test/scala/vulcan/generic/examples/CaseClassTypeParameterField.scala new file mode 100644 index 00000000..859e3a49 --- /dev/null +++ b/modules/generic/src/test/scala/vulcan/generic/examples/CaseClassTypeParameterField.scala @@ -0,0 +1,21 @@ +package vulcan.generic.examples + +import vulcan.Codec +import vulcan.generic._ + +final case class CaseClassTypeParameterField[T](s: String, value: T) +final case class CaseClassInner[T](inner: T) + +object CaseClassTypeParameterField { + implicit val intCodec: Codec[CaseClassTypeParameterField[Int]] = + Codec.derive + + implicit val longCodec: Codec[CaseClassTypeParameterField[Long]] = + Codec.derive + + implicit val innerIntCodec: Codec[CaseClassInner[Int]] = + Codec.derive + + implicit val withInnerIntCodec: Codec[CaseClassTypeParameterField[CaseClassInner[Int]]] = + Codec.derive +} From 8af789b04cc165276ff17f9e9de5daec15d868a4 Mon Sep 17 00:00:00 2001 From: charlibot Date: Thu, 28 Apr 2022 21:43:07 +0200 Subject: [PATCH 2/4] add roundtrip test --- .../generic/GenericDerivationRoundtripSpec.scala | 3 ++- .../examples/CaseClassTypeParameterField.scala | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/modules/generic/src/test/scala/vulcan/generic/GenericDerivationRoundtripSpec.scala b/modules/generic/src/test/scala/vulcan/generic/GenericDerivationRoundtripSpec.scala index 0a7d62dd..7fe04370 100644 --- a/modules/generic/src/test/scala/vulcan/generic/GenericDerivationRoundtripSpec.scala +++ b/modules/generic/src/test/scala/vulcan/generic/GenericDerivationRoundtripSpec.scala @@ -5,7 +5,7 @@ import cats.implicits._ import java.io.{ByteArrayInputStream, ByteArrayOutputStream} import org.apache.avro.generic.{GenericData, GenericDatumReader, GenericDatumWriter} import org.apache.avro.io.{DecoderFactory, EncoderFactory} -import org.scalacheck.{Arbitrary} +import org.scalacheck.Arbitrary import org.scalatest.Assertion import vulcan._ import vulcan.generic.examples._ @@ -20,6 +20,7 @@ final class GenericDerivationRoundtripSpec extends BaseSpec { it("CaseClassAvroNullDefault") { roundtrip[CaseClassAvroNullDefault] } it("CaseClassFieldAvroNullDefault") { roundtrip[CaseClassFieldAvroNullDefault] } it("CaseClassAndFieldAvroNullDefault") { roundtrip[CaseClassAndFieldAvroNullDefault] } + it("CaseClassTypeParameterField") { roundtrip[CaseClassTypeParameterField[Int]] } } def roundtrip[A]( diff --git a/modules/generic/src/test/scala/vulcan/generic/examples/CaseClassTypeParameterField.scala b/modules/generic/src/test/scala/vulcan/generic/examples/CaseClassTypeParameterField.scala index 859e3a49..62cf2449 100644 --- a/modules/generic/src/test/scala/vulcan/generic/examples/CaseClassTypeParameterField.scala +++ b/modules/generic/src/test/scala/vulcan/generic/examples/CaseClassTypeParameterField.scala @@ -1,5 +1,8 @@ package vulcan.generic.examples +import cats.Eq +import org.scalacheck.Arbitrary +import org.scalacheck.Arbitrary.arbitrary import vulcan.Codec import vulcan.generic._ @@ -18,4 +21,13 @@ object CaseClassTypeParameterField { implicit val withInnerIntCodec: Codec[CaseClassTypeParameterField[CaseClassInner[Int]]] = Codec.derive + + implicit val caseClassTypeParameterFieldArbitrary: Arbitrary[CaseClassTypeParameterField[Int]] = + Arbitrary(for { + s <- arbitrary[String] + i <- arbitrary[Int] + } yield CaseClassTypeParameterField(s, i)) + + implicit val caseClassTypeParameterFieldEq: Eq[CaseClassTypeParameterField[Int]] = + Eq.fromUniversalEquals } From 43eee8429349003b985c59e097e62162ef21cbfe Mon Sep 17 00:00:00 2001 From: charlibot Date: Tue, 10 May 2022 18:27:56 +0200 Subject: [PATCH 3/4] add Configuration --- .../main/scala-2/vulcan/generic/package.scala | 17 +++++++++++------ .../scala/vulcan/generic/Configuration.scala | 19 +++++++++++++++++++ .../CaseClassTypeParameterField.scala | 2 ++ 3 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 modules/generic/src/main/scala/vulcan/generic/Configuration.scala diff --git a/modules/generic/src/main/scala-2/vulcan/generic/package.scala b/modules/generic/src/main/scala-2/vulcan/generic/package.scala index 6a3ddedb..5d229318 100644 --- a/modules/generic/src/main/scala-2/vulcan/generic/package.scala +++ b/modules/generic/src/main/scala-2/vulcan/generic/package.scala @@ -51,7 +51,12 @@ package object generic { implicit final class MagnoliaCodec private[generic] ( private val codec: Codec.type ) extends AnyVal { - final def combine[A](caseClass: CaseClass[Codec, A]): Codec[A] = + final def combine[A](caseClass: CaseClass[Codec, A])(implicit config: Configuration = Configuration.default): Codec[A] = { + def extractName(typeName: TypeName, genSep: String, typeSep: String): String = { + if (typeName.typeArguments.isEmpty) typeName.short + else typeName.short + genSep + typeName.typeArguments.map(extractName(_, genSep, typeSep)).mkString(typeSep) + } + if (caseClass.isValueClass) { val param = caseClass.parameters.head param.typeclass.imap(value => caseClass.rawConstruct(List(value)))(param.dereference) @@ -61,7 +66,10 @@ package object generic { .record[A]( name = caseClass.annotations .collectFirst { case AvroName(namespace) => namespace } - .getOrElse(extractName(caseClass.typeName)), + .getOrElse(config.typeSeparators match { + case Some((genSep, typeSep)) => extractName(caseClass.typeName, genSep, typeSep) + case None => caseClass.typeName.short + }), namespace = caseClass.annotations .collectFirst { case AvroNamespace(namespace) => namespace } .getOrElse(caseClass.typeName.owner), @@ -97,10 +105,7 @@ package object generic { .map(caseClass.rawConstruct(_)) } } - - private def extractName(typeName: TypeName): String = - if (typeName.typeArguments.isEmpty) typeName.short - else typeName.short + "__" + typeName.typeArguments.map(extractName).mkString("_") + } /** * Returns a `Codec` instance for the specified type, diff --git a/modules/generic/src/main/scala/vulcan/generic/Configuration.scala b/modules/generic/src/main/scala/vulcan/generic/Configuration.scala new file mode 100644 index 00000000..4969b3e9 --- /dev/null +++ b/modules/generic/src/main/scala/vulcan/generic/Configuration.scala @@ -0,0 +1,19 @@ +package vulcan.generic + +final case class Configuration(typeSeparators: Option[(String, String)]) { + def withTypeSeparators(genericTypeSep: String, typeSep: String): Configuration = + copy(typeSeparators = Some((genericTypeSep, typeSep))) +} + +object Configuration { + val default = Configuration(None) +} + +object defaults { + implicit val defaultGenericConfiguration: Configuration = Configuration.default +} + +object avro4s { + implicit val avro4sGenericConfiguration: Configuration = + Configuration.default.withTypeSeparators("__", "_") +} \ No newline at end of file diff --git a/modules/generic/src/test/scala/vulcan/generic/examples/CaseClassTypeParameterField.scala b/modules/generic/src/test/scala/vulcan/generic/examples/CaseClassTypeParameterField.scala index 62cf2449..7612b55e 100644 --- a/modules/generic/src/test/scala/vulcan/generic/examples/CaseClassTypeParameterField.scala +++ b/modules/generic/src/test/scala/vulcan/generic/examples/CaseClassTypeParameterField.scala @@ -10,6 +10,8 @@ final case class CaseClassTypeParameterField[T](s: String, value: T) final case class CaseClassInner[T](inner: T) object CaseClassTypeParameterField { + implicit val configuration: Configuration = avro4s.avro4sGenericConfiguration + implicit val intCodec: Codec[CaseClassTypeParameterField[Int]] = Codec.derive From bc8d44f749bcd70c0a1b47b95b56ad79f8fcf9f2 Mon Sep 17 00:00:00 2001 From: charlibot Date: Wed, 11 May 2022 11:40:40 +0200 Subject: [PATCH 4/4] give test CaseClassInner two generic parameters --- .../scala/vulcan/generic/GenericDerivationCodecSpec.scala | 4 ++-- .../generic/examples/CaseClassTypeParameterField.scala | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/generic/src/test/scala/vulcan/generic/GenericDerivationCodecSpec.scala b/modules/generic/src/test/scala/vulcan/generic/GenericDerivationCodecSpec.scala index e311c359..05195d4e 100644 --- a/modules/generic/src/test/scala/vulcan/generic/GenericDerivationCodecSpec.scala +++ b/modules/generic/src/test/scala/vulcan/generic/GenericDerivationCodecSpec.scala @@ -82,8 +82,8 @@ final class GenericDerivationCodecSpec extends CodecBase { } it("should support case classes with nested generic case classes and include the types in the schema name") { - assertSchemaIs[CaseClassTypeParameterField[CaseClassInner[Int]]] { - """{"type":"record","name":"CaseClassTypeParameterField__CaseClassInner__Int","namespace":"vulcan.generic.examples","fields":[{"name":"s","type":"string"},{"name":"value","type":{"type":"record","name":"CaseClassInner__Int","fields":[{"name":"inner","type":"int"}]}}]}""" + assertSchemaIs[CaseClassTypeParameterField[CaseClassInner[Int, Long]]] { + """{"type":"record","name":"CaseClassTypeParameterField__CaseClassInner__Int_Long","namespace":"vulcan.generic.examples","fields":[{"name":"s","type":"string"},{"name":"value","type":{"type":"record","name":"CaseClassInner__Int_Long","fields":[{"name":"inner1","type":"int"},{"name":"inner2","type":"long"}]}}]}""" } } diff --git a/modules/generic/src/test/scala/vulcan/generic/examples/CaseClassTypeParameterField.scala b/modules/generic/src/test/scala/vulcan/generic/examples/CaseClassTypeParameterField.scala index 7612b55e..6edf8575 100644 --- a/modules/generic/src/test/scala/vulcan/generic/examples/CaseClassTypeParameterField.scala +++ b/modules/generic/src/test/scala/vulcan/generic/examples/CaseClassTypeParameterField.scala @@ -7,7 +7,7 @@ import vulcan.Codec import vulcan.generic._ final case class CaseClassTypeParameterField[T](s: String, value: T) -final case class CaseClassInner[T](inner: T) +final case class CaseClassInner[T, S](inner1: T, inner2: S) object CaseClassTypeParameterField { implicit val configuration: Configuration = avro4s.avro4sGenericConfiguration @@ -18,10 +18,10 @@ object CaseClassTypeParameterField { implicit val longCodec: Codec[CaseClassTypeParameterField[Long]] = Codec.derive - implicit val innerIntCodec: Codec[CaseClassInner[Int]] = + implicit val innerIntCodec: Codec[CaseClassInner[Int, Long]] = Codec.derive - implicit val withInnerIntCodec: Codec[CaseClassTypeParameterField[CaseClassInner[Int]]] = + implicit val withInnerIntCodec: Codec[CaseClassTypeParameterField[CaseClassInner[Int, Long]]] = Codec.derive implicit val caseClassTypeParameterFieldArbitrary: Arbitrary[CaseClassTypeParameterField[Int]] =