Skip to content

Commit

Permalink
Closes fd4s#525
Browse files Browse the repository at this point in the history
  • Loading branch information
soujiro32167 committed May 24, 2023
1 parent 0018130 commit ad7c916
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 3 deletions.
12 changes: 10 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 @@ -89,6 +89,13 @@ package object generic {
case AvroName(newName) => newName
}

def defaultValue =
param.annotations
.collectFirst {
case AvroFieldDefault(None) => Some(None)
case AvroFieldDefault(default) => default
}

implicit val codec = param.typeclass

f(
Expand All @@ -97,8 +104,9 @@ package object generic {
doc = param.annotations.collectFirst {
case AvroDoc(doc) => doc
},
default = (if (codec.schema.exists(_.isNullable) && nullDefaultField) Some(None)
else None).asInstanceOf[Option[param.PType]] // TODO: remove cast
default =
(if (codec.schema.exists(_.isNullable) && nullDefaultField) Some(None)
else defaultValue).asInstanceOf[Option[param.PType]] // TODO: remove cast
).widen
}
.map(caseClass.rawConstruct(_))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ package object generic {
case AvroName(newName) => newName
}

def defaultValue =
param.annotations
.collectFirst {
case AvroFieldDefault(default) => default
}

implicit val codec = param.typeclass

f(
Expand All @@ -69,7 +75,7 @@ package object generic {
case AvroDoc(doc) => doc
},
default = (if (codec.schema.exists(_.isNullable) && nullDefaultField) Some(None)
else None).asInstanceOf[Option[param.PType]] // TODO: remove cast
else defaultValue).asInstanceOf[Option[param.PType]] // TODO: remove cast
).widen
}
.map(caseClass.rawConstruct(_))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2019-2023 OVO Energy Limited
*
* SPDX-License-Identifier: Apache-2.0
*/

package vulcan.generic

import scala.annotation.StaticAnnotation

/**
* Annotation which can be used to set a default value for an Avro field.
*
* The annotation can be used in the following situations.<br>
* - Annotate a field in a case class using `Codec.derive`
*
* @see https://avro.apache.org/docs/1.8.1/spec.html#Unions
*
* (Note that when a default value is specified for a record field whose type is a union,
* the type of the default value must match the first element of the union.
* Thus, for unions containing "null", the "null" is usually listed first, since the default value of such unions is typically null.)
*/
final class AvroFieldDefault[A](final val value: A) extends StaticAnnotation {
override final def toString: String =
s"AvroFieldDefault($value)"
}

private[vulcan] object AvroFieldDefault {
final def unapply[A](avroFieldDefault: AvroFieldDefault[A]): Some[A] =
Some(avroFieldDefault.value)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* Copyright 2019-2023 OVO Energy Limited
*
* SPDX-License-Identifier: Apache-2.0
*/

package vulcan.generic

import vulcan.{AvroError, Codec}

final class AvroFieldDefaultSpec extends CodecBase {

sealed trait Enum extends Product {
self =>
def value: String = self.productPrefix
}

object Enum {
case object A extends Enum

case object B extends Enum

implicit val codec: Codec[Enum] = deriveEnum(
symbols = List(A.value, B.value),
encode = _.value,
decode = {
case "A" => Right(A)
case "B" => Right(B)
case other => Left(AvroError(s"Invalid S: $other"))
}
)
}

sealed trait Union

object Union {
case class A(a: Int) extends Union

case class B(b: String) extends Union

implicit val codec: Codec[Union] = Codec.derive
}

describe("AvroFieldDefault") {
it("should create a schema with a default for a field") {
case class Foo(
@AvroFieldDefault(1) a: Int,
@AvroFieldDefault("foo") b: String,
)

object Foo {
implicit val codec: Codec[Foo] = Codec.derive
}

assert(Foo.codec.schema.exists(_.getField("a").defaultVal() == 1))
assert(Foo.codec.schema.exists(_.getField("b").defaultVal() == "foo"))
}

it("should fail when the default value is not of the correct type") {
case class InvalidDefault(
@AvroFieldDefault("foo") a: Int
)
object InvalidDefault {
implicit val codec: Codec[InvalidDefault] = Codec.derive
}

assertSchemaError[InvalidDefault]
}

it("should fail when annotating an Option") {
case class InvalidDefault2(
@AvroFieldDefault(Some("foo")) a: Option[String]
)
object InvalidDefault2 {
implicit val codec: Codec[InvalidDefault2] = Codec.derive
}

assertSchemaError[InvalidDefault2]
}

it("should succeed when annotating an enum first element") {
case class HasSFirst(
@AvroFieldDefault(Enum.A) s: Enum
)
object HasSFirst {
implicit val codec: Codec[HasSFirst] = Codec.derive
}

assert(HasSFirst.codec.schema.exists(_.getField("s").defaultVal() == "A"))
}

it("should succeed when annotating an enum second element") {
case class HasSSecond(
@AvroFieldDefault(Enum.B) s: Enum
)
object HasSSecond {
implicit val codec: Codec[HasSSecond] = Codec.derive
}

assert(HasSSecond.codec.schema.exists(_.getField("s").defaultVal() == "B"))
}

it("should succeed with the first member of a union"){
case class HasUnion(
@AvroFieldDefault(Union.A(1)) u: Union
)
object HasUnion {
implicit val codec: Codec[HasUnion] = Codec.derive
}

case class Empty()
object Empty {
implicit val codec: Codec[Empty] = Codec.derive
}

assertSchemaIs[HasUnion](
"""{"type":"record","name":"HasUnion","namespace":"vulcan.generic.AvroFieldDefaultSpec.<local AvroFieldDefaultSpec>","fields":[{"name":"u","type":[{"type":"record","name":"A","namespace":"vulcan.generic.AvroFieldDefaultSpec.Union","fields":[{"name":"a","type":"int"}]},{"type":"record","name":"B","namespace":"vulcan.generic.AvroFieldDefaultSpec.Union","fields":[{"name":"b","type":"string"}]}],"default":{"a":1}}]}"""
)

val result = unsafeDecode[HasUnion](unsafeEncode[Empty](Empty()))

assert(result == HasUnion(Union.A(1)))

}

it("should fail with the second member of a union"){
case class HasUnionSecond(
@AvroFieldDefault(Union.B("foo")) u: Union
)
object HasUnionSecond {
implicit val codec: Codec[HasUnionSecond] = Codec.derive
}

assertSchemaError[HasUnionSecond]
}
}
}
3 changes: 3 additions & 0 deletions modules/generic/src/test/scala/vulcan/generic/CodecBase.scala
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ class CodecBase extends AnyFunSpec with ScalaCheckPropertyChecks with EitherValu
)(implicit codec: Codec[A]): Assertion =
assert(codec.schema.swap.value.message == expectedErrorMessage)

def assertSchemaError[A](implicit codec: Codec[A]): Assertion =
assert(codec.schema.isLeft)

def assertDecodeError[A](
value: Any,
schema: Schema,
Expand Down

0 comments on commit ad7c916

Please sign in to comment.