Skip to content

Commit

Permalink
Circe support.
Browse files Browse the repository at this point in the history
  • Loading branch information
tarao committed Nov 7, 2023
1 parent a45e044 commit 5deb742
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 2 deletions.
21 changes: 19 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ ThisBuild / githubWorkflowJavaVersions := Seq(
JavaSpec.temurin("17"),
)

val circeVersion = "0.14.6"
val scalaTestVersion = "3.2.17"

lazy val compileSettings = Def.settings(
// Default options are set by sbt-typelevel-settings
tlFatalWarnings := true,
Expand All @@ -53,7 +56,7 @@ lazy val commonSettings = Def.settings(
)

lazy val root = tlCrossRootProject
.aggregate(core)
.aggregate(core, circe)
.settings(commonSettings)
.settings(
console := (core.jvm / Compile / console).value,
Expand All @@ -68,7 +71,21 @@ lazy val core = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.settings(commonSettings)
.settings(
libraryDependencies ++= Seq(
"org.scalatest" %%% "scalatest" % "3.2.17" % Test,
"org.scalatest" %%% "scalatest" % scalaTestVersion % Test,
),
)

lazy val circe = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.crossType(CrossType.Pure)
.dependsOn(core % "compile->compile;test->test")
.in(file("modules/circe"))
.settings(commonSettings)
.settings(
libraryDependencies ++= Seq(
"io.circe" %%% "circe-core" % circeVersion,
"io.circe" %%% "circe-generic" % circeVersion % Test,
"io.circe" %%% "circe-parser" % circeVersion % Test,
"org.scalatest" %%% "scalatest" % scalaTestVersion % Test,
),
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.github.tarao.record4s
package circe

import io.circe.{Decoder, Encoder, HCursor, Json}

object Codec {
inline given encoder[R <: %, RR <: ProductRecord](using
ar: typing.ArrayRecord.Aux[R, RR],
enc: Encoder[RR],
): Encoder[R] = new Encoder[R] {
final def apply(record: R): Json = enc(ArrayRecord.from(record))
}

inline given decoder[R <: %](using
r: RecordLike[R],
dec: Decoder[ArrayRecord[r.TupledFieldTypes]],
c: typing.Record.Concat[%, ArrayRecord[r.TupledFieldTypes]],
ev: c.Out =:= R,
): Decoder[R] = new Decoder[R] {
final def apply(c: HCursor): Decoder.Result[R] =
dec(c).map(ar => ev(ar.toRecord))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package com.github.tarao.record4s
package circe

import io.circe.generic.auto.*
import io.circe.parser.parse
import io.circe.syntax.*

class CodecSpec extends helper.UnitSpec {
describe("ArrayRecord") {
// ArrayRecord can be encoded/decoded without any special codec

describe("encoding") {
it("should encode an array record to json") {
val r = ArrayRecord(name = "tarao", age = 3)
val json = r.asJson.noSpaces
json shouldBe """{"name":"tarao","age":3}"""
}

it("should encode a nested array record to json") {
val r = ArrayRecord(
name = "tarao",
age = 3,
email = ArrayRecord(user = "tarao", domain = "example.com"),
)
val json = r.asJson.noSpaces
json shouldBe """{"name":"tarao","age":3,"email":{"user":"tarao","domain":"example.com"}}"""
}
}

describe("decoding") {
it("should decode json to an array record") {
val json = """{"name":"tarao","age":3}"""
val ShouldBeRight(jsonObj) = parse(json)
val ShouldBeRight(record) =
jsonObj.as[ArrayRecord[(("name", String), ("age", Int))]]
record.name shouldBe "tarao"
record.age shouldBe 3
}

it("should decode json to a nested array record") {
val json =
"""{"name":"tarao","age":3,"email":{"user":"tarao","domain":"example.com"}}"""
val ShouldBeRight(jsonObj) = parse(json)
val ShouldBeRight(record) = jsonObj.as[ArrayRecord[
(
("name", String),
("age", Int),
("email", ArrayRecord[(("user", String), ("domain", String))]),
),
]]
record.name shouldBe "tarao"
record.age shouldBe 3
record.email.user shouldBe "tarao"
record.email.domain shouldBe "example.com"
}

it("can decode partially") {
locally {
val json = """{"name":"tarao","age":3}"""
val ShouldBeRight(jsonObj) = parse(json)
val ShouldBeRight(record) =
jsonObj.as[ArrayRecord[("name", String) *: EmptyTuple]]
record.name shouldBe "tarao"
"record.age" shouldNot typeCheck
}

locally {
val json =
"""{"name":"tarao","age":3,"email":{"user":"tarao","domain":"example.com"}}"""
val ShouldBeRight(jsonObj) = parse(json)
val ShouldBeRight(record) = jsonObj.as[ArrayRecord[
("email", ArrayRecord[("domain", String) *: EmptyTuple]) *:
EmptyTuple,
]]
"record.name" shouldNot typeCheck
"record.age" shouldNot typeCheck
"record.email.user" shouldNot typeCheck
record.email.domain shouldBe "example.com"
}
}
}
}

describe("%") {
import Codec.{decoder, encoder}

describe("encoder") {
it("should encode a record to json") {
val r = %(name = "tarao", age = 3)
val json = r.asJson.noSpaces
json shouldBe """{"name":"tarao","age":3}"""
}

it("should encode a nested record to json") {
val r = %(
name = "tarao",
age = 3,
email = %(user = "tarao", domain = "example.com"),
)
val json = r.asJson.noSpaces
json shouldBe """{"name":"tarao","age":3,"email":{"user":"tarao","domain":"example.com"}}"""
}
}

describe("decoder") {
it("should decode json to a record") {
val json = """{"name":"tarao","age":3}"""
val ShouldBeRight(jsonObj) = parse(json)
val ShouldBeRight(record) =
jsonObj.as[% { val name: String; val age: Int }]
record.name shouldBe "tarao"
record.age shouldBe 3
}

it("should decode json to a nested record") {
val json =
"""{"name":"tarao","age":3,"email":{"user":"tarao","domain":"example.com"}}"""
val ShouldBeRight(jsonObj) = parse(json)
val ShouldBeRight(record) = jsonObj.as[
% {
val name: String; val age: Int;
val email: % { val user: String; val domain: String }
},
]
record.name shouldBe "tarao"
record.age shouldBe 3
record.email.user shouldBe "tarao"
record.email.domain shouldBe "example.com"
}

it("can decode partially") {
locally {
val json = """{"name":"tarao","age":3}"""
val ShouldBeRight(jsonObj) = parse(json)
val ShouldBeRight(record) = jsonObj.as[% { val name: String }]
record.name shouldBe "tarao"
"record.age" shouldNot typeCheck
}

locally {
val json =
"""{"name":"tarao","age":3,"email":{"user":"tarao","domain":"example.com"}}"""
val ShouldBeRight(jsonObj) = parse(json)
val ShouldBeRight(record) =
jsonObj.as[% { val email: % { val domain: String } }]
"record.name" shouldNot typeCheck
"record.age" shouldNot typeCheck
"record.email.user" shouldNot typeCheck
record.email.domain shouldBe "example.com"
}
}
}
}
}
12 changes: 12 additions & 0 deletions modules/core/src/test/scala/helper/EitherValues.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package helper

trait EitherValues {
self: org.scalatest.Assertions =>

object ShouldBeRight {
def unapply[A, B](x: Either[A, B]): Some[B] = x match {
case Right(value) => Some(value)
case _ => fail(s"$x was not Right")
}
}
}
1 change: 1 addition & 0 deletions modules/core/src/test/scala/helper/UnitSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@ abstract class UnitSpec
with matchers.should.Matchers
with StaticTypeMatcher
with OptionValues
with EitherValues
with Inside
with Inspectors

0 comments on commit 5deb742

Please sign in to comment.