Skip to content

Commit

Permalink
Merge pull request #274 from theiterators/circe-support
Browse files Browse the repository at this point in the history
kebs-circe - Scala 3 support
  • Loading branch information
pk044 committed Apr 21, 2023
2 parents f5e8a6c + 7d2b896 commit b42d99e
Show file tree
Hide file tree
Showing 26 changed files with 1,322 additions and 347 deletions.
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
File renamed without changes.
File renamed without changes.
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
File renamed without changes.
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

0 comments on commit b42d99e

Please sign in to comment.