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

Scala3 (WIP) #249

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
32 changes: 18 additions & 14 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -88,17 +92,17 @@ 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
.in(file("benchmarks"))
.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)
Expand Down
29 changes: 6 additions & 23 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions generic-extras/src/main/scala-3/io/circe/generic/extras/auto.scala
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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] = ???

}
Original file line number Diff line number Diff line change
@@ -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)))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
}
Loading