Derivation
The vulcan-generic module provides generic derivation of Codecs using Magnolia for records and unions, and reflection for enumerations and fixed types.
Getting Started
To get started with sbt, add the following line to your build.sbt file.
libraryDependencies += "com.github.fd4s" %% "vulcan-generic" % "1.14.1"
Enumerations
deriveEnum can be used to partly derive Codecs for enumeration types. Annotations like @AvroDoc can be used to customize the derivation.
import vulcan.AvroError
import vulcan.generic._
@AvroNamespace("com.example")
@AvroDoc("A selection of different fruits")
sealed trait Fruit
case object Apple extends Fruit
case object Banana extends Fruit
case object Cherry extends Fruit
deriveEnum[Fruit](
symbols = List("apple", "banana", "cherry"),
encode = {
case Apple => "apple"
case Banana => "banana"
case Cherry => "cherry"
},
decode = {
case "apple" => Right(Apple)
case "banana" => Right(Banana)
case "cherry" => Right(Cherry)
case other => Left(AvroError(s"$other is not a Fruit"))
}
)
// res1: vulcan.Codec.Aux[vulcan.Avro.EnumSymbol, Fruit] = WithTypeName(
// codec = Validated(
// codec = Codec({
// "type" : "enum",
// "name" : "Fruit",
// "namespace" : "com.example",
// "doc" : "A selection of different fruits",
// "symbols" : [ "apple", "banana", "cherry" ]
// }),
// validSchema = {"type":"enum","name":"Fruit","namespace":"com.example","doc":"A selection of different fruits","symbols":["apple","banana","cherry"]}
// ),
// typeName = "com.example.Fruit"
// )
Fixed
deriveFixed can be used to partly derive Codecs for fixed types. Annotations like @AvroDoc can be used to customize the derivation.
import vulcan.AvroError
sealed abstract case class Pence(value: Byte)
object Pence {
def apply(value: Byte): Either[AvroError, Pence] =
if(0 <= value && value < 100) Right(new Pence(value) {})
else Left(AvroError(s"Expected pence value, got $value"))
}
deriveFixed[Pence](
size = 1,
encode = pence => Array[Byte](pence.value),
decode = bytes => Pence(bytes.head)
)
// res2: vulcan.Codec.Aux[vulcan.Avro.Fixed, Pence] = WithTypeName(
// codec = Validated(
// codec = Codec({
// "type" : "fixed",
// "name" : "Pence",
// "namespace" : "repl.MdocSession.MdocApp0",
// "size" : 1
// }),
// validSchema = {"type":"fixed","name":"Pence","namespace":"repl.MdocSession.MdocApp0","size":1}
// ),
// typeName = "repl.MdocSession.MdocApp0.Pence"
// )
Records
To derive Codecs for case classes or sealed traits, we can use Codec.derive. Annotations like @AvroDoc and @AvroNamespace can be used to customize the documentation and namespace during derivation.
import vulcan.Codec
import vulcan.generic._
@AvroNamespace("com.example")
@AvroDoc("Person with a first name, last name, and optional age")
final case class Person(firstName: String, lastName: String, age: Option[Int])
Codec.derive[Person]
// res3: Codec[Person] = WithTypeName(
// codec = Validated(
// codec = Codec({
// "type" : "record",
// "name" : "Person",
// "namespace" : "com.example",
// "doc" : "Person with a first name, last name, and optional age",
// "fields" : [ {
// "name" : "firstName",
// "type" : "string"
// }, {
// "name" : "lastName",
// "type" : "string"
// }, {
// "name" : "age",
// "type" : [ "null", "int" ]
// } ]
// }),
// validSchema = {"type":"record","name":"Person","namespace":"com.example","doc":"Person with a first name, last name, and optional age","fields":[{"name":"firstName","type":"string"},{"name":"lastName","type":"string"},{"name":"age","type":["null","int"]}]}
// ),
// typeName = "com.example.Person"
// )
Unions
While case classes correspond to Avro records, sealed traits correspond to unions.
sealed trait FirstOrSecond
@AvroNamespace("com.example")
final case class First(value: Int) extends FirstOrSecond
@AvroNamespace("com.example")
final case class Second(value: String) extends FirstOrSecond
Codec.derive[FirstOrSecond]
// res4: Codec.Aux[Any, FirstOrSecond] = WithTypeName(
// codec = Validated(
// codec = WithTypeName(
// codec = Validated(
// codec = UnionCodec(
// alts = Append(
// leftNE = Singleton(a = vulcan.Codec$Alt$$anon$6@68fc56d3),
// rightNE = Singleton(a = vulcan.Codec$Alt$$anon$6@53ccc017)
// )
// ),
// validSchema = [{"type":"record","name":"First","namespace":"com.example","fields":[{"name":"value","type":"int"}]},{"type":"record","name":"Second","namespace":"com.example","fields":[{"name":"value","type":"string"}]}]
// ),
// typeName = "union"
// ),
// validSchema = [{"type":"record","name":"First","namespace":"com.example","fields":[{"name":"value","type":"int"}]},{"type":"record","name":"Second","namespace":"com.example","fields":[{"name":"value","type":"string"}]}]
// ),
// typeName = "repl.MdocSession.MdocApp0.FirstOrSecond"
// )
Shapeless Coproducts are also supported and correspond to Avro unions.
import shapeless.{:+:, CNil}
Codec[Int :+: String :+: CNil]
// res5: Codec[Int :+: String :+: CNil] = WithTypeName(
// codec = Validated(
// codec = UnionCodec(
// alts = Append(
// leftNE = Singleton(a = vulcan.Codec$Alt$$anon$6@73b03467),
// rightNE = Singleton(a = vulcan.Codec$Alt$$anon$6@5513a170)
// )
// ),
// validSchema = ["int","string"]
// ),
// typeName = "Coproduct"
// )