diff --git a/build.sbt b/build.sbt index b48d2128..bbeb31ba 100644 --- a/build.sbt +++ b/build.sbt @@ -2,19 +2,20 @@ import sbtcrossproject.{ CrossType, crossProject } val Scala212V = "2.12.17" val Scala213V = "2.13.7" +val Scala3V = "3.2.2" -val circeVersion = "0.14.3" +val circeVersion = "0.14.5" val paradiseVersion = "2.1.1" val jawnVersion = "1.4.0" -val munitVersion = "0.7.29" -val disciplineMunitVersion = "1.0.9" +val munitVersion = "1.0.0-M6" +val disciplineMunitVersion = "2.0.0-M3" ThisBuild / tlBaseVersion := "0.14" ThisBuild / tlCiReleaseTags := false ThisBuild / organization := "io.circe" -ThisBuild / crossScalaVersions := List(Scala212V, Scala213V) +ThisBuild / crossScalaVersions := List(Scala212V, Scala213V, Scala3V) ThisBuild / scalaVersion := Scala213V ThisBuild / githubWorkflowJavaVersions := Seq("8", "17").map(JavaSpec.temurin) @@ -54,19 +55,22 @@ lazy val genericExtras = crossProject(JSPlatform, JVMPlatform, NativePlatform) .in(file("generic-extras")) .settings( moduleName := "circe-generic-extras", - libraryDependencies ++= Seq( + libraryDependencies ++= List( "io.circe" %%% "circe-generic" % circeVersion, "io.circe" %%% "circe-literal" % circeVersion % Test, "io.circe" %%% "circe-testing" % circeVersion % Test, "org.scalameta" %%% "munit" % munitVersion % Test, "org.scalameta" %%% "munit-scalacheck" % munitVersion % Test, "org.typelevel" %%% "discipline-munit" % disciplineMunitVersion % Test, - "org.typelevel" %% "jawn-parser" % jawnVersion % Test, - scalaOrganization.value % "scala-compiler" % scalaVersion.value % Provided, - scalaOrganization.value % "scala-reflect" % scalaVersion.value % Provided - ) ++ ( + "org.typelevel" %% "jawn-parser" % jawnVersion % Test + ) ++ (if (scalaBinaryVersion.value.startsWith("2")) + List( + scalaOrganization.value % "scala-compiler" % scalaVersion.value % Provided, + scalaOrganization.value % "scala-reflect" % scalaVersion.value % Provided + ) + else Nil) ++ ( if (scalaBinaryVersion.value == "2.12") { - Seq( + List( compilerPlugin(("org.scalamacros" % "paradise" % paradiseVersion).cross(CrossVersion.patch)) ) } else Nil @@ -88,7 +92,7 @@ lazy val genericExtras = crossProject(JSPlatform, JVMPlatform, NativePlatform) ) .jsSettings() .nativeSettings( - tlVersionIntroduced := List("2.12", "2.13").map(_ -> "0.14.3").toMap + tlVersionIntroduced := List("2.12", "2.13", "3").map(_ -> "0.14.3").toMap ) lazy val benchmarks = project @@ -96,9 +100,9 @@ lazy val benchmarks = project .settings( moduleName := "circe-generic-extras-benchmarks", libraryDependencies ++= List( - "io.circe" %%% "circe-parser" % circeVersion, - scalaOrganization.value % "scala-reflect" % scalaVersion.value - ) + "io.circe" %%% "circe-parser" % circeVersion + ) ++ (if(scalaBinaryVersion.value.startsWith("2")) List(scalaOrganization.value % "scala-reflect" % scalaVersion.value) + else List.empty) ) .dependsOn(genericExtras.jvm) .enablePlugins(JmhPlugin, NoPublishPlugin) diff --git a/flake.lock b/flake.lock index f8b5bf65..57546595 100644 --- a/flake.lock +++ b/flake.lock @@ -19,22 +19,6 @@ "type": "github" } }, - "flake-compat": { - "flake": false, - "locked": { - "lastModified": 1650374568, - "narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "b4a34015c698c7793d592d66adbab377907a2be8", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, "flake-utils": { "locked": { "lastModified": 1642700792, @@ -83,11 +67,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1663491030, - "narHash": "sha256-MVsfBhE9US5DvLtBAaTRjwYdv1tLO8xjahM8qLXTgTo=", + "lastModified": 1665296151, + "narHash": "sha256-uOB0oxqxN9K7XGF1hcnY+PQnlQJ+3bP2vCn/+Ru/bbc=", "owner": "nixos", "repo": "nixpkgs", - "rev": "767542707d394ff15ac1981e903e005ba69528b5", + "rev": "14ccaaedd95a488dd7ae142757884d8e125b3363", "type": "github" }, "original": { @@ -113,16 +97,15 @@ "typelevel-nix": { "inputs": { "devshell": "devshell", - "flake-compat": "flake-compat", "flake-utils": "flake-utils_2", "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1663698741, - "narHash": "sha256-dDk+/DvxQ/C1r9cyqznTB7nJzqRiF2xhDmcl7Zn8XCE=", + "lastModified": 1665404222, + "narHash": "sha256-HH0aa1RAtJojBH4e1oq5p8IGVR3bjBztKEuWAlawfRw=", "owner": "typelevel", "repo": "typelevel-nix", - "rev": "0a576f4d82a48ac8838f2a08eb327df5442da8ef", + "rev": "ea0308a79e6dba34d0698b130af9d2166e7a99c9", "type": "github" }, "original": { diff --git a/generic-extras/src/main/scala/io/circe/generic/extras/AutoDerivation.scala b/generic-extras/src/main/scala-2/io/circe/generic/extras/AutoDerivation.scala similarity index 100% rename from generic-extras/src/main/scala/io/circe/generic/extras/AutoDerivation.scala rename to generic-extras/src/main/scala-2/io/circe/generic/extras/AutoDerivation.scala diff --git a/generic-extras/src/main/scala/io/circe/generic/extras/ConfigurableDeriver.scala b/generic-extras/src/main/scala-2/io/circe/generic/extras/ConfigurableDeriver.scala similarity index 100% rename from generic-extras/src/main/scala/io/circe/generic/extras/ConfigurableDeriver.scala rename to generic-extras/src/main/scala-2/io/circe/generic/extras/ConfigurableDeriver.scala diff --git a/generic-extras/src/main/scala/io/circe/generic/extras/Configuration.scala b/generic-extras/src/main/scala-2/io/circe/generic/extras/Configuration.scala similarity index 100% rename from generic-extras/src/main/scala/io/circe/generic/extras/Configuration.scala rename to generic-extras/src/main/scala-2/io/circe/generic/extras/Configuration.scala diff --git a/generic-extras/src/main/scala/io/circe/generic/extras/codec/ConfiguredAsObjectCodec.scala b/generic-extras/src/main/scala-2/io/circe/generic/extras/ConfiguredAsObjectCodec.scala similarity index 100% rename from generic-extras/src/main/scala/io/circe/generic/extras/codec/ConfiguredAsObjectCodec.scala rename to generic-extras/src/main/scala-2/io/circe/generic/extras/ConfiguredAsObjectCodec.scala diff --git a/generic-extras/src/main/scala/io/circe/generic/extras/encoding/ConfiguredAsObjectEncoder.scala b/generic-extras/src/main/scala-2/io/circe/generic/extras/ConfiguredAsObjectEncoder.scala similarity index 100% rename from generic-extras/src/main/scala/io/circe/generic/extras/encoding/ConfiguredAsObjectEncoder.scala rename to generic-extras/src/main/scala-2/io/circe/generic/extras/ConfiguredAsObjectEncoder.scala diff --git a/generic-extras/src/main/scala/io/circe/generic/extras/decoding/ConfiguredDecoder.scala b/generic-extras/src/main/scala-2/io/circe/generic/extras/ConfiguredDecoder.scala similarity index 100% rename from generic-extras/src/main/scala/io/circe/generic/extras/decoding/ConfiguredDecoder.scala rename to generic-extras/src/main/scala-2/io/circe/generic/extras/ConfiguredDecoder.scala diff --git a/generic-extras/src/main/scala/io/circe/generic/extras/ConfiguredJsonCodec.scala b/generic-extras/src/main/scala-2/io/circe/generic/extras/ConfiguredJsonCodec.scala similarity index 100% rename from generic-extras/src/main/scala/io/circe/generic/extras/ConfiguredJsonCodec.scala rename to generic-extras/src/main/scala-2/io/circe/generic/extras/ConfiguredJsonCodec.scala diff --git a/generic-extras/src/main/scala/io/circe/generic/extras/codec/EnumerationCodec.scala b/generic-extras/src/main/scala-2/io/circe/generic/extras/EnumerationCodec.scala similarity index 100% rename from generic-extras/src/main/scala/io/circe/generic/extras/codec/EnumerationCodec.scala rename to generic-extras/src/main/scala-2/io/circe/generic/extras/EnumerationCodec.scala diff --git a/generic-extras/src/main/scala/io/circe/generic/extras/decoding/EnumerationDecoder.scala b/generic-extras/src/main/scala-2/io/circe/generic/extras/EnumerationDecoder.scala similarity index 100% rename from generic-extras/src/main/scala/io/circe/generic/extras/decoding/EnumerationDecoder.scala rename to generic-extras/src/main/scala-2/io/circe/generic/extras/EnumerationDecoder.scala diff --git a/generic-extras/src/main/scala/io/circe/generic/extras/encoding/EnumerationEncoder.scala b/generic-extras/src/main/scala-2/io/circe/generic/extras/EnumerationEncoder.scala similarity index 100% rename from generic-extras/src/main/scala/io/circe/generic/extras/encoding/EnumerationEncoder.scala rename to generic-extras/src/main/scala-2/io/circe/generic/extras/EnumerationEncoder.scala diff --git a/generic-extras/src/main/scala/io/circe/generic/extras/decoding/IncompleteConfiguredDecoders.scala b/generic-extras/src/main/scala-2/io/circe/generic/extras/IncompleteConfiguredDecoders.scala similarity index 100% rename from generic-extras/src/main/scala/io/circe/generic/extras/decoding/IncompleteConfiguredDecoders.scala rename to generic-extras/src/main/scala-2/io/circe/generic/extras/IncompleteConfiguredDecoders.scala diff --git a/generic-extras/src/main/scala/io/circe/generic/extras/util/RecordToMap.scala b/generic-extras/src/main/scala-2/io/circe/generic/extras/RecordToMap.scala similarity index 100% rename from generic-extras/src/main/scala/io/circe/generic/extras/util/RecordToMap.scala rename to generic-extras/src/main/scala-2/io/circe/generic/extras/RecordToMap.scala diff --git a/generic-extras/src/main/scala/io/circe/generic/extras/codec/ReprAsObjectCodec.scala b/generic-extras/src/main/scala-2/io/circe/generic/extras/ReprAsObjectCodec.scala similarity index 100% rename from generic-extras/src/main/scala/io/circe/generic/extras/codec/ReprAsObjectCodec.scala rename to generic-extras/src/main/scala-2/io/circe/generic/extras/ReprAsObjectCodec.scala diff --git a/generic-extras/src/main/scala/io/circe/generic/extras/encoding/ReprAsObjectEncoder.scala b/generic-extras/src/main/scala-2/io/circe/generic/extras/ReprAsObjectEncoder.scala similarity index 100% rename from generic-extras/src/main/scala/io/circe/generic/extras/encoding/ReprAsObjectEncoder.scala rename to generic-extras/src/main/scala-2/io/circe/generic/extras/ReprAsObjectEncoder.scala diff --git a/generic-extras/src/main/scala/io/circe/generic/extras/decoding/ReprDecoder.scala b/generic-extras/src/main/scala-2/io/circe/generic/extras/ReprDecoder.scala similarity index 100% rename from generic-extras/src/main/scala/io/circe/generic/extras/decoding/ReprDecoder.scala rename to generic-extras/src/main/scala-2/io/circe/generic/extras/ReprDecoder.scala diff --git a/generic-extras/src/main/scala/io/circe/generic/extras/codec/UnwrappedCodec.scala b/generic-extras/src/main/scala-2/io/circe/generic/extras/UnwrappedCodec.scala similarity index 100% rename from generic-extras/src/main/scala/io/circe/generic/extras/codec/UnwrappedCodec.scala rename to generic-extras/src/main/scala-2/io/circe/generic/extras/UnwrappedCodec.scala diff --git a/generic-extras/src/main/scala/io/circe/generic/extras/decoding/UnwrappedDecoder.scala b/generic-extras/src/main/scala-2/io/circe/generic/extras/UnwrappedDecoder.scala similarity index 100% rename from generic-extras/src/main/scala/io/circe/generic/extras/decoding/UnwrappedDecoder.scala rename to generic-extras/src/main/scala-2/io/circe/generic/extras/UnwrappedDecoder.scala diff --git a/generic-extras/src/main/scala/io/circe/generic/extras/encoding/UnwrappedEncoder.scala b/generic-extras/src/main/scala-2/io/circe/generic/extras/UnwrappedEncoder.scala similarity index 100% rename from generic-extras/src/main/scala/io/circe/generic/extras/encoding/UnwrappedEncoder.scala rename to generic-extras/src/main/scala-2/io/circe/generic/extras/UnwrappedEncoder.scala diff --git a/generic-extras/src/main/scala/io/circe/generic/extras/auto/package.scala b/generic-extras/src/main/scala-2/io/circe/generic/extras/auto/package.scala similarity index 100% rename from generic-extras/src/main/scala/io/circe/generic/extras/auto/package.scala rename to generic-extras/src/main/scala-2/io/circe/generic/extras/auto/package.scala diff --git a/generic-extras/src/main/scala/io/circe/generic/extras/encoding/package.scala b/generic-extras/src/main/scala-2/io/circe/generic/extras/encoding/package.scala similarity index 100% rename from generic-extras/src/main/scala/io/circe/generic/extras/encoding/package.scala rename to generic-extras/src/main/scala-2/io/circe/generic/extras/encoding/package.scala diff --git a/generic-extras/src/main/scala/io/circe/generic/extras/package.scala b/generic-extras/src/main/scala-2/io/circe/generic/extras/package.scala similarity index 100% rename from generic-extras/src/main/scala/io/circe/generic/extras/package.scala rename to generic-extras/src/main/scala-2/io/circe/generic/extras/package.scala diff --git a/generic-extras/src/main/scala/io/circe/generic/extras/semiauto.scala b/generic-extras/src/main/scala-2/io/circe/generic/extras/semiauto.scala similarity index 100% rename from generic-extras/src/main/scala/io/circe/generic/extras/semiauto.scala rename to generic-extras/src/main/scala-2/io/circe/generic/extras/semiauto.scala diff --git a/generic-extras/src/main/scala-3/io/circe/generic/extras/auto.scala b/generic-extras/src/main/scala-3/io/circe/generic/extras/auto.scala new file mode 100644 index 00000000..c85de512 --- /dev/null +++ b/generic-extras/src/main/scala-3/io/circe/generic/extras/auto.scala @@ -0,0 +1,21 @@ +package io.circe.generic.extras + +import io.circe.{ Decoder, Encoder } +import io.circe.`export`.Exported +import scala.deriving.Mirror + +/** + * Fully automatic codec derivation. + * + * Extending this trait provides [[io.circe.Decoder]] and [[io.circe.Encoder]] + * instances for case classes (if all members have instances), sealed + * trait hierarchies, etc. + */ +trait AutoDerivation { + implicit inline final def deriveDecoder[A](using inline A: Mirror.Of[A]): Exported[Decoder[A]] = + Exported(Decoder.derived[A]) + implicit inline final def deriveEncoder[A](using inline A: Mirror.Of[A]): Exported[Encoder.AsObject[A]] = + Exported(Encoder.AsObject.derived[A]) +} + +object auto extends AutoDerivation diff --git a/generic-extras/src/main/scala-3/io/circe/generic/extras/package.scala b/generic-extras/src/main/scala-3/io/circe/generic/extras/package.scala new file mode 100644 index 00000000..f75d08c2 --- /dev/null +++ b/generic-extras/src/main/scala-3/io/circe/generic/extras/package.scala @@ -0,0 +1,51 @@ +package io.circe.generic + +import io.circe.Codec +import java.util.regex.Pattern + +package object extras { + type ExtrasAsObjectCodec[A] = Codec.AsObject[A] with ExtrasDecoder[A] + type Configuration = io.circe.derivation.Configuration + + object Configuration { + def apply( + transformMemberNames: String => String = Predef.identity, + transformConstructorNames: String => String = Predef.identity, + useDefaults: Boolean = false, + discriminator: Option[String] = None, + strictDecoding: Boolean = false + ): Configuration = Configuration( + transformMemberNames = transformMemberNames, + transformConstructorNames = transformConstructorNames, + useDefaults = useDefaults, + discriminator = discriminator, + strictDecoding = strictDecoding + ) + + val default: Configuration = io.circe.derivation.Configuration.default + private val basePattern: Pattern = Pattern.compile("([A-Z]+)([A-Z][a-z])") + private val swapPattern: Pattern = Pattern.compile("([a-z\\d])([A-Z])") + + val snakeCaseTransformation: String => String = s => { + val partial = basePattern.matcher(s).replaceAll("$1_$2") + swapPattern.matcher(partial).replaceAll("$1_$2").toLowerCase + } + + val screamingSnakeCaseTransformation: String => String = s => { + val partial = basePattern.matcher(s).replaceAll("$1_$2") + swapPattern.matcher(partial).replaceAll("$1_$2").toUpperCase + } + + val kebabCaseTransformation: String => String = s => { + val partial = basePattern.matcher(s).replaceAll("$1-$2") + swapPattern.matcher(partial).replaceAll("$1-$2").toLowerCase + } + + val pascalCaseTransformation: String => String = s => { + s"${s.charAt(0).toUpper}${s.substring(1)}" + } + } + object defaults { + implicit val defaultGenericConfiguration: Configuration = Configuration.default + } +} diff --git a/generic-extras/src/main/scala-3/io/circe/generic/extras/semiauto.scala b/generic-extras/src/main/scala-3/io/circe/generic/extras/semiauto.scala new file mode 100644 index 00000000..4676de2a --- /dev/null +++ b/generic-extras/src/main/scala-3/io/circe/generic/extras/semiauto.scala @@ -0,0 +1,73 @@ +package io.circe.generic.extras + +import io.circe.{ Codec, Decoder, Encoder } +import scala.deriving.Mirror + +/** + * Semi-automatic codec derivation. + * + * This object provides helpers for creating [[io.circe.Decoder]] and [[io.circe.ObjectEncoder]] + * instances for case classes, "incomplete" case classes, sealed trait hierarchies, etc. + * + * Typical usage will look like the following: + * + * {{{ + * import io.circe._, io.circe.generic.semiauto._ + * + * case class Foo(i: Int, p: (String, Double)) + * + * object Foo { + * implicit val decodeFoo: Decoder[Foo] = deriveDecoder[Foo] + * implicit val encodeFoo: Encoder.AsObject[Foo] = deriveEncoder[Foo] + * } + * }}} + */ +object semiauto { + inline final def deriveConfiguredDecoder[A](using inline A: Mirror.Of[A], configuration: Configuration): Decoder[A] = io.circe.derivation.ConfiguredDecoder.derived[A] + inline final def deriveConfiguredEncoder[A](using inline A: Mirror.Of[A], configuration: Configuration): Encoder.AsObject[A] = io.circe.derivation.ConfiguredEncoder.derived[A] + inline final def deriveConfiguredCodec[A](using inline A: Mirror.Of[A], configuration: Configuration): Codec.AsObject[A] = io.circe.derivation.ConfiguredCodec.derived[A] + + inline final def deriveExtrasDecoder[A](using inline A: Mirror.Of[A], configuration: Configuration): ExtrasDecoder[A] = ??? + inline final def deriveExtrasEncoder[A](using inline A: Mirror.Of[A], configuration: Configuration): Encoder.AsObject[A] = deriveConfiguredEncoder[A] + inline final def deriveExtrasCodec[A](using inline A: Mirror.Of[A], configuration: Configuration): ExtrasAsObjectCodec[A] = ??? + + /** + * Derive a decoder for a sealed trait hierarchy made up of case objects. + * + * Note that this differs from the usual derived decoder in that the leaves of the ADT are represented as JSON + * strings. + */ + def deriveEnumerationDecoder[A](): Decoder[A] = ??? + + /** + * Derive an encoder for a sealed trait hierarchy made up of case objects. + * + * Note that this differs from the usual derived encoder in that the leaves of the ADT are represented as JSON + * strings. + */ + def deriveEnumerationEncoder[A](): Encoder[A] = ??? + + /** + * Derive a codec for a sealed trait hierarchy made up of case objects. + * + * Note that this differs from the usual derived encoder in that the leaves of the ADT are represented as JSON + * strings. + */ + def deriveEnumerationCodec[A](): Codec[A] = ??? + + /** + * Derive a decoder for a value class. + */ + def deriveUnwrappedDecoder[A](): Decoder[A] = ??? + + /** + * Derive an encoder for a value class. + */ + def deriveUnwrappedEncoder[A](): Encoder[A] = ??? + + /** + * Derive a codec for a value class. + */ + def deriveUnwrappedCodec[A](): Codec[A] = ??? + +} diff --git a/generic-extras/src/test/scala-2/io/circe/generic/extras/ConfiguredAutoDerivedScala2Suite.scala b/generic-extras/src/test/scala-2/io/circe/generic/extras/ConfiguredAutoDerivedScala2Suite.scala new file mode 100644 index 00000000..733693a4 --- /dev/null +++ b/generic-extras/src/test/scala-2/io/circe/generic/extras/ConfiguredAutoDerivedScala2Suite.scala @@ -0,0 +1,32 @@ +package io.circe.generic.extras + +import cats.data.Validated +import cats.kernel.Eq +import io.circe.{ Decoder, DecodingFailure, Encoder, Json } +import io.circe.CursorOp.DownField +import io.circe.generic.extras.auto._ +import io.circe.literal._ +import io.circe.testing.CodecTests +import org.scalacheck.{ Arbitrary, Gen } +import org.scalacheck.Arbitrary.arbitrary +import org.scalacheck.Prop.forAll +import examples._ + +class ConfiguredAutoDerivedScala2Suite extends CirceSuite { + + import defaults._ + + property("Decoder[Int => Qux[String]] should decode partial JSON representations") { + forAll { (i: Int, s: String, j: Int) => + val result = Json + .obj( + "a" -> Json.fromString(s), + "j" -> Json.fromInt(j) + ) + .as[Int => Qux[String]] + .map(_(i)) + + assertEquals(result, Right(Qux(i, s, j))) + } + } +} diff --git a/generic-extras/src/test/scala/io/circe/generic/extras/ConfiguredJsonCodecSuite.scala b/generic-extras/src/test/scala-2/io/circe/generic/extras/ConfiguredJsonCodecSuite.scala similarity index 97% rename from generic-extras/src/test/scala/io/circe/generic/extras/ConfiguredJsonCodecSuite.scala rename to generic-extras/src/test/scala-2/io/circe/generic/extras/ConfiguredJsonCodecSuite.scala index a8092741..7c5995ea 100644 --- a/generic-extras/src/test/scala/io/circe/generic/extras/ConfiguredJsonCodecSuite.scala +++ b/generic-extras/src/test/scala-2/io/circe/generic/extras/ConfiguredJsonCodecSuite.scala @@ -76,8 +76,8 @@ class ConfiguredJsonCodecSuite extends CirceSuite { val json = json"""{ "type": "config_example_foo", "this_is_a_field": $f, "b": $b}""" val expected = json"""{ "type": "config_example_foo", "this_is_a_field": $f, "a": 0, "b": $b}""" - assert(Encoder[ConfigExampleBase].apply(foo) === expected) - assert(Decoder[ConfigExampleBase].decodeJson(json) === Right(foo)) + assertEquals(Encoder[ConfigExampleBase].apply(foo), expected) + assertEquals(Decoder[ConfigExampleBase].decodeJson(json), Right(foo)) } } diff --git a/generic-extras/src/test/scala/io/circe/generic/extras/ConfiguredJsonCodecWithKeySuite.scala b/generic-extras/src/test/scala-2/io/circe/generic/extras/ConfiguredJsonCodecWithKeySuite.scala similarity index 93% rename from generic-extras/src/test/scala/io/circe/generic/extras/ConfiguredJsonCodecWithKeySuite.scala rename to generic-extras/src/test/scala-2/io/circe/generic/extras/ConfiguredJsonCodecWithKeySuite.scala index abc1d811..3c79a2e5 100644 --- a/generic-extras/src/test/scala/io/circe/generic/extras/ConfiguredJsonCodecWithKeySuite.scala +++ b/generic-extras/src/test/scala-2/io/circe/generic/extras/ConfiguredJsonCodecWithKeySuite.scala @@ -45,8 +45,8 @@ class ConfiguredJsonCodecWithKeySuite extends CirceSuite { val json = json"""{ "type": "config_example_foo", "this_is_a_field": $f, "myField": $b}""" val expected = json"""{ "type": "config_example_foo", "this_is_a_field": $f, "a": 0, "myField": $b}""" - assert(Encoder[ConfigExampleBase].apply(foo) === expected) - assert(Decoder[ConfigExampleBase].decodeJson(json) === Right(foo)) + assertEquals(Encoder[ConfigExampleBase].apply(foo), expected) + assertEquals(Decoder[ConfigExampleBase].decodeJson(json), Right(foo)) } } } diff --git a/generic-extras/src/test/scala-2/io/circe/generic/extras/ConfiguredSemiautoDerivedScala2Suite.scala b/generic-extras/src/test/scala-2/io/circe/generic/extras/ConfiguredSemiautoDerivedScala2Suite.scala new file mode 100644 index 00000000..96745e98 --- /dev/null +++ b/generic-extras/src/test/scala-2/io/circe/generic/extras/ConfiguredSemiautoDerivedScala2Suite.scala @@ -0,0 +1,76 @@ +package io.circe.generic.extras + +import cats.kernel.Eq +import io.circe.{ Codec, Decoder, DecodingFailure, Encoder, Json } +import io.circe.generic.extras.semiauto._ +import io.circe.literal._ +import io.circe.testing.CodecTests +import org.scalacheck.{ Arbitrary, Gen } +import org.scalacheck.Arbitrary.arbitrary +import shapeless.Witness +import shapeless.labelled.{ FieldType, field } +import org.scalacheck.Prop.forAll + +import examples._ +import cats.laws.discipline.ScalaVersionSpecific + +object ConfiguredSemiautoDerivedScala2Suite { + implicit val customConfig: Configuration = + Configuration.default.withSnakeCaseMemberNames.withDefaults.withDiscriminator("type").withSnakeCaseConstructorNames + + implicit val decodeJlessQux: Decoder[FieldType[Witness.`'j`.T, Int] => Qux[String]] = + deriveConfiguredFor[FieldType[Witness.`'j`.T, Int] => Qux[String]].incomplete + + implicit val decodeIntlessQux: Decoder[Int => Qux[String]] = + deriveConfiguredFor[Int => Qux[String]].incomplete + + implicit val decodeQuxPatch: Decoder[Qux[String] => Qux[String]] = deriveConfiguredFor[Qux[String]].patch +} + +class ConfiguredSemiautoDerivedScala2Suite extends CirceSuite { + import ConfiguredSemiautoDerivedScala2Suite._ + + property("Decoder[FieldType[Witness.`'j`.T, Int] => Qux[String]] should decode partial JSON representations") { + forAll { (i: Int, s: String, j: Int) => + val result = Json + .obj( + "i" -> Json.fromInt(i), + "a" -> Json.fromString(s) + ) + .as[FieldType[Witness.`'j`.T, Int] => Qux[String]] + .map( + _(field(j)) + ) + + assertEquals(result, Right(Qux(i, s, j))) + } + } + + property("Decoder[Int => Qux[String]] should decode partial JSON representations") { + forAll { (i: Int, s: String, j: Int) => + val result = Json + .obj( + "a" -> Json.fromString(s), + "j" -> Json.fromInt(j) + ) + .as[Int => Qux[String]] + .map(_(i)) + + assertEquals(result, Right(Qux(i, s, j))) + } + } + + property("Decoder[Qux[String] => Qux[String]] should decode patch JSON representations") { + forAll { (q: Qux[String], i: Option[Int], a: Option[String], j: Option[Int]) => + val json = Json.obj( + "i" -> Encoder[Option[Int]].apply(i), + "a" -> Encoder[Option[String]].apply(a), + "j" -> Encoder[Option[Int]].apply(j) + ) + + val expected = Qux[String](i.getOrElse(q.i), a.getOrElse(q.a), j.getOrElse(q.j)) + + assertEquals(json.as[Qux[String] => Qux[String]].map(_(q)), Right(expected)) + } + } +} diff --git a/generic-extras/src/test/scala/io/circe/generic/extras/ConfiguredAutoDerivedSuite.scala b/generic-extras/src/test/scala/io/circe/generic/extras/ConfiguredAutoDerivedSuite.scala index e767051a..d58f6aa5 100644 --- a/generic-extras/src/test/scala/io/circe/generic/extras/ConfiguredAutoDerivedSuite.scala +++ b/generic-extras/src/test/scala/io/circe/generic/extras/ConfiguredAutoDerivedSuite.scala @@ -60,8 +60,8 @@ class ConfiguredAutoDerivedSuite extends CirceSuite { import foo._ val json = json"""{ "this_is_a_field": $thisIsAField, "a": $a, "b": $b}""" - assert(Encoder[ConfigExampleFoo].apply(foo) === json) - assert(Decoder[ConfigExampleFoo].decodeJson(json) === Right(foo)) + assertEquals(Encoder[ConfigExampleFoo].apply(foo), json) + assertEquals(Decoder[ConfigExampleFoo].decodeJson(json), Right(foo)) } } @@ -72,8 +72,8 @@ class ConfiguredAutoDerivedSuite extends CirceSuite { import foo._ val json = json"""{ "THIS_IS_A_FIELD": $thisIsAField, "A": $a, "B": $b}""" - assert(Encoder[ConfigExampleFoo].apply(foo) === json) - assert(Decoder[ConfigExampleFoo].decodeJson(json) === Right(foo)) + assertEquals(Encoder[ConfigExampleFoo].apply(foo), json) + assertEquals(Decoder[ConfigExampleFoo].decodeJson(json), Right(foo)) } } @@ -84,8 +84,8 @@ class ConfiguredAutoDerivedSuite extends CirceSuite { import foo._ val json = json"""{ "this-is-a-field": $thisIsAField, "a": $a, "b": $b}""" - assert(Encoder[ConfigExampleFoo].apply(foo) === json) - assert(Decoder[ConfigExampleFoo].decodeJson(json) === Right(foo)) + assertEquals(Encoder[ConfigExampleFoo].apply(foo), json) + assertEquals(Decoder[ConfigExampleFoo].decodeJson(json), Right(foo)) } } @@ -109,8 +109,8 @@ class ConfiguredAutoDerivedSuite extends CirceSuite { val json = json"""{ "thisIsAField": $f, "b": $b }""" val expected = json"""{ "thisIsAField": $f, "a": 0, "b": $b}""" - assert(Encoder[ConfigExampleFoo].apply(foo) === expected) - assert(Decoder[ConfigExampleFoo].decodeJson(json) === Right(foo)) + assertEquals(Encoder[ConfigExampleFoo].apply(foo), expected) + assertEquals(Decoder[ConfigExampleFoo].decodeJson(json), Right(foo)) } } @@ -129,12 +129,12 @@ class ConfiguredAutoDerivedSuite extends CirceSuite { test("Option[T] without default should be None if null decoded") { val json = json"""{ "a": null }""" - assert(Decoder[FooNoDefault].decodeJson(json) === Right(FooNoDefault(None, "b"))) + assertEquals(Decoder[FooNoDefault].decodeJson(json), Right(FooNoDefault(None, "b"))) } test("Option[T] without default should be None if missing key decoded") { val json = json"""{}""" - assert(Decoder[FooNoDefault].decodeJson(json) === Right(FooNoDefault(None, "b"))) + assertEquals(Decoder[FooNoDefault].decodeJson(json), Right(FooNoDefault(None, "b"))) } test("Option[T] with default should be None if null decoded") { @@ -144,19 +144,19 @@ class ConfiguredAutoDerivedSuite extends CirceSuite { test("Option[T] with default should be default value if missing key decoded") { val json = json"""{}""" - assert(Decoder[FooWithDefault].decodeJson(json) === Right(FooWithDefault(Some(0), "b"))) - assert(Decoder[FooWithDefault].decodeAccumulating(json.hcursor) === Validated.valid(FooWithDefault(Some(0), "b"))) + assertEquals(Decoder[FooWithDefault].decodeJson(json), Right(FooWithDefault(Some(0), "b"))) + assertEquals(Decoder[FooWithDefault].decodeAccumulating(json.hcursor), Validated.valid(FooWithDefault(Some(0), "b"))) } test("Value with default should be default value if value is null") { val json = json"""{"b": null}""" - assert(Decoder[FooWithDefault].decodeJson(json) === Right(FooWithDefault(Some(0), "b"))) - assert(Decoder[FooWithDefault].decodeAccumulating(json.hcursor) === Validated.valid(FooWithDefault(Some(0), "b"))) + assertEquals(Decoder[FooWithDefault].decodeJson(json), Right(FooWithDefault(Some(0), "b"))) + assertEquals(Decoder[FooWithDefault].decodeAccumulating(json.hcursor), Validated.valid(FooWithDefault(Some(0), "b"))) } test("Option[T] with default should fail to decode if type in json is not correct") { val json = json"""{"a": "NotAnInt"}""" - assert(Decoder[FooWithDefault].decodeJson(json) === Left(DecodingFailure("Int", List(DownField("a"))))) + assertEquals(Decoder[FooWithDefault].decodeJson(json), Left(DecodingFailure("Int", List(DownField("a"))))) assert( Decoder[FooWithDefault].decodeAccumulating(json.hcursor) === Validated.invalidNel(DecodingFailure("Int", List(DownField("a")))) @@ -187,8 +187,8 @@ class ConfiguredAutoDerivedSuite extends CirceSuite { import foo._ val json = json"""{ "type": "ConfigExampleFoo", "thisIsAField": $thisIsAField, "a": $a, "b": $b}""" - assert(Encoder[ConfigExampleBase].apply(foo) === json) - assert(Decoder[ConfigExampleBase].decodeJson(json) === Right(foo)) + assertEquals(Encoder[ConfigExampleBase].apply(foo), json) + assertEquals(Decoder[ConfigExampleBase].decodeJson(json), Right(foo)) } } @@ -200,8 +200,8 @@ class ConfiguredAutoDerivedSuite extends CirceSuite { import foo._ val json = json"""{ "type": "config_example_foo", "thisIsAField": $thisIsAField, "a": $a, "b": $b}""" - assert(Encoder[ConfigExampleBase].apply(foo) === json) - assert(Decoder[ConfigExampleBase].decodeJson(json) === Right(foo)) + assertEquals(Encoder[ConfigExampleBase].apply(foo), json) + assertEquals(Decoder[ConfigExampleBase].decodeJson(json), Right(foo)) } } @@ -215,8 +215,8 @@ class ConfiguredAutoDerivedSuite extends CirceSuite { import foo._ val json = json"""{ "type": "CONFIG_EXAMPLE_FOO", "thisIsAField": $thisIsAField, "a": $a, "b": $b}""" - assert(Encoder[ConfigExampleBase].apply(foo) === json) - assert(Decoder[ConfigExampleBase].decodeJson(json) === Right(foo)) + assertEquals(Encoder[ConfigExampleBase].apply(foo), json) + assertEquals(Decoder[ConfigExampleBase].decodeJson(json), Right(foo)) } } @@ -228,8 +228,8 @@ class ConfiguredAutoDerivedSuite extends CirceSuite { import foo._ val json = json"""{ "type": "config-example-foo", "thisIsAField": $thisIsAField, "a": $a, "b": $b}""" - assert(Encoder[ConfigExampleBase].apply(foo) === json) - assert(Decoder[ConfigExampleBase].decodeJson(json) === Right(foo)) + assertEquals(Encoder[ConfigExampleBase].apply(foo), json) + assertEquals(Decoder[ConfigExampleBase].decodeJson(json), Right(foo)) } } @@ -268,8 +268,8 @@ class ConfiguredAutoDerivedSuite extends CirceSuite { val json = json"""{ "type": "ConfigExampleFoo", "this_is_a_field": $f, "b": $b}""" val expected = json"""{ "type": "ConfigExampleFoo", "this_is_a_field": $f, "a": 0, "b": $b}""" - assert(Encoder[ConfigExampleBase].apply(foo) === expected) - assert(Decoder[ConfigExampleBase].decodeJson(json) === Right(foo)) + assertEquals(Encoder[ConfigExampleBase].apply(foo), expected) + assertEquals(Decoder[ConfigExampleBase].decodeJson(json), Right(foo)) } } @@ -279,19 +279,5 @@ class ConfiguredAutoDerivedSuite extends CirceSuite { checkAll("Codec[(Int, Int, Foo)]", CodecTests[(Int, Int, Foo)].codec) checkAll("Codec[Qux[Int]]", CodecTests[Qux[Int]].codec) checkAll("Codec[Foo]", CodecTests[Foo].codec) - - property("Decoder[Int => Qux[String]] should decode partial JSON representations") { - forAll { (i: Int, s: String, j: Int) => - val result = Json - .obj( - "a" -> Json.fromString(s), - "j" -> Json.fromInt(j) - ) - .as[Int => Qux[String]] - .map(_(i)) - - assert(result === Right(Qux(i, s, j))) - } - } } } diff --git a/generic-extras/src/test/scala/io/circe/generic/extras/ConfiguredSemiautoDerivedSuite.scala b/generic-extras/src/test/scala/io/circe/generic/extras/ConfiguredSemiautoDerivedSuite.scala index a49f7f44..7098fbb1 100644 --- a/generic-extras/src/test/scala/io/circe/generic/extras/ConfiguredSemiautoDerivedSuite.scala +++ b/generic-extras/src/test/scala/io/circe/generic/extras/ConfiguredSemiautoDerivedSuite.scala @@ -8,11 +8,10 @@ import io.circe.testing.CodecTests import io.circe.syntax._ import org.scalacheck.{ Arbitrary, Gen } import org.scalacheck.Arbitrary.arbitrary -import shapeless.Witness -import shapeless.labelled.{ FieldType, field } import org.scalacheck.Prop.forAll import examples._ +import cats.laws.discipline.ScalaVersionSpecific object ConfiguredSemiautoDerivedSuite { sealed trait ConfigExampleBase @@ -39,17 +38,9 @@ object ConfiguredSemiautoDerivedSuite { implicit val customConfig: Configuration = Configuration.default.withSnakeCaseMemberNames.withDefaults.withDiscriminator("type").withSnakeCaseConstructorNames - implicit val decodeIntlessQux: Decoder[Int => Qux[String]] = - deriveConfiguredFor[Int => Qux[String]].incomplete - - implicit val decodeJlessQux: Decoder[FieldType[Witness.`'j`.T, Int] => Qux[String]] = - deriveConfiguredFor[FieldType[Witness.`'j`.T, Int] => Qux[String]].incomplete - - implicit val decodeQuxPatch: Decoder[Qux[String] => Qux[String]] = deriveConfiguredFor[Qux[String]].patch - - implicit val decodeConfigExampleBase: Decoder[ConfigExampleBase] = deriveConfiguredDecoder - implicit val encodeConfigExampleBase: Encoder.AsObject[ConfigExampleBase] = deriveConfiguredEncoder - val codecForConfigExampleBase: Codec.AsObject[ConfigExampleBase] = deriveConfiguredCodec + implicit lazy val decodeConfigExampleBase: Decoder[ConfigExampleBase] = deriveConfiguredDecoder + implicit lazy val encodeConfigExampleBase: Encoder.AsObject[ConfigExampleBase] = deriveConfiguredEncoder + lazy val codecForConfigExampleBase: Codec.AsObject[ConfigExampleBase] = deriveConfiguredCodec } class ConfiguredSemiautoDerivedSuite extends CirceSuite { @@ -75,8 +66,8 @@ class ConfiguredSemiautoDerivedSuite extends CirceSuite { val json = json"""{ "type": "config_example_foo", "this_is_a_field": $f, "b": $b}""" val expected = json"""{ "type": "config_example_foo", "this_is_a_field": $f, "a": 0, "b": $b}""" - assert(Encoder[ConfigExampleBase].apply(foo) === expected) - assert(Decoder[ConfigExampleBase].decodeJson(json) === Right(foo)) + assertEquals(Encoder[ConfigExampleBase].apply(foo), expected) + assertEquals(Decoder[ConfigExampleBase].decodeJson(json), Right(foo)) } } @@ -106,9 +97,9 @@ class ConfiguredSemiautoDerivedSuite extends CirceSuite { val foo: ConfigExampleBase = ConfigExampleFoo("field_value", 0, 100) val encoded = encoder.apply(foo) val decoded = decoder.decodeJson(encoded) - assert(decoded === Right(foo)) - assert(transformMemberNamesCallCount === fieldCount * 2) - assert(transformConstructorCallCount === decodeConstructorCount + encodeConstructorCount) + assertEquals(decoded, Right(foo)) + assertEquals(transformMemberNamesCallCount, fieldCount * 2) + assertEquals(transformConstructorCallCount, decodeConstructorCount + encodeConstructorCount) } } @@ -130,7 +121,7 @@ class ConfiguredSemiautoDerivedSuite extends CirceSuite { val expectedError = DecodingFailure("Unexpected field: [stowaway_field]; valid fields: this_is_a_field, a, b, type_field", Nil) - assert(Decoder[ConfigExampleBase].decodeJson(json) === Left(expectedError)) + assertEquals(Decoder[ConfigExampleBase].decodeJson(json), Left(expectedError)) } } @@ -226,51 +217,7 @@ class ConfiguredSemiautoDerivedSuite extends CirceSuite { assert(clue(Decoder[ConfigExampleBase].decodeJson(json)).isRight) assert(Decoder[ConfigExampleBase].decodeJson(jsonExtra) === Left(expectedError)) } - } - - property("Decoder[Int => Qux[String]] should decode partial JSON representations") { - forAll { (i: Int, s: String, j: Int) => - val result = Json - .obj( - "a" -> Json.fromString(s), - "j" -> Json.fromInt(j) - ) - .as[Int => Qux[String]] - .map(_(i)) - - assert(result === Right(Qux(i, s, j))) - } - } - - property("Decoder[FieldType[Witness.`'j`.T, Int] => Qux[String]] should decode partial JSON representations") { - forAll { (i: Int, s: String, j: Int) => - val result = Json - .obj( - "i" -> Json.fromInt(i), - "a" -> Json.fromString(s) - ) - .as[FieldType[Witness.`'j`.T, Int] => Qux[String]] - .map( - _(field(j)) - ) - - assert(result === Right(Qux(i, s, j))) - } - } - - property("Decoder[Qux[String] => Qux[String]] should decode patch JSON representations") { - forAll { (q: Qux[String], i: Option[Int], a: Option[String], j: Option[Int]) => - val json = Json.obj( - "i" -> Encoder[Option[Int]].apply(i), - "a" -> Encoder[Option[String]].apply(a), - "j" -> Encoder[Option[Int]].apply(j) - ) - - val expected = Qux[String](i.getOrElse(q.i), a.getOrElse(q.a), j.getOrElse(q.j)) - - assert(json.as[Qux[String] => Qux[String]].map(_(q)) === Right(expected)) - } - } + } property("A generically derived codec for an empty case class should not accept non-objects") { forAll { (j: Json) => diff --git a/generic-extras/src/test/scala/io/circe/generic/extras/EnumerationSemiautoDerivedSuite.scala b/generic-extras/src/test/scala/io/circe/generic/extras/EnumerationSemiautoDerivedSuite.scala index aff5773c..8ed5280d 100644 --- a/generic-extras/src/test/scala/io/circe/generic/extras/EnumerationSemiautoDerivedSuite.scala +++ b/generic-extras/src/test/scala/io/circe/generic/extras/EnumerationSemiautoDerivedSuite.scala @@ -4,7 +4,6 @@ import io.circe.{ Codec, Decoder, Encoder } import io.circe.generic.extras.semiauto._ import io.circe.literal._ import io.circe.testing.CodecTests -import shapeless.test.illTyped import examples._ @@ -29,25 +28,47 @@ class EnumerationSemiautoDerivedSuite extends CirceSuite { test("deriveEnumerationDecoder should not compile on an ADT with case classes") { implicit val config: Configuration = Configuration.default - illTyped("deriveEnumerationDecoder[ExtendedCardinalDirection]") + assertNoDiff( + compileErrors("deriveEnumerationDecoder[ExtendedCardinalDirection]"), + """|error: + |Could not find EnumerationDecoder for type io.circe.generic.extras.examples.ExtendedCardinalDirection. + |Some possible causes for this: + |- io.circe.generic.extras.examples.ExtendedCardinalDirection isn't a case class or sealed trait + |- some of io.circe.generic.extras.examples.ExtendedCardinalDirection's members don't have codecs of their own + |- missing implicit Configuration + |deriveEnumerationDecoder[ExtendedCardinalDirection] + | ^ + |""".stripMargin + ) } test("it should respect Configuration") { implicit val config: Configuration = Configuration.default.withSnakeCaseConstructorNames val decodeMary = deriveEnumerationDecoder[Mary] val expected = json""""little_lamb"""" - assert(decodeMary.decodeJson(expected) === Right(LittleLamb)) + assertEquals(decodeMary.decodeJson(expected), Right(LittleLamb)) } test("deriveEnumerationEncoder should not compile on an ADT with case classes") { implicit val config: Configuration = Configuration.default - illTyped("deriveEnumerationEncoder[ExtendedCardinalDirection]") + assertNoDiff( + compileErrors("deriveEnumerationEncoder[ExtendedCardinalDirection]"), + """|error: + |Could not find EnumerationEncoder for type io.circe.generic.extras.examples.ExtendedCardinalDirection. + |Some possible causes for this: + |- io.circe.generic.extras.examples.ExtendedCardinalDirection isn't a case class or sealed trait + |- some of io.circe.generic.extras.examples.ExtendedCardinalDirection's members don't have codecs of their own + |- missing implicit Configuration + |deriveEnumerationEncoder[ExtendedCardinalDirection] + | ^ + |""".stripMargin + ) } test("it should respect Configuration") { implicit val config: Configuration = Configuration.default.withSnakeCaseConstructorNames val encodeMary = deriveEnumerationEncoder[Mary] val expected = json""""little_lamb"""" - assert(encodeMary(LittleLamb) === expected) + assertEquals(encodeMary(LittleLamb), expected) } } diff --git a/generic-extras/src/test/scala/io/circe/generic/extras/UnwrappedSemiautoDerivedSuite.scala b/generic-extras/src/test/scala/io/circe/generic/extras/UnwrappedSemiautoDerivedSuite.scala index 9898d312..e3321d7b 100644 --- a/generic-extras/src/test/scala/io/circe/generic/extras/UnwrappedSemiautoDerivedSuite.scala +++ b/generic-extras/src/test/scala/io/circe/generic/extras/UnwrappedSemiautoDerivedSuite.scala @@ -35,7 +35,7 @@ class UnwrappedSemiautoDerivedSuite extends CirceSuite { val foo = Foo(s) val expected = Json.fromString(s) - assert(Encoder[Foo].apply(foo) === expected) + assertEquals(Encoder[Foo].apply(foo), expected) } } @@ -44,7 +44,7 @@ class UnwrappedSemiautoDerivedSuite extends CirceSuite { val json = Json.fromString(s) val expected = Right(Foo(s)) - assert(Decoder[Foo].decodeJson(json) === expected) + assertEquals(Decoder[Foo].decodeJson(json), expected) } }