Skip to content

Commit

Permalink
Implement upickle ReadWriter.
Browse files Browse the repository at this point in the history
  • Loading branch information
tarao committed Dec 20, 2023
1 parent b18b55b commit c9a0f1d
Show file tree
Hide file tree
Showing 6 changed files with 334 additions and 2 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,11 @@ jobs:

- name: Make target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v'))
run: mkdir -p modules/core/.native/target modules/core/.js/target modules/circe/.js/target modules/circe/.jvm/target modules/circe/.native/target modules/core/.jvm/target project/target
run: mkdir -p modules/upickle/.js/target modules/core/.native/target modules/core/.js/target modules/circe/.js/target modules/upickle/.jvm/target modules/circe/.jvm/target modules/circe/.native/target modules/core/.jvm/target modules/upickle/.native/target project/target

- name: Compress target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v'))
run: tar cf targets.tar modules/core/.native/target modules/core/.js/target modules/circe/.js/target modules/circe/.jvm/target modules/circe/.native/target modules/core/.jvm/target project/target
run: tar cf targets.tar modules/upickle/.js/target modules/core/.native/target modules/core/.js/target modules/circe/.js/target modules/upickle/.jvm/target modules/circe/.jvm/target modules/circe/.native/target modules/core/.jvm/target modules/upickle/.native/target project/target

- name: Upload target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v'))
Expand Down
13 changes: 13 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,19 @@ lazy val circe = crossProject(JVMPlatform, JSPlatform, NativePlatform)
),
)

lazy val upickle = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.crossType(CrossType.Pure)
.withoutSuffixFor(JVMPlatform)
.dependsOn(core % "compile->compile;test->test")
.asModule
.settings(commonSettings)
.settings(
description := "uPickle / uJson integration for record4s",
libraryDependencies ++= Seq(
"com.lihaoyi" %%% "upickle" % "3.1.3",
),
)

lazy val benchmark_3 = (project in file("modules/benchmark_3"))
.dependsOn(core.jvm)
.settings(commonSettings)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2023 record4s authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.github.tarao
package record4s
package upickle

import _root_.upickle.default.{ReadWriter, readwriter}
import upickle.Record.{readDict, writeDict}

object ArrayRecord {
inline given readWriter[T <: Tuple](using
r: RecordLike[record4s.ArrayRecord[T]],
): ReadWriter[record4s.ArrayRecord[T]] = {
type Types = r.ElemTypes
type Labels = r.ElemLabels

readwriter[ujson.Value].bimap[ArrayRecord[T]](
record => ujson.Obj(writeDict[Types, Labels](r.iterableOf(record).toMap)),
json => {
val dict = json.obj
val iterable = readDict[Types, Labels](dict)
record4s.ArrayRecord.newArrayRecord[ArrayRecord[T]](iterable)
},
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright 2023 record4s authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.github.tarao
package record4s
package upickle

import _root_.upickle.core.LinkedHashMap
import _root_.upickle.default.{
ReadWriter,
Reader,
Writer,
read,
readwriter,
writeJs,
}

import scala.compiletime.{constValue, erasedValue, summonInline}
import scala.util.NotGiven

object Record {
private[upickle] inline def writeDict[Types, Labels](
record: Map[String, Any],
res: LinkedHashMap[String, ujson.Value] = LinkedHashMap(),
): LinkedHashMap[String, ujson.Value] =
inline (erasedValue[Types], erasedValue[Labels]) match {
case _: (EmptyTuple, EmptyTuple) =>
res

case _: (tpe *: types, label *: labels) =>
val labelStr = constValue[label & String]
val value =
inline erasedValue[tpe] match {
case _: ujson.Value =>
record(labelStr).asInstanceOf[ujson.Value]
case _ =>
val writer = summonInline[Writer[tpe]]
val elem = record(labelStr).asInstanceOf[tpe]
writeJs[tpe](elem)(using writer)
}
writeDict[types, labels](record, res += (labelStr -> value))
}

private[upickle] inline def readDict[Types, Labels](
dict: LinkedHashMap[String, ujson.Value],
res: Vector[(String, Any)] = Vector.empty,
): Vector[(String, Any)] =
inline (erasedValue[Types], erasedValue[Labels]) match {
case _: (EmptyTuple, EmptyTuple) =>
res

case _: (tpe *: types, label *: labels) =>
val labelStr = constValue[label & String]
val jsonElem = dict.getOrElse(labelStr, ujson.Null)
val reader = summonInline[Reader[tpe]]
val elem = read[tpe](jsonElem)(using reader)
readDict[types, labels](dict, res :+ (labelStr, elem))
}

inline given readWriter[R <: %](using
r: RecordLike[R],
nonTuple: NotGiven[R <:< Tuple],
): ReadWriter[R] = {
type Types = r.ElemTypes
type Labels = r.ElemLabels

readwriter[ujson.Value].bimap[R](
record => ujson.Obj(writeDict[Types, Labels](r.iterableOf(record).toMap)),
json => {
val dict = json.obj
val iterable = readDict[Types, Labels](dict)
record4s.Record.newMapRecord[R](iterable)
},
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright 2023 record4s authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.github.tarao
package record4s
package upickle

class ArrayRecordSpec extends helper.UnitSpec {
describe("ArrayRecord.readWriter") {
import upickle.ArrayRecord.readWriter

describe("write") {
import _root_.upickle.default.{write, writeJs}

it("should write a record") {
val r = record4s.ArrayRecord(name = "tarao", age = 3)
write(r) shouldBe """{"name":"tarao","age":3}"""
writeJs(r) shouldBe ujson.Obj(
"name" -> ujson.Str("tarao"),
"age" -> ujson.Num(3),
)
}

it("should write a nested record") {
val r = record4s.ArrayRecord(
name = "tarao",
age = 3,
email = record4s.ArrayRecord(user = "tarao", domain = "example.com"),
)
write(
r,
) shouldBe """{"name":"tarao","age":3,"email":{"user":"tarao","domain":"example.com"}}"""
writeJs(r) shouldBe ujson.Obj(
"name" -> ujson.Str("tarao"),
"age" -> ujson.Num(3),
"email" -> ujson.Obj(
"user" -> ujson.Str("tarao"),
"domain" -> ujson.Str("example.com"),
),
)
}
}

describe("read") {
import _root_.upickle.default.read

it("should read a record") {
val json = """{"name":"tarao","age":3}"""
val r =
read[record4s.ArrayRecord[(("name", String), ("age", Int))]](json)
r shouldStaticallyBe a[
record4s.ArrayRecord[(("name", String), ("age", Int))],
]
r.name shouldBe "tarao"
r.age shouldBe 3
}

it("should read a nested record") {
val json =
"""{"name":"tarao","age":3,"email":{"user":"tarao","domain":"example.com"}}"""
val r = read[record4s.ArrayRecord[
(
("name", String),
("age", Int),
(
"email",
record4s.ArrayRecord[(("user", String), ("domain", String))],
),
),
]](json)
r shouldStaticallyBe a[record4s.ArrayRecord[
(
("name", String),
("age", Int),
(
"email",
record4s.ArrayRecord[(("user", String), ("domain", String))],
),
),
]]
r.name shouldBe "tarao"
r.age shouldBe 3
r.email.user shouldBe "tarao"
r.email.domain shouldBe "example.com"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright 2023 record4s authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.github.tarao.record4s
package upickle

class RecordSpec extends helper.UnitSpec {
describe("Record.readWriter") {
import upickle.Record.readWriter

describe("write") {
import _root_.upickle.default.{write, writeJs}

it("should write a record") {
val r = %(name = "tarao", age = 3)
write(r) shouldBe """{"name":"tarao","age":3}"""
writeJs(r) shouldBe ujson.Obj(
"name" -> ujson.Str("tarao"),
"age" -> ujson.Num(3),
)
}

it("should write a nested record") {
val r = %(
name = "tarao",
age = 3,
email = %(user = "tarao", domain = "example.com"),
)
write(
r,
) shouldBe """{"name":"tarao","age":3,"email":{"user":"tarao","domain":"example.com"}}"""
writeJs(r) shouldBe ujson.Obj(
"name" -> ujson.Str("tarao"),
"age" -> ujson.Num(3),
"email" -> ujson.Obj(
"user" -> ujson.Str("tarao"),
"domain" -> ujson.Str("example.com"),
),
)
}
}

describe("read") {
import _root_.upickle.default.read

it("should read a record") {
val json = """{"name":"tarao","age":3}"""
val r = read[% { val name: String; val age: Int }](json)
r shouldStaticallyBe a[% { val name: String; val age: Int }]
r.name shouldBe "tarao"
r.age shouldBe 3
}

it("should read a nested record") {
val json =
"""{"name":"tarao","age":3,"email":{"user":"tarao","domain":"example.com"}}"""
val r = read[
% {
val name: String; val age: Int;
val email: % { val user: String; val domain: String }
},
](json)
r shouldStaticallyBe a[
% {
val name: String; val age: Int;
val email: % { val user: String; val domain: String }
},
]
r.name shouldBe "tarao"
r.age shouldBe 3
r.email.user shouldBe "tarao"
r.email.domain shouldBe "example.com"
}
}
}
}

0 comments on commit c9a0f1d

Please sign in to comment.