support default values for case class fields
It seems like this is all that's required to support defaults for case class fields in Scala 3.
Users need -Yretain-trees in their scalacOptions as per the documentation:
https://github.com/softwaremill/magnolia#limitations
I also bumped sbt as it crashes otherwise on recent OpenJDK versions.
This is great! I can't get my test to pass though...
/*
* 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(
a: Int = 1,
b: String = "foo",
)
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(
a: Int
)
object InvalidDefault {
implicit val codec: Codec[InvalidDefault] = Codec.derive
}
assertSchemaError[InvalidDefault]
}
it("should fail when annotating an Option") {
case class InvalidDefault2(
a: Option[String] = Some("foo")
)
object InvalidDefault2 {
implicit val codec: Codec[InvalidDefault2] = Codec.derive
}
assertSchemaError[InvalidDefault2]
}
it("should succeed when annotating an enum first element") {
case class HasSFirst(
s: Enum = Enum.A
)
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(
s: Enum = Enum.B
)
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(
u: Union = Union.A(1)
)
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(
u: Union = Union.B("foo")
)
object HasUnionSecond {
implicit val codec: Codec[HasUnionSecond] = Codec.derive
}
assertSchemaError[HasUnionSecond]
}
}
}
Did you compile with the -Yretain-trees compiler flag?
Oh, I had added it to Scala 3 only. I've made the same change for Scala 2 now, for the poor sods stuck on the legacy version 😉
I've stolen one of your tests to add it to your PR. Seems to work if you enable the -Yretain-trees compiler flag!
I was sure it was enabled 🤔 Thanks for checking
@soujiro32167 So does it work for you now?
I've discovered some fascinating stuff:
it("should fail when annotating an Option") {
case class InvalidDefault2(
a: Option[String] = Some("foo")
)
object InvalidDefault2 {
implicit val codec: Codec[InvalidDefault2] = Codec.derive
}
assertSchemaError[InvalidDefault2]
}
results in a stack overflow error:
An exception or error caused a run to abort.
java.lang.StackOverflowError
at vulcan.Codec$OptionCodec.<init>(Codec.scala:1017)
at vulcan.Codec$.option(Codec.scala:995)
at vulcan.generic.AvroFieldDefaultSpec$InvalidDefault2$2$.<init>(AvroFieldDefaultSpec.scala:64)
at vulcan.generic.AvroFieldDefaultSpec.InvalidDefault2$lzycompute$1(AvroFieldDefaultSpec.scala:63)
at vulcan.generic.AvroFieldDefaultSpec.vulcan$generic$AvroFieldDefaultSpec$$InvalidDefault2$3(AvroFieldDefaultSpec.scala:63)
at vulcan.generic.AvroFieldDefaultSpec$InvalidDefault2$2$.$anonfun$codec$19(AvroFieldDefaultSpec.scala:64)
while
case class InvalidDefault2(
a: Option[String] = Some("foo")
)
object InvalidDefault2 {
implicit val codec: Codec[InvalidDefault2] = Codec.derive
}
final class AvroFieldDefaultSpec extends CodecBase {
describe("AvroFieldDefault") {
it("should create a schema with a default for a field") {
assert(Foo.codec.schema.exists(_.getField("a").defaultVal() == 1))
assert(Foo.codec.schema.exists(_.getField("b").defaultVal() == "foo"))
}
it("should fail when annotating an Option") {
assertSchemaError[InvalidDefault2]
}
works just fine 🤯
Added a PR with all the tests https://github.com/mberndt123/vulcan/pull/1
@bplommer Any chance this could be merged?
Since this project appears to be unmaintained, we've decided to use avro4s instead 🤷🏻♂️
Hi, I just realised that https://github.com/fd4s/vulcan/pull/579 aims at tackling the same problem. @bplommer Is it possible to give this PR a review ?
@mberndt123 could you merge https://github.com/mberndt123/vulcan/pull/1 into your repo? In case this PR gets reviewed
@soujiro32167 would you be able to make a PR later to add https://github.com/mberndt123/vulcan/pull/1 directly here ?
@ayoub-benali here we go https://github.com/fd4s/vulcan/pull/593