From 9e717bde0cac4124e5b4bc2aa0d2fad5e2f18d6a Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Sat, 13 Jan 2024 20:16:34 +0100 Subject: [PATCH 01/12] support defaults --- modules/generic/src/main/scala-3/vulcan/generic/package.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/generic/src/main/scala-3/vulcan/generic/package.scala b/modules/generic/src/main/scala-3/vulcan/generic/package.scala index ed59ef38..65aea568 100644 --- a/modules/generic/src/main/scala-3/vulcan/generic/package.scala +++ b/modules/generic/src/main/scala-3/vulcan/generic/package.scala @@ -68,8 +68,8 @@ 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 = param.default.orElse(if (codec.schema.exists(_.isNullable) && nullDefaultField) Some(None.asInstanceOf[param.PType]) // TODO: remove cast + else None) ).widen } .map(caseClass.rawConstruct(_)) From 88e97eb2997f3a470f0c37ea708d173de66bcb7a Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Sat, 13 Jan 2024 20:25:08 +0100 Subject: [PATCH 02/12] simplify --- .../generic/src/main/scala-3/vulcan/generic/package.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/generic/src/main/scala-3/vulcan/generic/package.scala b/modules/generic/src/main/scala-3/vulcan/generic/package.scala index 65aea568..2649d3aa 100644 --- a/modules/generic/src/main/scala-3/vulcan/generic/package.scala +++ b/modules/generic/src/main/scala-3/vulcan/generic/package.scala @@ -68,8 +68,11 @@ package object generic { doc = param.annotations.collectFirst { case AvroDoc(doc) => doc }, - default = param.default.orElse(if (codec.schema.exists(_.isNullable) && nullDefaultField) Some(None.asInstanceOf[param.PType]) // TODO: remove cast - else None) + default = param.default.orElse( + Option.when(codec.schema.exists(_.isNullable) && nullDefaultField)( + None.asInstanceOf[param.PType] // TODO: remove cast + ) + ) ).widen } .map(caseClass.rawConstruct(_)) From 582f86e47894b5a0438647d05bdf4be5a835a645 Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Sat, 13 Jan 2024 20:25:29 +0100 Subject: [PATCH 03/12] bump sbt --- project/build.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/build.properties b/project/build.properties index ef3d2662..0aa5c39b 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version = 1.8.3 +sbt.version = 1.9.8 From 71e3b088ba1b5a58e198775ce0016d1845191cff Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Sun, 14 Jan 2024 15:11:15 +0100 Subject: [PATCH 04/12] support defaults in Scala 2 as well --- .../generic/src/main/scala-2/vulcan/generic/package.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/generic/src/main/scala-2/vulcan/generic/package.scala b/modules/generic/src/main/scala-2/vulcan/generic/package.scala index 27b5fe0e..2461f678 100644 --- a/modules/generic/src/main/scala-2/vulcan/generic/package.scala +++ b/modules/generic/src/main/scala-2/vulcan/generic/package.scala @@ -97,8 +97,11 @@ 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 = param.default.orElse( + (if (codec.schema.exists(_.isNullable) && nullDefaultField) + Some(None.asInstanceOf[param.PType]) // TODO: remove cast + else None) + ) ).widen } .map(caseClass.rawConstruct(_)) From 74962aa14618ab8c36bcf271b0b23b55fd3c9cd2 Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Sun, 14 Jan 2024 15:21:46 +0100 Subject: [PATCH 05/12] add a test for case class field defaults --- build.sbt | 1 + .../vulcan/generic/AvroFieldDefaultSpec.scala | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 modules/generic/src/test/scala/vulcan/generic/AvroFieldDefaultSpec.scala diff --git a/build.sbt b/build.sbt index eaa66e08..b0f80ee3 100644 --- a/build.sbt +++ b/build.sbt @@ -94,6 +94,7 @@ lazy val generic = project scalaSettings ++ Seq( crossScalaVersions += scala3 ), + scalacOptions += "-Yretain-trees", testSettings ) .dependsOn(core % "compile->compile;test->test") diff --git a/modules/generic/src/test/scala/vulcan/generic/AvroFieldDefaultSpec.scala b/modules/generic/src/test/scala/vulcan/generic/AvroFieldDefaultSpec.scala new file mode 100644 index 00000000..2f8c5dcd --- /dev/null +++ b/modules/generic/src/test/scala/vulcan/generic/AvroFieldDefaultSpec.scala @@ -0,0 +1,28 @@ +/* + * Copyright 2019-2023 OVO Energy Limited + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package vulcan.generic + +import vulcan.Codec + +case class Foo( + a: Int = 1, + b: String = "foo", +) + +object Foo { + implicit val codec: Codec[Foo] = 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")) + } + } +} \ No newline at end of file From 152ebb868ea7cbe099fda7e13347ede4b1822a1b Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Sun, 14 Jan 2024 15:29:54 +0100 Subject: [PATCH 06/12] remove unnecessary parens --- .../generic/src/main/scala-2/vulcan/generic/package.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/generic/src/main/scala-2/vulcan/generic/package.scala b/modules/generic/src/main/scala-2/vulcan/generic/package.scala index 2461f678..09274bc2 100644 --- a/modules/generic/src/main/scala-2/vulcan/generic/package.scala +++ b/modules/generic/src/main/scala-2/vulcan/generic/package.scala @@ -98,9 +98,9 @@ package object generic { case AvroDoc(doc) => doc }, default = param.default.orElse( - (if (codec.schema.exists(_.isNullable) && nullDefaultField) - Some(None.asInstanceOf[param.PType]) // TODO: remove cast - else None) + if (codec.schema.exists(_.isNullable) && nullDefaultField) + Some(None.asInstanceOf[param.PType]) // TODO: remove cast + else None ) ).widen } From af3d068415a3efdd3ca3c7e219bcdea56b9ed45f Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Sun, 14 Jan 2024 15:39:39 +0100 Subject: [PATCH 07/12] fix compiler flags --- build.sbt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index b0f80ee3..d98b5350 100644 --- a/build.sbt +++ b/build.sbt @@ -94,7 +94,8 @@ lazy val generic = project scalaSettings ++ Seq( crossScalaVersions += scala3 ), - scalacOptions += "-Yretain-trees", + // magnolia requires compilation with the -Yretain-trees flag to support case class field default values on Scala 3 + Test / scalacOptions ++= (if (CrossVersion.partialVersion(scalaVersion.value).exists(_._1 == 3)) Seq("-Yretain-trees") else Nil), testSettings ) .dependsOn(core % "compile->compile;test->test") From 1e5f69908b438b54948b6312a924ed7355513cd7 Mon Sep 17 00:00:00 2001 From: Matthias Berndt Date: Sun, 14 Jan 2024 15:41:46 +0100 Subject: [PATCH 08/12] trailing newline --- .../src/test/scala/vulcan/generic/AvroFieldDefaultSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/generic/src/test/scala/vulcan/generic/AvroFieldDefaultSpec.scala b/modules/generic/src/test/scala/vulcan/generic/AvroFieldDefaultSpec.scala index 2f8c5dcd..59d959bb 100644 --- a/modules/generic/src/test/scala/vulcan/generic/AvroFieldDefaultSpec.scala +++ b/modules/generic/src/test/scala/vulcan/generic/AvroFieldDefaultSpec.scala @@ -25,4 +25,4 @@ final class AvroFieldDefaultSpec extends CodecBase { assert(Foo.codec.schema.exists(_.getField("b").defaultVal() == "foo")) } } -} \ No newline at end of file +} From 2de4f5a796be3bed63bfcea4c888137c9b8b9f91 Mon Sep 17 00:00:00 2001 From: Eli Kasik Date: Tue, 16 Jan 2024 11:37:29 -0500 Subject: [PATCH 09/12] Add comprehensive tests --- .gitignore | 3 +- .../vulcan/generic/AvroFieldDefaultSpec.scala | 99 ++++++++++++++++++- .../test/scala/vulcan/generic/CodecBase.scala | 4 + 3 files changed, 103 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 0b4d3394..35140cfc 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ target/ .metals/ .vscode/ .bloop/ -metals.sbt \ No newline at end of file +metals.sbt +.idea/ \ No newline at end of file diff --git a/modules/generic/src/test/scala/vulcan/generic/AvroFieldDefaultSpec.scala b/modules/generic/src/test/scala/vulcan/generic/AvroFieldDefaultSpec.scala index 59d959bb..f44f6160 100644 --- a/modules/generic/src/test/scala/vulcan/generic/AvroFieldDefaultSpec.scala +++ b/modules/generic/src/test/scala/vulcan/generic/AvroFieldDefaultSpec.scala @@ -6,7 +6,39 @@ package vulcan.generic -import vulcan.Codec +import vulcan.{AvroError, Codec} + + +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 +} case class Foo( a: Int = 1, @@ -17,12 +49,75 @@ object Foo { implicit val codec: Codec[Foo] = Codec.derive } +case class InvalidDefault2( + a: Option[String] = Some("foo") +) +object InvalidDefault2 { + implicit val codec: Codec[InvalidDefault2] = Codec.derive +} + +case class HasSFirst( + s: Enum = Enum.A +) +object HasSFirst { + implicit val codec: Codec[HasSFirst] = Codec.derive +} + +case class HasSSecond( + s: Enum = Enum.B +) +object HasSSecond { + implicit val codec: Codec[HasSSecond] = Codec.derive +} + +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 +} + +case class HasUnionSecond( + u: Union = Union.B("foo") +) +object HasUnionSecond { + implicit val codec: Codec[HasUnionSecond] = 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] + } + + it("should succeed when annotating an enum first element") { + assert(HasSFirst.codec.schema.exists(_.getField("s").defaultVal() == "A")) + } + + it("should succeed when annotating an enum second element") { + assert(HasSSecond.codec.schema.exists(_.getField("s").defaultVal() == "B")) + } + + it("should succeed with the first member of a union"){ + assertSchemaIs[HasUnion]( + """{"type":"record","name":"HasUnion","namespace":"vulcan.generic","fields":[{"name":"u","type":[{"type":"record","name":"A","namespace":"vulcan.generic.Union","fields":[{"name":"a","type":"int"}]},{"type":"record","name":"B","namespace":"vulcan.generic.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"){ + assertSchemaError[HasUnionSecond] + } } } diff --git a/modules/generic/src/test/scala/vulcan/generic/CodecBase.scala b/modules/generic/src/test/scala/vulcan/generic/CodecBase.scala index 3b66e529..1b4122ea 100644 --- a/modules/generic/src/test/scala/vulcan/generic/CodecBase.scala +++ b/modules/generic/src/test/scala/vulcan/generic/CodecBase.scala @@ -51,6 +51,10 @@ 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, codec.schema) + def assertDecodeError[A]( value: Any, schema: Schema, From b8e69da196c5b54b883664ec6c3d340f9ca7b8e0 Mon Sep 17 00:00:00 2001 From: Eli Kasik Date: Mon, 10 Jun 2024 11:56:15 -0400 Subject: [PATCH 10/12] fmt --- .../scala/vulcan/generic/AvroFieldDefaultSpec.scala | 11 +++++------ .../src/test/scala/vulcan/generic/CodecBase.scala | 1 - 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/modules/generic/src/test/scala/vulcan/generic/AvroFieldDefaultSpec.scala b/modules/generic/src/test/scala/vulcan/generic/AvroFieldDefaultSpec.scala index f44f6160..c6bf216f 100644 --- a/modules/generic/src/test/scala/vulcan/generic/AvroFieldDefaultSpec.scala +++ b/modules/generic/src/test/scala/vulcan/generic/AvroFieldDefaultSpec.scala @@ -8,7 +8,6 @@ package vulcan.generic import vulcan.{AvroError, Codec} - sealed trait Enum extends Product { self => def value: String = self.productPrefix @@ -23,8 +22,8 @@ object Enum { symbols = List(A.value, B.value), encode = _.value, decode = { - case "A" => Right(A) - case "B" => Right(B) + case "A" => Right(A) + case "B" => Right(B) case other => Left(AvroError(s"Invalid S: $other")) } ) @@ -42,7 +41,7 @@ object Union { case class Foo( a: Int = 1, - b: String = "foo", + b: String = "foo" ) object Foo { @@ -108,7 +107,7 @@ final class AvroFieldDefaultSpec extends CodecBase { assert(HasSSecond.codec.schema.exists(_.getField("s").defaultVal() == "B")) } - it("should succeed with the first member of a union"){ + it("should succeed with the first member of a union") { assertSchemaIs[HasUnion]( """{"type":"record","name":"HasUnion","namespace":"vulcan.generic","fields":[{"name":"u","type":[{"type":"record","name":"A","namespace":"vulcan.generic.Union","fields":[{"name":"a","type":"int"}]},{"type":"record","name":"B","namespace":"vulcan.generic.Union","fields":[{"name":"b","type":"string"}]}],"default":{"a":1}}]}""" ) @@ -116,7 +115,7 @@ final class AvroFieldDefaultSpec extends CodecBase { assert(result == HasUnion(Union.A(1))) } - it("should fail with the second member of a union"){ + it("should fail with the second member of a union") { assertSchemaError[HasUnionSecond] } } diff --git a/modules/generic/src/test/scala/vulcan/generic/CodecBase.scala b/modules/generic/src/test/scala/vulcan/generic/CodecBase.scala index 1b4122ea..c96ad6db 100644 --- a/modules/generic/src/test/scala/vulcan/generic/CodecBase.scala +++ b/modules/generic/src/test/scala/vulcan/generic/CodecBase.scala @@ -51,7 +51,6 @@ 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, codec.schema) From afb81bb02fccaad8fbb32942d2dd6cb4470c4d3b Mon Sep 17 00:00:00 2001 From: Eli Kasik Date: Mon, 10 Jun 2024 11:59:46 -0400 Subject: [PATCH 11/12] fmt 2 --- build.sbt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index d98b5350..be52a454 100644 --- a/build.sbt +++ b/build.sbt @@ -95,7 +95,9 @@ lazy val generic = project crossScalaVersions += scala3 ), // magnolia requires compilation with the -Yretain-trees flag to support case class field default values on Scala 3 - Test / scalacOptions ++= (if (CrossVersion.partialVersion(scalaVersion.value).exists(_._1 == 3)) Seq("-Yretain-trees") else Nil), + Test / scalacOptions ++= (if (CrossVersion.partialVersion(scalaVersion.value).exists(_._1 == 3)) + Seq("-Yretain-trees") + else Nil), testSettings ) .dependsOn(core % "compile->compile;test->test") From f21a876de2e9d0c6a44a1c262d3154f282527847 Mon Sep 17 00:00:00 2001 From: Eli Kasik Date: Mon, 10 Jun 2024 13:32:27 -0400 Subject: [PATCH 12/12] PR comments --- .../vulcan/generic/AvroFieldDefaultSpec.scala | 86 +----------------- .../generic/examples/AvroRecordDefault.scala | 89 +++++++++++++++++++ 2 files changed, 93 insertions(+), 82 deletions(-) create mode 100644 modules/generic/src/test/scala/vulcan/generic/examples/AvroRecordDefault.scala diff --git a/modules/generic/src/test/scala/vulcan/generic/AvroFieldDefaultSpec.scala b/modules/generic/src/test/scala/vulcan/generic/AvroFieldDefaultSpec.scala index c6bf216f..04a2b996 100644 --- a/modules/generic/src/test/scala/vulcan/generic/AvroFieldDefaultSpec.scala +++ b/modules/generic/src/test/scala/vulcan/generic/AvroFieldDefaultSpec.scala @@ -6,93 +6,15 @@ package vulcan.generic -import vulcan.{AvroError, Codec} - -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 -} - -case class Foo( - a: Int = 1, - b: String = "foo" -) - -object Foo { - implicit val codec: Codec[Foo] = Codec.derive -} - -case class InvalidDefault2( - a: Option[String] = Some("foo") -) -object InvalidDefault2 { - implicit val codec: Codec[InvalidDefault2] = Codec.derive -} - -case class HasSFirst( - s: Enum = Enum.A -) -object HasSFirst { - implicit val codec: Codec[HasSFirst] = Codec.derive -} - -case class HasSSecond( - s: Enum = Enum.B -) -object HasSSecond { - implicit val codec: Codec[HasSSecond] = Codec.derive -} - -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 -} - -case class HasUnionSecond( - u: Union = Union.B("foo") -) -object HasUnionSecond { - implicit val codec: Codec[HasUnionSecond] = Codec.derive -} +import examples.AvroRecordDefault._ +import org.apache.avro.JsonProperties 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")) + assert(Foo.codec.schema.exists(_.getField("c").defaultVal() == JsonProperties.NULL_VALUE)) } it("should fail when annotating an Option") { @@ -109,7 +31,7 @@ final class AvroFieldDefaultSpec extends CodecBase { it("should succeed with the first member of a union") { assertSchemaIs[HasUnion]( - """{"type":"record","name":"HasUnion","namespace":"vulcan.generic","fields":[{"name":"u","type":[{"type":"record","name":"A","namespace":"vulcan.generic.Union","fields":[{"name":"a","type":"int"}]},{"type":"record","name":"B","namespace":"vulcan.generic.Union","fields":[{"name":"b","type":"string"}]}],"default":{"a":1}}]}""" + """{"type":"record","name":"HasUnion","namespace":"vulcan.generic.examples.AvroRecordDefault","fields":[{"name":"u","type":[{"type":"record","name":"A","namespace":"vulcan.generic.examples.AvroRecordDefault.Union","fields":[{"name":"a","type":"int"}]},{"type":"record","name":"B","namespace":"vulcan.generic.examples.AvroRecordDefault.Union","fields":[{"name":"b","type":"string"}]}],"default":{"a":1}}]}""" ) val result = unsafeDecode[HasUnion](unsafeEncode[Empty](Empty())) assert(result == HasUnion(Union.A(1))) diff --git a/modules/generic/src/test/scala/vulcan/generic/examples/AvroRecordDefault.scala b/modules/generic/src/test/scala/vulcan/generic/examples/AvroRecordDefault.scala new file mode 100644 index 00000000..5bb29e3d --- /dev/null +++ b/modules/generic/src/test/scala/vulcan/generic/examples/AvroRecordDefault.scala @@ -0,0 +1,89 @@ +package vulcan.generic.examples + +import vulcan.{AvroError, Codec} +import vulcan.generic._ + +object AvroRecordDefault { + 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 + } + + case class Foo( + a: Int = 1, + b: String = "foo", + c: Option[String] = None + ) + + object Foo { + implicit val codec: Codec[Foo] = Codec.derive + } + + case class InvalidDefault2( + a: Option[String] = Some("foo") + ) + object InvalidDefault2 { + implicit val codec: Codec[InvalidDefault2] = Codec.derive + } + + case class HasSFirst( + s: Enum = Enum.A + ) + object HasSFirst { + implicit val codec: Codec[HasSFirst] = Codec.derive + } + + case class HasSSecond( + s: Enum = Enum.B + ) + object HasSSecond { + implicit val codec: Codec[HasSSecond] = Codec.derive + } + + 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 + } + + case class HasUnionSecond( + u: Union = Union.B("foo") + ) + object HasUnionSecond { + implicit val codec: Codec[HasUnionSecond] = Codec.derive + } +} + +