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..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(caseClass.typeName.short), + .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,6 +105,7 @@ package object generic { .map(caseClass.rawConstruct(_)) } } + } /** * Returns a `Codec` instance for the specified type, 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/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/GenericDerivationCodecSpec.scala b/modules/generic/src/test/scala/vulcan/generic/GenericDerivationCodecSpec.scala index b0505e24..05195d4e 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, 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"}]}}]}""" + } + } + } describe("encode") { 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 new file mode 100644 index 00000000..6edf8575 --- /dev/null +++ b/modules/generic/src/test/scala/vulcan/generic/examples/CaseClassTypeParameterField.scala @@ -0,0 +1,35 @@ +package vulcan.generic.examples + +import cats.Eq +import org.scalacheck.Arbitrary +import org.scalacheck.Arbitrary.arbitrary +import vulcan.Codec +import vulcan.generic._ + +final case class CaseClassTypeParameterField[T](s: String, value: T) +final case class CaseClassInner[T, S](inner1: T, inner2: S) + +object CaseClassTypeParameterField { + implicit val configuration: Configuration = avro4s.avro4sGenericConfiguration + + implicit val intCodec: Codec[CaseClassTypeParameterField[Int]] = + Codec.derive + + implicit val longCodec: Codec[CaseClassTypeParameterField[Long]] = + Codec.derive + + implicit val innerIntCodec: Codec[CaseClassInner[Int, Long]] = + Codec.derive + + implicit val withInnerIntCodec: Codec[CaseClassTypeParameterField[CaseClassInner[Int, Long]]] = + 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 +}