Skip to content

Commit

Permalink
AvroAlias support added (#636)
Browse files Browse the repository at this point in the history
  • Loading branch information
vkorchik authored Jan 22, 2025
1 parent a72eca8 commit bb175bb
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 6 deletions.
9 changes: 9 additions & 0 deletions modules/core/src/main/scala-2/vulcan/internal/tags.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ private[vulcan] object tags {
doc.substring(1, doc.length - 1)
}

final def aliasFrom[A](tag: WeakTypeTag[A]): Seq[String] =
tag.tpe.typeSymbol.annotations.collectFirst {
case annotation
if annotation.tree.tpe.typeSymbol.fullName == "vulcan.AvroAlias" ||
annotation.tree.tpe.typeSymbol.fullName == "vulcan.generic.AvroAlias" =>
val doc = annotation.tree.children.last.toString
doc.substring(1, doc.length - 1)
}.toList

final def nameFrom[A](tag: WeakTypeTag[A]): String =
tag.tpe.typeSymbol.annotations
.collectFirst {
Expand Down
14 changes: 11 additions & 3 deletions modules/generic/src/main/scala-2/vulcan/generic/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ package object generic {
.getOrElse(caseClass.typeName.owner),
doc = caseClass.annotations.collectFirst {
case AvroDoc(doc) => doc
}
},
aliases = caseClass.annotations.collectFirst {
case AvroAlias(alias) => alias
}.toList
) { (f: Codec.FieldBuilder[A]) =>
val nullDefaultBase = caseClass.annotations
.collectFirst { case AvroNullDefault(enabled) => enabled }
Expand Down Expand Up @@ -97,6 +100,9 @@ package object generic {
doc = param.annotations.collectFirst {
case AvroDoc(doc) => doc
},
aliases = param.annotations.collectFirst {
case AvroAlias(alias) => alias
}.toList,
default = param.default.orElse(
if (codec.schema.exists(_.isNullable) && nullDefaultField)
Some(None.asInstanceOf[param.PType]) // TODO: remove cast
Expand Down Expand Up @@ -149,7 +155,8 @@ package object generic {
encode = encode,
decode = decode,
namespace = namespaceFrom(tag),
doc = docFrom(tag)
doc = docFrom(tag),
aliases = aliasFrom(tag)
)

/**
Expand All @@ -170,6 +177,7 @@ package object generic {
encode = encode,
decode = decode,
namespace = namespaceFrom(tag),
doc = docFrom(tag)
doc = docFrom(tag),
aliases = aliasFrom(tag)
)
}
19 changes: 16 additions & 3 deletions modules/generic/src/main/scala-3/vulcan/generic/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ package object generic {
.getOrElse(caseClass.typeInfo.owner),
doc = caseClass.annotations.collectFirst {
case AvroDoc(doc) => doc
}
},
aliases = caseClass.annotations.collectFirst {
case AvroAlias(alias) => alias
}.toList
) { (f: Codec.FieldBuilder[A]) =>
val nullDefaultBase = caseClass.annotations
.collectFirst { case AvroNullDefault(enabled) => enabled }
Expand Down Expand Up @@ -68,6 +71,9 @@ package object generic {
doc = param.annotations.collectFirst {
case AvroDoc(doc) => doc
},
aliases = param.annotations.collectFirst {
case AvroAlias(alias) => alias
}.toList,
default = param.default.orElse(
Option.when(codec.schema.exists(_.isNullable) && nullDefaultField)(
None.asInstanceOf[param.PType] // TODO: remove cast
Expand Down Expand Up @@ -112,7 +118,8 @@ package object generic {
encode = encode,
decode = decode,
namespace = namespaceOf[A],
doc = docOf[A]
doc = docOf[A],
aliases = aliasOf[A]
)

/**
Expand All @@ -133,7 +140,8 @@ package object generic {
encode = encode,
decode = decode,
namespace = namespaceOf[A],
doc = docOf[A]
doc = docOf[A],
aliases = aliasOf[A]
)


Expand All @@ -151,4 +159,9 @@ package object generic {
case a: Annotation[AvroDoc, A] => Some(a().doc)
case _ => None
}

private inline def aliasOf[A]: Seq[String] = summonFrom {
case a: Annotation[AvroAlias, A] => Seq(a().alias)
case _ => Seq()
}
}
34 changes: 34 additions & 0 deletions modules/generic/src/main/scala/vulcan/generic/AvroAlias.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2019-2025 OVO Energy Limited
*
* SPDX-License-Identifier: Apache-2.0
*/

package vulcan.generic

import scala.annotation.StaticAnnotation

/**
* Annotation which can be used to include the record alias
* in derived schemas.
*
* The annotation can be used in the following situations.<br>
* - Annotate a type for enum alias when using
* [[deriveEnum]].<br>
* - Annotate a type for fixed alias when using
* [[deriveFixed]].<br>
* - Annotate a `case class` for record alias
* when using `Codec.derive` from the generic module.<br>
* - Annotate a `case class` parameter for record field
* alias when using `Codec.derive` from the
* generic module.
*/
final class AvroAlias(final val alias: String) extends StaticAnnotation {
override final def toString: String =
s"AvroAlias($alias)"
}

private[vulcan] object AvroAlias {
final def unapply(avroAlias: AvroAlias): Some[String] =
Some(avroAlias.alias)
}
34 changes: 34 additions & 0 deletions modules/generic/src/test/scala/vulcan/generic/AvroAliasSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2019-2024 OVO Energy Limited
*
* SPDX-License-Identifier: Apache-2.0
*/

package vulcan.generic

import vulcan.BaseSpec

final class AvroAliasSpec extends BaseSpec {
describe("AvroAlias") {
it("should provide alias via alias") {
forAll { (s: String) =>
assert(new AvroAlias(s).alias == s)
}
}

it("should include alias in toString") {
forAll { (s: String) =>
assert(new AvroAlias(s).toString.contains(s))
}
}

it("should provide an extractor for alias") {
forAll { (s1: String) =>
assert(new AvroAlias(s1) match {
case AvroAlias(`s1`) => true
case AvroAlias(s2) => fail(s2)
})
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ final class DerivationSpec extends BaseSpec with RoundtripHelpers with CodecSpec
}
}

it("should use alias annotation") {
assertSchemaIs[FixedAvroAlias] {
"""{"type":"fixed","name":"FixedAvroAlias","namespace":"vulcan.generic.examples","size":1,"aliases":["FixedOtherAlias"]}"""
}
}

it("should use namespace annotation") {
assertSchemaIs[FixedNamespace] {
"""{"type":"fixed","name":"FixedNamespace","namespace":"vulcan.generic.examples.overridden","doc":"Some documentation","size":1}"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ final class GenericDerivationCodecSpec extends CodecBase {
}
}

it("should support annotation for record alias") {
assertSchemaIs[CaseClassAvroAlias] {
"""{"type":"record","name":"CaseClassAvroAlias","namespace":"vulcan.generic.examples","fields":[{"name":"value","type":["null","string"],"aliases":["otherValueAlias"]}],"aliases":["CaseClassOtherAlias"]}"""
}
}

it("should capture errors on invalid names") {
assertSchemaError[CaseClassFieldInvalidName] {
"""org.apache.avro.SchemaParseException: Illegal initial character: -value"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2019-2024 OVO Energy Limited
*
* SPDX-License-Identifier: Apache-2.0
*/

package vulcan.generic.examples

import vulcan.Codec
import vulcan.generic._

@AvroAlias("CaseClassOtherAlias")
final case class CaseClassAvroAlias(
@AvroAlias("otherValueAlias")
value: Option[String]
)

object CaseClassAvroAlias {
implicit val codec: Codec[CaseClassAvroAlias] = Codec.derive
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright 2019-2024 OVO Energy Limited
*
* SPDX-License-Identifier: Apache-2.0
*/

package vulcan.generic.examples

import vulcan.Codec
import vulcan.generic._

@AvroAlias("FixedOtherAlias")
final case class FixedAvroAlias(bytes: Array[Byte])

object FixedAvroAlias {
implicit val codec: Codec[FixedAvroAlias] =
deriveFixed(
size = 1,
encode = _.bytes,
decode = bytes => Right(apply(bytes))
)
}

0 comments on commit bb175bb

Please sign in to comment.