Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding support for Nullable and withDropNoneValues to the configuration to match scala3 configured derivation #335

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import sbtcrossproject.{ CrossType, crossProject }

val Scala212V = "2.12.18"
val Scala213V = "2.13.7"
val Scala213V = "2.13.12"

val circeVersion = "0.14.6"
val circeVersion = "0.14.6-33-54d01da-SNAPSHOT"
val paradiseVersion = "2.1.1"

val jawnVersion = "1.5.1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ class ConfigurableDeriver(val c: whitebox.Context)
protected[this] override def encodeMethodArgs: List[Tree] = List(
q"transformMemberNames: (_root_.java.lang.String => _root_.java.lang.String)",
q"transformConstructorNames: (_root_.java.lang.String => _root_.java.lang.String)",
q"discriminator: _root_.scala.Option[_root_.java.lang.String]"
q"discriminator: _root_.scala.Option[_root_.java.lang.String]",
q"dropNoneValues: _root_.scala.Boolean"
)

protected[this] def decodeField(name: String, decode: TermName): Tree = q"""
Expand Down Expand Up @@ -87,8 +88,17 @@ class ConfigurableDeriver(val c: whitebox.Context)
)
"""

protected[this] def encodeField(name: String, encode: TermName, value: TermName): Tree =
q"(transformMemberNames($name), $encode($value))"
protected[this] def encodeField(name: String, encode: TermName, value: TermName): Tree = q"""
if ($value == _root_.scala.None && dropNoneValues) {
_root_.scala.None
} else if ($value == _root_.io.circe.Nullable.Undefined) {
_root_.scala.None
} else if ($value == _root_.io.circe.Nullable.Null) {
_root_.scala.Some(_root_.scala.Tuple2(transformMemberNames($name), _root_.io.circe.Json.Null))
} else {
_root_.scala.Some(_root_.scala.Tuple2(transformMemberNames($name), $encode($value)))
}
"""

protected[this] def encodeSubtype(name: String, encode: TermName, value: TermName): Tree =
q"addDiscriminator($encode, $value, transformConstructorNames($name), discriminator)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ final case class Configuration(
transformConstructorNames: String => String,
useDefaults: Boolean,
discriminator: Option[String],
strictDecoding: Boolean = false
strictDecoding: Boolean = false,
dropNoneValues: Boolean = false
) {
def withSnakeCaseMemberNames: Configuration = copy(
transformMemberNames = Configuration.snakeCaseTransformation
Expand Down Expand Up @@ -62,6 +63,10 @@ final case class Configuration(
def withDiscriminator(discriminator: String): Configuration = copy(discriminator = Some(discriminator))

def withStrictDecoding: Configuration = copy(strictDecoding = true)

def withDropNoneValues: Configuration = copy(dropNoneValues = true)
def withoutDropNoneValues: Configuration = copy(dropNoneValues = false)

}

object Configuration {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ object ReprAsObjectCodec {
def configuredEncodeObject(a: HNil)(
transformMemberNames: String => String,
transformDiscriminator: String => String,
discriminator: Option[String]
discriminator: Option[String],
dropNoneValues: Boolean
): JsonObject = JsonObject.empty
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ object ConfiguredAsObjectEncoder {
encode.value.configuredEncodeObject(gen.to(a))(
transformMemberName,
constructorNameTransformer,
None
None,
config.dropNoneValues
)
}

Expand All @@ -86,7 +87,8 @@ object ConfiguredAsObjectEncoder {
encode.value.configuredEncodeObject(gen.to(a))(
Predef.identity,
constructorNameTransformer,
config.discriminator
config.discriminator,
config.dropNoneValues
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ trait ReprAsObjectEncoder[A] extends Encoder.AsObject[A] {
def configuredEncodeObject(a: A)(
transformMemberNames: String => String,
transformDiscriminator: String => String,
discriminator: Option[String]
discriminator: Option[String],
dropNoneValues: Boolean
): JsonObject

final protected[this] def addDiscriminator[B](
Expand All @@ -37,7 +38,8 @@ trait ReprAsObjectEncoder[A] extends Encoder.AsObject[A] {
}
}

final def encodeObject(a: A): JsonObject = configuredEncodeObject(a)(Predef.identity, Predef.identity, None)
final def encodeObject(a: A): JsonObject =
configuredEncodeObject(a)(Predef.identity, Predef.identity, None, dropNoneValues = false)
}

object ReprAsObjectEncoder {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package io.circe.generic.extras

import cats.data.Validated
import cats.kernel.Eq
import io.circe.{ Decoder, DecodingFailure, Encoder, Json }
import io.circe.{ Decoder, DecodingFailure, Encoder, Json, JsonObject }
import io.circe.CursorOp.DownField
import io.circe.generic.extras.auto._
import io.circe.literal._
Expand Down Expand Up @@ -259,6 +259,142 @@ class ConfiguredAutoDerivedSuite extends CirceSuite {
}
}

property("Configuration#dropNoneValues should drop None values from the JSON AST") {
import io.circe.Nullable
import io.circe.syntax._

implicit def arbitraryNullable[A](implicit A: Arbitrary[A]): Arbitrary[Nullable[A]] =
Arbitrary[Nullable[A]](
A.arbitrary.flatMap { a =>
implicitly[Arbitrary[Int]].arbitrary.map {
case byte if byte % 3 == 0 =>
Nullable.Null: Nullable[A]
case byte if byte % 3 == 1 =>
Nullable.Undefined: Nullable[A]
case _ =>
Nullable.Value(a): Nullable[A]
}
}
)

case class ExampleInner(innerField: String, innerOptionalField: Option[String], innerNullable: Nullable[String])

object ExampleInner {
implicit val eqExampleInner: Eq[ExampleInner] = Eq.fromUniversalEquals
val genExampleInner: Gen[ExampleInner] = for {
thisIsAField <- arbitrary[String]
innerOptionalField <- arbitrary[Option[String]]
innerNullable <- arbitrary[Nullable[String]]
} yield ExampleInner(thisIsAField, innerOptionalField, innerNullable)
implicit val arbitraryExampleFoo: Arbitrary[ExampleInner] = Arbitrary(genExampleInner)
}

case class ExampleFoo(
thisIsAField: String,
optionalField: Option[String],
optionalObjectField: Option[ExampleInner],
nullableField: Nullable[String],
nullableObjectField: Nullable[ExampleInner]
)

object ExampleFoo {
implicit val eqExampleFoo: Eq[ExampleFoo] = Eq.fromUniversalEquals
val genExampleFoo: Gen[ExampleFoo] = for {
thisIsAField <- arbitrary[String]
optionalField <- arbitrary[Option[String]]
optionalObjectField <- arbitrary[Option[ExampleInner]]
nullableField <- arbitrary[Nullable[String]]
nullableObjectField <- arbitrary[Nullable[ExampleInner]]
} yield ExampleFoo(thisIsAField, optionalField, optionalObjectField, nullableField, nullableObjectField)
implicit val arbitraryExampleFoo: Arbitrary[ExampleFoo] = Arbitrary(genExampleFoo)
}

def optFields(fields: (String, Option[Json])*): Json = {
import io.circe.syntax._
JsonObject
.fromIterable(
fields.collect { case (key, Some(value)) =>
(key, value)
}
)
.asJson
}

// first, without dropNoneValues to be sure it's off by default
forAll { foo: ExampleFoo =>
implicit val dropNoneValuesConfig: Configuration =
Configuration.default

def buildInner(inner: ExampleInner): Json =
optFields(
"innerField" -> inner.innerField.asJson.some,
"innerOptionalField" -> inner.innerOptionalField.map(_.asJson).orElse(Json.Null.some),
"innerNullable" -> inner.innerNullable.fold(
none[Json],
Json.Null.some,
x => x.asJson.some
)
)

val json =
optFields(
"thisIsAField" -> foo.thisIsAField.asJson.some,
"optionalField" -> foo.optionalField.map(_.asJson).orElse(Json.Null.some),
"optionalObjectField" -> foo.optionalObjectField.map(buildInner).orElse(Json.Null.some),
"nullableField" -> foo.nullableField.fold(
none[Json],
Json.Null.some,
x => x.asJson.some
),
"nullableObjectField" -> foo.nullableObjectField.fold(
none[Json],
Json.Null.some,
inner => buildInner(inner).some
)
)

assertEquals(Encoder[ExampleFoo].apply(foo), json)
assertEquals(Decoder[ExampleFoo].decodeJson(json), Right(foo))
}

// first, withDropNoneValues
forAll { foo: ExampleFoo =>
implicit val dropNoneValuesConfig: Configuration =
Configuration.default.withDropNoneValues

def buildInner(inner: ExampleInner): Json =
optFields(
"innerField" -> inner.innerField.asJson.some,
"innerOptionalField" -> inner.innerOptionalField.map(_.asJson),
"innerNullable" -> inner.innerNullable.fold(
none[Json],
Json.Null.some,
x => x.asJson.some
)
)

val json =
optFields(
"thisIsAField" -> foo.thisIsAField.asJson.some,
"optionalField" -> foo.optionalField.map(_.asJson),
"optionalObjectField" -> foo.optionalObjectField.map(buildInner),
"nullableField" -> foo.nullableField.fold(
none[Json],
Json.Null.some,
x => x.asJson.some
),
"nullableObjectField" -> foo.nullableObjectField.fold(
none[Json],
Json.Null.some,
inner => buildInner(inner).some
)
)

assertEquals(Encoder[ExampleFoo].apply(foo), json)
assertEquals(Decoder[ExampleFoo].decodeJson(json), Right(foo))
}
}

property("Configuration options should work together") {
forAll { (f: String, b: Double) =>
implicit val customConfig: Configuration =
Expand Down
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.9.7
sbt.version=1.9.8