Skip to content

Commit

Permalink
Merge pull request #28 from tarao/circe
Browse files Browse the repository at this point in the history
Circe support
  • Loading branch information
tarao authored Nov 8, 2023
2 parents a45e044 + 2ff832b commit 6734edc
Show file tree
Hide file tree
Showing 9 changed files with 336 additions and 11 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -169,5 +169,5 @@ jobs:
- name: Submit Dependencies
uses: scalacenter/sbt-dependency-submission@v2
with:
modules-ignore: record4s_3 benchmark_2_11_2.11 record4s_3 benchmark_2_13_2.13 record4s_3 record4s_3
modules-ignore: benchmark_3_3 benchmark_2_11_2.11 rootjs_3 benchmark_2_13_2.13 rootjvm_3 rootnative_3
configs-ignore: test scala-tool scala-doc-tool test-internal
7 changes: 7 additions & 0 deletions .jvmopts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-Dfile.encoding=UTF8
-Xms1G
-Xmx6G
-XX:MaxMetaspaceSize=512M
-XX:ReservedCodeCacheSize=250M
-XX:+TieredCompilation
-XX:-UseGCOverheadLimit
41 changes: 31 additions & 10 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
val groupId = "com.github.tarao"
val projectName = "record4s"
val rootPkg = s"$groupId.$projectName"
import ProjectKeys._
import Implicits._

ThisBuild / organization := groupId
ThisBuild / projectName := "record4s"
ThisBuild / groupId := "com.github.tarao"
ThisBuild / rootPkg := "${groupId.value}.${projectName.value}"

ThisBuild / organization := groupId.value
ThisBuild / organizationName := "record4s authors"
ThisBuild / startYear := Some(2023)
ThisBuild / licenses := Seq(License.MIT)
Expand All @@ -12,8 +15,7 @@ ThisBuild / developers := List(
)

lazy val metadataSettings = Def.settings(
name := projectName,
organization := groupId,
organization := groupId.value,
description := "Extensible records for Scala",
homepage := Some(url("https://github.com/tarao/record4s")),
)
Expand All @@ -30,6 +32,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 @@ -48,12 +53,12 @@ lazy val commonSettings = Def.settings(
metadataSettings,
compileSettings,
initialCommands := s"""
import $rootPkg.*
import ${rootPkg.value}.*
""",
)

lazy val root = tlCrossRootProject
.aggregate(core)
.aggregate(core, circe)
.settings(commonSettings)
.settings(
console := (core.jvm / Compile / console).value,
Expand All @@ -64,11 +69,27 @@ lazy val root = tlCrossRootProject
lazy val core = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.crossType(CrossType.Pure)
.withoutSuffixFor(JVMPlatform)
.in(file("modules/core"))
.asModuleWithoutSuffix
.settings(commonSettings)
.settings(
libraryDependencies ++= Seq(
"org.scalatest" %%% "scalatest" % scalaTestVersion % Test,
),
)

lazy val circe = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.crossType(CrossType.Pure)
.withoutSuffixFor(JVMPlatform)
.dependsOn(core % "compile->compile;test->test")
.asModule
.settings(commonSettings)
.settings(
description := "Circe integration for record4s",
libraryDependencies ++= Seq(
"org.scalatest" %%% "scalatest" % "3.2.17" % Test,
"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,44 @@
/*
* Copyright (c) 2023 record4s authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

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,175 @@
/*
* Copyright (c) 2023 record4s authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

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"
}
}
}
}
}
33 changes: 33 additions & 0 deletions modules/core/src/test/scala/helper/EitherValues.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2023 record4s authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

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
37 changes: 37 additions & 0 deletions project/Implicits.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import sbt._
import sbt.Keys._
import sbtcrossproject.{CrossPlugin, CrossProject}
import scala.language.implicitConversions
import ProjectKeys.projectName

object Implicits {
implicit class CrossProjectOps(private val p: CrossProject) extends AnyVal {
def asModuleWithoutSuffix: CrossProject = asModule(true)

def asModule: CrossProject = asModule(false)

private def asModule(noSuffix: Boolean): CrossProject = {
val project = p.componentProjects(0)
val s = project.settings(0)
p
.settings(
moduleName := {
if (noSuffix)
(ThisBuild / projectName).value
else
s"${(ThisBuild / projectName).value}-${(project / name).value}"
},
CrossPlugin.autoImport.crossProjectBaseDirectory := {
val dir = file(s"modules/${(project / name).value}")
IO.resolve((LocalRootProject / baseDirectory).value, dir)
},
)
.configure(project =>
project.in(file("modules") / project.base.getPath),
)
}
}

implicit def builderOps(b: CrossProject.Builder): CrossProjectOps =
new CrossProjectOps(b.build())
}
Loading

0 comments on commit 6734edc

Please sign in to comment.