Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include generic parameters in record name #444

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions modules/generic/src/main/scala-2/vulcan/generic/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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),
Expand Down Expand Up @@ -97,6 +105,7 @@ package object generic {
.map(caseClass.rawConstruct(_))
}
}
}

/**
* Returns a `Codec` instance for the specified type,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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](
Expand Down
19 changes: 19 additions & 0 deletions modules/generic/src/main/scala/vulcan/generic/Configuration.scala
Original file line number Diff line number Diff line change
@@ -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("__", "_")
}
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand All @@ -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](
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}