-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial commit: Add most of circe-generic-extras rewrites allowing fo…
…r migration from Scala 2 to Scala 3
- Loading branch information
1 parent
f05daa2
commit 3225649
Showing
30 changed files
with
2,103 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
name: Tests | ||
on: | ||
pull_request: | ||
push: | ||
branches: | ||
- main | ||
|
||
jobs: | ||
tests: | ||
name: Test scalafix rules | ||
runs-on: ubuntu-22.04 | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- uses: coursier/cache-action@v6 | ||
- uses: coursier/setup-action@v1 | ||
with: | ||
jvm: adopt:17 | ||
|
||
- name: Test rules | ||
run: sbt "test" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,16 @@ | ||
*.class | ||
*.log | ||
# sbt specific | ||
**/target/ | ||
project/plugins/project/ | ||
|
||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml | ||
hs_err_pid* | ||
# VS Code | ||
.vscode/ | ||
# Metals | ||
**/.bloop/ | ||
**/.metals/ | ||
**/metals.sbt | ||
|
||
# Bloop | ||
**/.bsp | ||
|
||
# scala-cli | ||
**/.scala-build |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
version = "3.7.15" | ||
runner.dialect = scala213 | ||
maxColumn = 120 | ||
fileOverride { | ||
"glob:**/output/src/main/scala/**" { | ||
runner.dialect = scala3 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# Scalafix rules for circe-generic-extra migration | ||
|
||
This project contains set of rules allowing to migrate Scala 2 macro-annotation based codecs into Scala 3 built-in derivation. | ||
|
||
## Available rules: | ||
|
||
### `CirceGenericExtrasMigration` | ||
Main rule, detects usages of `JsonCodec`, `ConfiguredJsonCodec` macro-annotations and replaces them with derived `Codec.AsObject` or `ConfiguredCodec` instances. | ||
Supports `@JsonKey` annotation used to renaem fields defined either in primary constructor or as member fields (renames or member fields are working using best effort principles). | ||
Supoprts `ConfiguredJsonCodec(encodeOnly = true)` and `ConfiguredJsonCodec(decodeOnly = true)` variants, these would be rewritten into `ConfiguredEncoder` or `ConfiguredDecoder` derived instances. | ||
Can be used to migrate usages of both `circe-generic-extras` and `circe-derivation` to Scala 3. | ||
|
||
### `CirceLiteralMigration` | ||
Simple rule allowing to adopt to `json` string interpolator changes done in Scala 3. Would rewrite imports `io.circe.literal.JsonStringContext` into `io.circe.literal.json`. | ||
|
||
## Usage | ||
For information on how to use this projects refer to [Scalafix user guide](https://scalacenter.github.io/scalafix/docs/users/installation.html) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
lazy val V = _root_.scalafix.sbt.BuildInfo | ||
|
||
val Scala2Version = "2.13.13" | ||
val Scala3Version = "3.4.0" | ||
|
||
inThisBuild( | ||
List( | ||
organization := "org.virtuslab", | ||
homepage := Some(url("https://github.com/VirtusLabRnD/scalafix-migrate-circe-generic-extras")), | ||
licenses := List( | ||
"Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0") | ||
), | ||
developers := List( | ||
Developer("WojciechMazur", "Wojciech Mazur", "[email protected]", url("https://github.com/WojciechMazur")) | ||
), | ||
scalaVersion := Scala2Version, | ||
semanticdbEnabled := true, | ||
semanticdbIncludeInJar := true, | ||
semanticdbVersion := scalafixSemanticdb.revision | ||
) | ||
) | ||
|
||
(publish / skip) := true | ||
|
||
lazy val rules = project.settings( | ||
moduleName := "scalafix-migrate-circe-generic-extras", | ||
libraryDependencies += "ch.epfl.scala" %% "scalafix-core" % V.scalafixVersion | ||
) | ||
|
||
// Dependencies mostly used to check compilation of circe-generic sources | ||
val circeVersion = "0.14.6" | ||
val circeGenericExtrasVersion = "0.14.3" | ||
val circeDerivationVersion = "0.13.0-M5" | ||
val jawnVersion = "1.5.1" | ||
val munitVersion = "0.7.29" | ||
val disciplineMunitVersion = "1.0.9" | ||
val enumeratumVersion = "1.7.3" | ||
|
||
lazy val commonTestDependencies = List( | ||
// used in examples | ||
"com.beachape" %% "enumeratum" % enumeratumVersion, | ||
"com.beachape" %% "enumeratum-circe" % enumeratumVersion, | ||
"io.circe" %% "circe-core" % circeVersion, | ||
"io.circe" %% "circe-literal" % circeVersion, | ||
"io.circe" %% "circe-parser" % circeVersion, | ||
// Used by circe-generic-extra tests | ||
"io.circe" %% "circe-testing" % circeVersion, | ||
"org.scalameta" %% "munit" % munitVersion, | ||
"org.scalameta" %% "munit-scalacheck" % munitVersion, | ||
"org.typelevel" %% "discipline-munit" % disciplineMunitVersion, | ||
"org.typelevel" %% "jawn-parser" % jawnVersion | ||
) | ||
|
||
lazy val input = project.settings( | ||
(publish / skip) := true, | ||
scalaVersion := Scala2Version, | ||
scalacOptions ++= Seq( | ||
"-Ymacro-annotations" | ||
), | ||
libraryDependencySchemes ++= Seq( | ||
"io.circe" %% "circe-core" % VersionScheme.Always // See https://github.com/circe/circe-derivation/issues/346 | ||
), | ||
libraryDependencies ++= commonTestDependencies ++ Seq( | ||
"io.circe" %% "circe-derivation" % circeDerivationVersion, | ||
"io.circe" %% "circe-derivation-annotations" % circeDerivationVersion, | ||
"io.circe" %% "circe-generic-extras" % circeGenericExtrasVersion | ||
) | ||
) | ||
|
||
lazy val output = project.settings( | ||
(publish / skip) := true, | ||
scalaVersion := Scala3Version, | ||
libraryDependencies ++= commonTestDependencies ++ Seq( | ||
"io.circe" %% "circe-generic" % circeVersion | ||
) | ||
) | ||
|
||
lazy val tests = project | ||
.settings( | ||
(publish / skip) := true, | ||
libraryDependencies += "ch.epfl.scala" % "scalafix-testkit" % V.scalafixVersion % Test cross CrossVersion.full, | ||
scalafixTestkitOutputSourceDirectories := | ||
(output / Compile / unmanagedSourceDirectories).value, | ||
scalafixTestkitInputSourceDirectories := | ||
(input / Compile / unmanagedSourceDirectories).value, | ||
scalafixTestkitInputClasspath := | ||
(input / Compile / fullClasspath).value | ||
) | ||
.dependsOn(rules) | ||
.enablePlugins(ScalafixTestkitPlugin) |
55 changes: 55 additions & 0 deletions
55
input/src/main/scala/test/CirceDerivationAnnotations.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
/* | ||
rule = CirceGenericExtrasMigration | ||
*/ | ||
import io.circe.derivation.annotations._ | ||
import io.circe.{Codec, Encoder, Decoder} | ||
|
||
object CirceDerivationAnnotations { | ||
object jsonCodec { | ||
@JsonCodec() case class CClass0() | ||
@JsonCodec() case class CClass1(name: String) | ||
@JsonCodec() case class CClass1Generic1[T](v: T) | ||
@JsonCodec() case class CClass2Generic2[A, B](a: A, b: B) | ||
@JsonCodec() sealed trait T1 { def name: String } | ||
@JsonCodec() case class T1Impl(name: String, count: Int) extends T1 | ||
|
||
implicitly[Codec[CClass0]] | ||
implicitly[Codec[CClass1]] | ||
implicitly[Codec[CClass1Generic1[Int]]] | ||
implicitly[Codec[CClass2Generic2[String, CClass0]]] | ||
implicitly[Codec[T1]] | ||
implicitly[Codec[T1Impl]] | ||
} | ||
|
||
object jsonExplicitCodec { | ||
@JsonCodec(Configuration.default) case class CClass0() | ||
@JsonCodec(Configuration.default) case class CClass1(name: String) | ||
@JsonCodec(Configuration.default) case class CClass1Generic1[T](v: T) | ||
@JsonCodec(Configuration.default) case class CClass2Generic2[A, B](a: A, b: B) | ||
@JsonCodec(Configuration.default) sealed trait T1 { def name: String } | ||
@JsonCodec(Configuration.default) case class T1Impl(name: String, count: Int) extends T1 | ||
|
||
implicitly[Codec[CClass0]] | ||
implicitly[Encoder[CClass1]] | ||
implicitly[Decoder[CClass1Generic1[Int]]] | ||
implicitly[Codec[CClass2Generic2[String, CClass0]]] | ||
implicitly[Encoder[T1]] | ||
implicitly[Encoder[T1Impl]] | ||
} | ||
|
||
object jsonCodecWithJsonKey { | ||
@JsonCodec() case class CClass0() | ||
@JsonCodec() case class CClass1(@JsonKey("id") name: String) | ||
@JsonCodec() case class CClass1Generic1[T](@JsonKey("firstValue")v: T) | ||
@JsonCodec() case class CClass2Generic2[A, B](@JsonKey("first")a: A, @JsonKey("secondValue")b: B) | ||
// @JsonCodec() sealed trait T1 { @JsonKey("memberName") def name: String } | ||
// @JsonCodec() case class T1Impl(name: String, count: Int) extends T1 | ||
|
||
implicitly[Codec[CClass0]] | ||
implicitly[Encoder[CClass1]] | ||
implicitly[Decoder[CClass1Generic1[Int]]] | ||
implicitly[Codec[CClass2Generic2[String, CClass0]]] | ||
// implicitly[Encoder[T1]] | ||
// implicitly[Encoder[T1Impl]] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
/* | ||
rule = CirceGenericExtrasMigration | ||
*/ | ||
import io.circe.generic.extras._ | ||
import io.circe.derivation.annotations.{ JsonCodec } | ||
import io.circe.{Codec, Encoder, Decoder} | ||
|
||
object CirceGenericExtras { | ||
implicit val defaultConfig: Configuration = Configuration.default | ||
|
||
object configuredJsonCodec { | ||
@ConfiguredJsonCodec() case class CClass0() | ||
@ConfiguredJsonCodec(encodeOnly = true) case class CClass1(name: String) | ||
@ConfiguredJsonCodec(decodeOnly = true) case class CClass1Generic1[T](v: T) | ||
@ConfiguredJsonCodec() case class CClass2Generic2[A, B](a: A, b: B) | ||
@ConfiguredJsonCodec() sealed trait T1 { def name: String } | ||
@ConfiguredJsonCodec() case class T1Impl(name: String, count: Int) extends T1 | ||
|
||
implicitly[Codec[CClass0]] | ||
implicitly[Encoder[CClass1]] | ||
implicitly[Decoder[CClass1Generic1[Int]]] | ||
// implicitly[Codec[CClass2Generic2[String, CClass0]]] | ||
implicitly[Codec[T1]] | ||
implicitly[Codec[T1Impl]] | ||
} | ||
|
||
object jsonCodecWithJsonKey { | ||
@ConfiguredJsonCodec() case class CClass0() | ||
@ConfiguredJsonCodec() case class CClass1(@JsonKey("id") name: String) | ||
@ConfiguredJsonCodec() case class CClass1Generic1[T](@JsonKey("firstValue")v: T) | ||
@ConfiguredJsonCodec() case class CClass2Generic2[A, B](@JsonKey("first")a: A, @JsonKey("secondValue")b: B) | ||
// @ConfiguredJsonCodec() sealed trait T1 { @JsonKey("memberName") def name: String } | ||
// @ConfiguredJsonCodec() case class T1Impl(name: String, count: Int) extends T1 | ||
|
||
implicitly[Codec[CClass0]] | ||
implicitly[Encoder[CClass1]] | ||
implicitly[Decoder[CClass1Generic1[Int]]] | ||
// implicitly[Codec[CClass2Generic2[String, CClass0]]] | ||
// implicitly[Encoder[T1]] | ||
// implicitly[Encoder[T1Impl]] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
/* | ||
rules = [CirceGenericExtrasMigration, CirceLiteralMigration] | ||
*/ | ||
package test | ||
|
||
import enumeratum.EnumEntry.UpperSnakecase | ||
import enumeratum.{CirceEnum, EnumEntry} | ||
import io.circe.Decoder.Result | ||
import io.circe.derivation.annotations.JsonCodec | ||
import io.circe.derivation.annotations.{JsonCodec => JsonCodecAlias} | ||
import io.circe.generic.extras.{ConfiguredJsonCodec, Configuration} | ||
import io.circe.literal.JsonStringContext | ||
import io.circe.syntax.EncoderOps | ||
|
||
object Example1 extends App { | ||
|
||
// 1. CirceGenericExtrasMigration | ||
@JsonCodec | ||
case class Person(name: String, age: Int) | ||
println("Person: " + Person("Bob", 42).asJson) | ||
|
||
@JsonCodecAlias | ||
case class Address(line: String) | ||
|
||
implicit val config: Configuration = Configuration.default | ||
@ConfiguredJsonCodec | ||
case class Foo(v: Int) | ||
@ConfiguredJsonCodec(decodeOnly = true) | ||
case class FooRead(foo: Foo) | ||
@ConfiguredJsonCodec(encodeOnly = true) | ||
case class FooWrite(foo: Foo) | ||
|
||
// 2. CirceEnumeratumMigration | ||
sealed trait Color extends EnumEntry with UpperSnakecase | ||
object Color extends enumeratum.Enum[Color] with CirceEnum[Color] { | ||
case object Red extends Color | ||
case object Green extends Color | ||
case object Blue extends Color | ||
val values = findValues | ||
} | ||
|
||
val color: Color = Color.Red | ||
println("Color: " + color.asJson) | ||
|
||
// 3. CirceLiteralMigration | ||
val decodedPerson: Result[Person] = json"""{ | ||
"name" : "Sam", | ||
"age" : 70 | ||
}""".as[Person] | ||
println("Decoded: " + decodedPerson) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
/* | ||
rules = [CirceGenericExtrasMigration, CirceLiteralMigration] | ||
*/ | ||
package test | ||
|
||
import io.circe.Codec | ||
import io.circe.generic.extras.{Configuration, ConfiguredJsonCodec, JsonKey} | ||
import io.circe.syntax.EncoderOps | ||
import java.time.Instant | ||
|
||
object CirceUsage extends App { | ||
|
||
// 1. deriveUnwrappedCodec | ||
case class ThingId(value: String) extends AnyVal | ||
object ThingId { | ||
implicit val codec: Codec[ThingId] = | ||
io.circe.generic.extras.semiauto.deriveUnwrappedCodec | ||
} | ||
|
||
// 2. @JsonKey annotation | ||
@ConfiguredJsonCodec | ||
case class ExternalServiceRequest( | ||
id: ThingId, | ||
@JsonKey("snake_case_field") snakeCaseField: String, | ||
@JsonKey("acheivedAt") achievedAt: Instant // Do not inherit misspelling | ||
) | ||
object ExternalServiceRequest { | ||
implicit val config: Configuration = Configuration.default | ||
} | ||
|
||
println( | ||
"Request example: " + ExternalServiceRequest( | ||
ThingId("abc123"), | ||
"foo", | ||
Instant.now() | ||
).asJson | ||
) | ||
|
||
// 3. Sum types with a discriminator | ||
@ConfiguredJsonCodec | ||
sealed trait Fruit | ||
object Fruit { | ||
implicit val configuration: Configuration = | ||
Configuration.default.withScreamingSnakeCaseConstructorNames | ||
.withDiscriminator("fruitType") | ||
|
||
case class Banana(curvature: Double) extends Fruit | ||
case class Apple(diameter: Double, variety: String) extends Fruit | ||
case object Grape extends Fruit | ||
} | ||
import Fruit.{Apple, Banana, Grape} | ||
|
||
val fruits: List[Fruit] = List(Banana(3.14), Apple(0.07, "Fuji"), Grape) | ||
println("Fruits: " + fruits.asJson) | ||
} |
Oops, something went wrong.