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

kebs-circe - Scala 3 support #274

Merged
merged 8 commits into from
Apr 21, 2023
Merged
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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,25 @@ And capitalized:
object KebsProtocol extends KebsCirce with KebsCirce.Capitalized
```

**NOTE for Scala 3 version of kebs-circe**:
1. As of today, there is no support for the @noflat annotation - using it will have no effect.
2. If you're using recursive types - due to [this issue](https://github.com/circe/circe/issues/1980) you'll have to add codecs explicitly in the following way:
```scala
case class R(a: Int, rs: Seq[R]) derives Decoder, Encoder.AsObject
```


3. If you're using flat format or Snakified/Capitalized formats, remember to import `given` instances, e.g.:
```scala
object KebsProtocol extends KebsCirce with KebsCirce.Snakified
import KebsProtocol.{given, _}
```

as for NoFlat, it should stay the same:
```scala
object KebsProtocol extends KebsCirce with KebsCirce.NoFlat
import KebsProtocol._
```
#### - kebs generates akka-http Unmarshaller (kebs-akka-http)

It makes it very easy to use 1-element case-classes or `enumeratum` enums/value enums in eg. `parameters` directive:
Expand Down
23 changes: 12 additions & 11 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def sv[A](scalaVersion: String, scala2_12Version: => A, scala2_13Version: => A)
}

def paradiseFlag(scalaVersion: String): Seq[String] =
if (scalaVersion == scala_2_12)
if (scalaVersion == scala_2_12 || scalaVersion == scala_32)
Seq.empty
else
Seq("-Ymacro-annotations")
Expand All @@ -113,10 +113,11 @@ val slickPg = "com.github.tminglei" %% "slick-pg" % "0.21.1"
val doobie = "org.tpolecat" %% "doobie-core" % "1.0.0-RC2"
val doobiePg = "org.tpolecat" %% "doobie-postgres" % "1.0.0-RC2"
val sprayJson = "io.spray" %% "spray-json" % "1.3.6"
val circe = Def.setting("io.circe" %%% "circe-core" % "0.14.5")
val circeAuto = "io.circe" %% "circe-generic" % "0.14.5"
val circeV = "0.14.5"
val circe = "io.circe" %% "circe-core" % circeV
val circeAuto = "io.circe" %% "circe-generic" % circeV
val circeAutoExtras = "io.circe" %% "circe-generic-extras" % "0.14.3"
val circeParser = "io.circe" %% "circe-parser" % "0.14.5"
val circeParser = "io.circe" %% "circe-parser" % circeV

val jsonschema = "com.github.andyglow" %% "scala-jsonschema" % "0.7.9"

Expand Down Expand Up @@ -190,12 +191,13 @@ lazy val playJsonSettings = commonSettings ++ Seq(
)

lazy val circeSettings = commonSettings ++ Seq(
libraryDependencies += circe.value,
libraryDependencies += circe,
libraryDependencies += circeAuto,
libraryDependencies += circeAutoExtras.cross(CrossVersion.for3Use2_13),
libraryDependencies += optionalEnumeratum.cross(CrossVersion.for3Use2_13),
libraryDependencies += circeParser % "test"
)
libraryDependencies += (circeParser % "test")
) ++ Seq(
libraryDependencies ++= (if (scalaVersion.value.startsWith("3")) Nil
else Seq(circeAutoExtras)))

lazy val akkaHttpSettings = commonSettings ++ Seq(
libraryDependencies += (akkaHttp).cross(CrossVersion.for3Use2_13),
Expand Down Expand Up @@ -225,7 +227,7 @@ lazy val scalacheckSettings = commonSettings ++ Seq(

lazy val taggedSettings = commonSettings ++ Seq(
libraryDependencies += optionalSlick.cross(CrossVersion.for3Use2_13),
libraryDependencies += optional(circe.value)
libraryDependencies += optional(circe)
)

lazy val opaqueSettings = commonSettings
Expand All @@ -247,7 +249,7 @@ lazy val benchmarkSettings = commonSettings ++ Seq(

lazy val taggedMetaSettings = metaSettings ++ Seq(
libraryDependencies += optional(sprayJson.cross(CrossVersion.for3Use2_13)),
libraryDependencies += optional(circe.value)
libraryDependencies += optional(circe)
)

lazy val instancesSettings = commonSettings
Expand Down Expand Up @@ -351,7 +353,6 @@ lazy val circeSupport = project
.settings(circeSettings: _*)
.settings(crossBuildSettings: _*)
.settings(publishSettings: _*)
.settings(disableScala("3"))
.settings(
name := "circe",
description := "Automatic generation of circe formats for case-classes",
Expand Down
57 changes: 57 additions & 0 deletions circe/src/main/scala-3/pl/iterators/kebs/circe/KebsCirce.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package pl.iterators.kebs.circe

import io.circe.{ Decoder, Encoder }
import scala.deriving._
import scala.util.Try
import scala.quoted.Quotes
import io.circe.HCursor
import pl.iterators.kebs.macros.CaseClass1Rep
import pl.iterators.kebs.instances.InstanceConverter
import io.circe.generic.AutoDerivation
import scala.quoted.Type
import io.circe.derivation.ConfiguredDecoder
import io.circe.derivation.Configuration
import io.circe.Derivation
import io.circe.DecoderDerivation
import io.circe.EncoderDerivation
import io.circe.derivation.ConfiguredEncoder
import scala.NonEmptyTuple

private[circe] trait KebsAutoDerivation {

implicit val configuration: Configuration = Configuration.default

inline implicit def exportDecoder[A](using conf: Configuration, inline m: Mirror.ProductOf[A]): ConfiguredDecoder[A] =
ConfiguredDecoder.derived[A]

inline implicit def exportEncoder[A](using conf: Configuration, inline m: Mirror.ProductOf[A]): ConfiguredEncoder[A] =
ConfiguredEncoder.derived[A]
}
trait KebsCirce extends KebsAutoDerivation {

inline given[T, A](using rep: CaseClass1Rep[T, A], decoder: Decoder[A]): Decoder[T] = {
decoder.emap(obj => Try(rep.apply(obj)).toEither.left.map(_.getMessage))
}

inline given[T, A](using rep: CaseClass1Rep[T, A], encoder: Encoder[A]): Encoder[T] =
encoder.contramap(rep.unapply)

inline given[T, A](using rep: InstanceConverter[T, A], encoder: Encoder[A]): Encoder[T] =
encoder.contramap(rep.encode)

inline given[T, A](using rep: InstanceConverter[T, A], decoder: Decoder[A]): Decoder[T] =
decoder.emap(obj => Try(rep.decode(obj)).toEither.left.map(_.getMessage))
}

object KebsCirce {
trait NoFlat extends KebsCirce {
}

trait Snakified extends KebsCirce {
override implicit val configuration: Configuration = Configuration.default.withSnakeCaseMemberNames
}

trait Capitalized extends KebsCirce {
override implicit val configuration: Configuration = Configuration.default.withPascalCaseMemberNames
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package pl.iterators.kebs.circe

import io.circe.Decoder.Result
import io.circe._
import pl.iterators.kebs.macros.enums.{EnumOf}
import scala.reflect.Enum
import scala.util.Try
import pl.iterators.kebs.enums.ValueEnum
import pl.iterators.kebs.macros.enums.ValueEnumLike
import pl.iterators.kebs.macros.enums.ValueEnumOf
trait CirceEnum {
@inline protected final def enumNameDeserializationError[E <: Enum](e: EnumOf[E], name: String): String = {
val enumNames = e.`enum`.values.mkString(", ")
s"$name should be one of $enumNames"
}

@inline protected final def enumValueDeserializationError[E <: Enum](e: EnumOf[E], value: Json): String = {
val enumNames = e.`enum`.values.mkString(", ")
s"$value should be a string of value $enumNames"
}

protected final def enumDecoder[E <: Enum](e: EnumOf[E], _comap: String => Option[E]): Decoder[E] =
(c: HCursor) =>
Decoder.decodeString.emap(str => _comap(str).toRight(""))
.withErrorMessage(enumValueDeserializationError(e, c.value))(c)

protected final def enumEncoder[E <: Enum](e: EnumOf[E], _map: E => String): Encoder[E] =
(obj: E) => Encoder.encodeString(_map(obj))

def enumDecoder[E <: Enum](e: EnumOf[E]): Decoder[E] =
enumDecoder[E](e, s => e.`enum`.values.find(_.toString.equalsIgnoreCase(s)))

def enumEncoder[E <: Enum](e: EnumOf[E]): Encoder[E] =
enumEncoder[E](e, (e: Enum) => e.toString)

def lowercaseEnumDecoder[E <: Enum](e: EnumOf[E]): Decoder[E] =
enumDecoder[E](e, s => e.`enum`.values.find(_.toString.toLowerCase == s))
def lowercaseEnumEncoder[E <: Enum](e: EnumOf[E]): Encoder[E] =
enumEncoder[E](e, (e: Enum) => e.toString.toLowerCase)

def uppercaseEnumDecoder[E <: Enum](e: EnumOf[E]): Decoder[E] =
enumDecoder[E](e, s => e.`enum`.values.find(_.toString().toUpperCase() == s))
def uppercaseEnumEncoder[E <: Enum](e: EnumOf[E]): Encoder[E] =
enumEncoder[E](e, (e: Enum) => e.toString().toUpperCase())
}

trait CirceValueEnum {
@inline protected final def valueEnumDeserializationError[V, E <: ValueEnum[V] with Enum](e: ValueEnumOf[V, E], value: Json): String = {
val enumValues = e.`enum`.values.map(_.value.toString()).mkString(", ")
s"$value is not a member of $enumValues"
}

def valueEnumDecoder[V, E <: ValueEnum[V] with Enum](e: ValueEnumOf[V, E])(implicit decoder: Decoder[V]): Decoder[E] =
(c: HCursor) =>
decoder.emap(obj => Try(e.`enum`.valueOf(obj)).toOption.toRight("")).withErrorMessage(valueEnumDeserializationError(e, c.value))(c)

def valueEnumEncoder[V, E <: ValueEnum[V] with Enum](e: ValueEnumOf[V, E])(implicit encoder: Encoder[V]): Encoder[E] =
(obj: E) => { encoder(obj.value) }
}

trait KebsEnumFormats extends CirceEnum with CirceValueEnum {
implicit inline given[E <: Enum](using ev: EnumOf[E]): Decoder[E] = enumDecoder(ev)

implicit inline given[E <: Enum](using ev: EnumOf[E]): Encoder[E] = enumEncoder(ev)

implicit inline given[V, E <: ValueEnum[V] with Enum](using ev: ValueEnumOf[V, E], decoder: Decoder[V]): Decoder[E] =
valueEnumDecoder(ev)

implicit inline given[V, E <: ValueEnum[V] with Enum](using ev: ValueEnumOf[V, E], encoder: Encoder[V]): Encoder[E] =
valueEnumEncoder(ev)

trait Uppercase extends CirceEnum {
implicit inline given[E <: Enum](using ev: EnumOf[E]): Decoder[E] =
uppercaseEnumDecoder(ev)

implicit inline given[E <: Enum](using ev: EnumOf[E]): Encoder[E] =
uppercaseEnumEncoder(ev)
}

trait Lowercase extends CirceEnum {
implicit inline given[E <: Enum](using ev: EnumOf[E]): Decoder[E] =
lowercaseEnumDecoder(ev)

implicit inline given[E <: Enum](using ev: EnumOf[E]): Encoder[E] =
lowercaseEnumEncoder(ev)
}
}

object KebsEnumFormats extends KebsEnumFormats
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import io.circe._
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import pl.iterators.kebs.circe.KebsEnumFormats

class CirceValueEnumDecoderEncoderTests extends AnyFunSuite with Matchers {
sealed abstract class LongGreeting(val value: Long) extends LongEnumEntry

Expand Down Expand Up @@ -35,4 +34,4 @@ class CirceValueEnumDecoderEncoderTests extends AnyFunSuite with Matchers {
val decoder = implicitly[Decoder[LongGreeting]]
decoder(Json.fromLong(4).hcursor) shouldBe Left(DecodingFailure("4 is not a member of 0, 1, 2, 3", List.empty[CursorOp]))
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package instances


import io.circe.{Decoder, Encoder, Json}
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import pl.iterators.kebs.circe.KebsCirce
import pl.iterators.kebs.instances.InstanceConverter
import pl.iterators.kebs.instances.time.LocalDateTimeString
import pl.iterators.kebs.instances.time.mixins.{DurationNanosLong, InstantEpochMilliLong}
import pl.iterators.kebs.instances.TimeInstances
import pl.iterators.kebs.instances.{InstanceConverter, TimeInstances}

import java.time._
import java.time.format.DateTimeFormatter
Expand Down Expand Up @@ -98,7 +97,7 @@ class TimeInstancesMixinTests extends AnyFunSuite with Matchers {
}
}
import TimeInstancesProtocol._

"implicitly[CaseClass1Rep[LocalDateTime, String]]" shouldNot typeCheck
"implicitly[CaseClass1Rep[String, LocalDateTime]]" shouldNot typeCheck

Expand Down
56 changes: 56 additions & 0 deletions circe/src/test/scala-3/CirceEnumDecoderEncoderTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import io.circe._
import org.scalatest.matchers.should.Matchers
import org.scalatest.funsuite.AnyFunSuite
import pl.iterators.kebs.circe.KebsEnumFormats
import scala.reflect.Enum

class CirceEnumDecoderEncoderTests extends AnyFunSuite with Matchers {

enum Greeting {
case Hello, GoodBye, Hi, Bye
}

object KebsProtocol extends KebsEnumFormats
object KebsProtocolUppercase extends KebsEnumFormats.Uppercase
object KebsProtocolLowercase extends KebsEnumFormats.Lowercase



import Greeting._
test("enum JsonFormat") {
import KebsProtocol._
val decoder = implicitly[Decoder[Greeting]]
val encoder = implicitly[Encoder[Greeting]]
decoder(Json.fromString("hElLo").hcursor) shouldBe Right(Hello)
decoder(Json.fromString("goodbye").hcursor) shouldBe Right(GoodBye)
encoder(Hello) shouldBe Json.fromString("Hello")
encoder(GoodBye) shouldBe Json.fromString("GoodBye")
}

test("enum name deserialization error") {
import KebsProtocol._
val decoder = implicitly[Decoder[Greeting]]
decoder(Json.fromInt(1).hcursor) shouldBe Left(
DecodingFailure("1 should be a string of value Hello, GoodBye, Hi, Bye", List.empty[CursorOp]))
}

test("enum JsonFormat - lowercase") {
import KebsProtocol._
val decoder = implicitly[Decoder[Greeting]]
val encoder = implicitly[Encoder[Greeting]]
decoder(Json.fromString("hello").hcursor) shouldBe Right(Hello)
decoder(Json.fromString("goodbye").hcursor) shouldBe Right(GoodBye)
encoder(Hello) shouldBe Json.fromString("Hello")
encoder(GoodBye) shouldBe Json.fromString("GoodBye")
}

test("enum JsonFormat - uppercase") {
import KebsProtocol._
val decoder = implicitly[Decoder[Greeting]]
val encoder = implicitly[Encoder[Greeting]]
decoder(Json.fromString("HELLO").hcursor) shouldBe Right(Hello)
decoder(Json.fromString("GOODBYE").hcursor) shouldBe Right(GoodBye)
encoder(Hello) shouldBe Json.fromString("Hello")
encoder(GoodBye) shouldBe Json.fromString("GoodBye")
}
}
Loading