Modules
The following sections describe the additional modules.
Enumeratum
The vulcan-enumeratum
module provides Codec
s for Enumeratum enumerations.
For regular Enum
s, also mix in VulcanEnum
to derive a Codec
instance.
import enumeratum.{Enum, EnumEntry, VulcanEnum}
import enumeratum.EnumEntry.Lowercase
import vulcan.Codec
import vulcan.generic.{AvroDoc, AvroNamespace}
@AvroNamespace("com.example")
@AvroDoc("The different card suits")
sealed trait Suit extends EnumEntry with Lowercase
object Suit extends Enum[Suit] with VulcanEnum[Suit] {
case object Clubs extends Suit
case object Diamonds extends Suit
case object Hearts extends Suit
case object Spades extends Suit
val values = findValues
}
Codec[Suit]
// res1: Codec[Suit] = WithTypeName(
// codec = Validated(
// codec = Codec({
// "type" : "enum",
// "name" : "Suit",
// "namespace" : "com.example",
// "doc" : "The different card suits",
// "symbols" : [ "clubs", "diamonds", "hearts", "spades" ]
// }),
// validSchema = {"type":"enum","name":"Suit","namespace":"com.example","doc":"The different card suits","symbols":["clubs","diamonds","hearts","spades"]}
// ),
// typeName = "com.example.Suit"
// )
Annotations like @AvroDoc
can be used to customize the derivation.
For ValueEnum
s, also mix in the matching VulcanValueEnum
to derive a Codec
instance.
import enumeratum.values.{StringEnum, StringEnumEntry, StringVulcanEnum}
@AvroNamespace("com.example")
@AvroDoc("The available colors")
sealed abstract class Color(val value: String) extends StringEnumEntry
object Color extends StringEnum[Color] with StringVulcanEnum[Color] {
case object Red extends Color("red")
case object Green extends Color("green")
case object Blue extends Color("blue")
val values = findValues
}
Codec[Color]
// res2: Codec.Aux[vulcan.Avro.EnumSymbol, Color] = WithTypeName(
// codec = Validated(
// codec = Codec({
// "type" : "enum",
// "name" : "Color",
// "namespace" : "com.example",
// "doc" : "The available colors",
// "symbols" : [ "red", "green", "blue" ]
// }),
// validSchema = {"type":"enum","name":"Color","namespace":"com.example","doc":"The available colors","symbols":["red","green","blue"]}
// ),
// typeName = "com.example.Color"
// )
For StringVulcanEnum
, the enumeration is encoded as an Avro enumeration. For the other VulcanValueEnum
s (ByteVulcanEnum
, CharVulcanEnum
, IntVulcanEnum
, LongVulcanEnum
, and ShortVulcanEnum
), the encoding relies on the encoding of the enumeration's value type (Byte
, Char
, Int
, Long
, and Short
).
import enumeratum.values.{IntEnum, IntEnumEntry, IntVulcanEnum}
sealed abstract class Day(val value: Int) extends IntEnumEntry
object Day extends IntEnum[Day] with IntVulcanEnum[Day] {
case object Monday extends Day(1)
case object Tuesday extends Day(2)
case object Wednesday extends Day(3)
case object Thursday extends Day(4)
case object Friday extends Day(5)
case object Saturday extends Day(6)
case object Sunday extends Day(7)
val values = findValues
}
Codec[Day]
// res3: Codec.Aux[Int, Day] = ImapErrors(
// codec = Codec("int"),
// f = enumeratum.values.Vulcan$$$Lambda$17148/0x0000000104885040@a6c506b,
// g = vulcan.Codec$$Lambda$16748/0x00000001046ba840@f5dd716
// )
Generic
The vulcan-generic
module provides generic derivation of Codec
s using Magnolia for records and unions, and reflection for enumerations and fixed types.
To derive Codec
s for case class
es or sealed trait
s, we can use Codec.derive
. Annotations like @AvroDoc
and @AvroNamespace
can be used to customize the documentation and namespace during derivation.
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]
// res4: 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"
// )
While case class
es correspond to Avro records, sealed trait
s 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]
// res5: Codec.Aux[Any, FirstOrSecond] = WithTypeName(
// codec = Validated(
// codec = WithTypeName(
// codec = Validated(
// codec = UnionCodec(
// alts = Append(
// leftNE = Singleton(a = vulcan.Codec$Alt$$anon$6@7896ac21),
// rightNE = Singleton(a = vulcan.Codec$Alt$$anon$6@76417db0)
// )
// ),
// 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 Coproduct
s are also supported and correspond to Avro unions.
import shapeless.{:+:, CNil}
Codec[Int :+: String :+: CNil]
// res6: Codec[Int :+: String :+: CNil] = WithTypeName(
// codec = Validated(
// codec = UnionCodec(
// alts = Append(
// leftNE = Singleton(a = vulcan.Codec$Alt$$anon$6@5178a87b),
// rightNE = Singleton(a = vulcan.Codec$Alt$$anon$6@18a393a9)
// )
// ),
// validSchema = ["int","string"]
// ),
// typeName = "Coproduct"
// )
deriveEnum
can be used to partly derive Codec
s for enumeration types. Annotations like @AvroDoc
can be used to customize the derivation.
import vulcan.AvroError
import vulcan.generic.{AvroDoc, AvroNamespace}
@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"))
}
)
// res7: 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"
// )
deriveFixed
can be used to partly derive Codec
s 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)
)
// res8: 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"
// )
Refined
The vulcan-refined
module provides Codec
s for refined refinement types.
Refinement types are encoded using their base type (e.g. Int
for PosInt
). When decoding, Codec
s check to ensure values conform to the predicate of the refinement type (e.g. Positive
for PosInt
), and raise an error for values which do not conform.
import eu.timepit.refined.auto._
import eu.timepit.refined.types.numeric.PosInt
import vulcan.refined._
Codec[PosInt]
// res9: Codec[eu.timepit.refined.api.Refined[Int, eu.timepit.refined.numeric.Positive]] = ImapErrors(
// codec = Codec("int"),
// f = vulcan.refined.package$$$Lambda$17170/0x000000010489f040@497d4be9,
// g = vulcan.Codec$$Lambda$16748/0x00000001046ba840@1fdb6c03
// )
Codec.encode[PosInt](1)
// res10: Either[AvroError, Codec[eu.timepit.refined.api.Refined[Int, eu.timepit.refined.numeric.Positive]]#AvroType] = Right(
// value = 1
// )
Codec.decode[PosInt](0)
// res11: Either[AvroError, PosInt] = Left(
// value = AvroError(Predicate failed: (0 > 0).)
// )