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

Unbox first level of options when serializing JSON #598

Merged
merged 8 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions upickle/implicits/src/upickle/implicits/MacrosCommon.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ trait MacrosCommon {
def objectTypeKeyReadMap(s: CharSequence): CharSequence = s
def objectTypeKeyWriteMap(s: CharSequence): CharSequence = s

def optionsAsNulls: Boolean = true
}

object MacrosCommon {
Expand Down
31 changes: 20 additions & 11 deletions upickle/implicits/src/upickle/implicits/Readers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -311,19 +311,28 @@ trait Readers extends upickle.core.Types
}
}

implicit def OptionReader[T: Reader]: Reader[Option[T]] = new SimpleReader[Option[T]] {
override def expectedMsg = "expected sequence"
override def visitArray(length: Int, index: Int) = new ArrVisitor[Any, Option[T]] {
var b: Option[T] = None

def visitValue(v: Any, index: Int): Unit = {
b = Some(v.asInstanceOf[T])
}
trait OptionReader[T] extends Reader[Option[T]]
@scala.annotation.nowarn("cat=unchecked")
implicit def OptionReader[T: Reader]: Reader[Option[T]] = implicitly[Reader[T]] match{
case inner if inner.isInstanceOf[OptionReader[T]] || !optionsAsNulls =>
new SimpleReader[Option[T]] with OptionReader[T]{
override def expectedMsg = "expected sequence"
override def visitArray(length: Int, index: Int) = new ArrVisitor[Any, Option[T]] {
var b: Option[T] = None

def visitValue(v: Any, index: Int): Unit = {
b = Some(v.asInstanceOf[T])
}

def visitEnd(index: Int) = b
def visitEnd(index: Int) = b

def subVisitor = implicitly[Reader[T]]
}
def subVisitor = inner
}
}
case inner =>
new Reader.Delegate[Any, Option[T]](implicitly[Reader[T]].map(Some(_))) with OptionReader[T]{
override def visitNull(index: Int) = None
}
}
implicit def SomeReader[T: Reader]: Reader[Some[T]] = OptionReader[T].narrow[Some[T]]
implicit def NoneReader: Reader[None.type] = OptionReader[Unit].narrow[None.type]
Expand Down
39 changes: 28 additions & 11 deletions upickle/implicits/src/upickle/implicits/Writers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -80,18 +80,35 @@ trait Writers extends upickle.core.Types
override def writeString(v: Symbol) = v.name
}

implicit def OptionWriter[T: Writer]: Writer[Option[T]] = new Writer[Option[T]] {
def write0[V](out: Visitor[_, V], v: Option[T]) = {
val ctx = out.visitArray(v.size, -1).narrow
val x = v.iterator
v match{
case None =>
case Some(next) =>
val written = implicitly[Writer[T]].write(ctx.subVisitor, next)
ctx.visitValue(written, -1)
}
trait OptionWriter[T] extends Writer[Option[T]]
@scala.annotation.nowarn("cat=unchecked")
implicit def OptionWriter[T: Writer]: Writer[Option[T]] = {
implicitly[Writer[T]] match{
case inner if inner.isInstanceOf[OptionWriter[T]] || !optionsAsNulls =>
new OptionWriter[T]{
def write0[V](out: Visitor[_, V], v: Option[T]) = {
val ctx = out.visitArray(v.size, -1).narrow
v match{
case None =>
case Some(next) =>
val written = inner.write(ctx.subVisitor, next)
ctx.visitValue(written, -1)
}

ctx.visitEnd(-1)
}
}

case inner =>
new OptionWriter[T]{
def write0[V](out: Visitor[_, V], v: Option[T]) = {
v match{
case None => out.visitNull(-1)
case Some(next) => inner.write(out, next)
}
}
}

ctx.visitEnd(-1)
}
}

Expand Down
6 changes: 3 additions & 3 deletions upickle/test/src-3/upickle/DerivationTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,9 @@ object DerivationTests extends TestSuite {
}

test("recursive"){
case class Recur(recur: Option[Recur]) derives ReadWriter
rw(Recur(None), """{"recur":[]}""")
rw(Recur(Some(Recur(None))), """{"recur":[{"recur": []}]}""")
case class Recur(recur: Option[Recur] = None) derives ReadWriter
rw(Recur(None), """{}""")
rw(Recur(Some(Recur(None))), """{"recur":{}}""")
}
test("multilevel"){
rw(Level1Cls(1), """{"$type": "upickle.Level1Cls", "i": 1}""")
Expand Down
6 changes: 4 additions & 2 deletions upickle/test/src-3/upickle/EnumTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ enum ColorEnum(val rgb: Int) derives ReadWriter:
end ColorEnum


case class Enclosing(str: String, simple1: SimpleEnum, simple2: Option[SimpleEnum]) derives ReadWriter
case class Enclosing(str: String,
simple1: SimpleEnum,
simple2: Option[SimpleEnum] = None) derives ReadWriter

enum LinkedList[+T] derives ReadWriter:
case End
Expand Down Expand Up @@ -65,7 +67,7 @@ object EnumTests extends TestSuite {
test("enclosingWrite") - {
rw(
Enclosing("test", SimpleEnum.A, Some(SimpleEnum.B)),
"""{"str":"test","simple1":"A","simple2":["B"]}"""
"""{"str":"test","simple1":"A","simple2":"B"}"""
)
}
}
Expand Down
6 changes: 4 additions & 2 deletions upickle/test/src/upickle/AdvancedTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -335,15 +335,17 @@ object AdvancedTests extends TestSuite {
// }

test("issue-371") {
val input = """{"head":"a","tail":[{"head":"b","tail":[]}]}"""
val input = """{"head":"a","tail":{"head":"b"}}"""
val expected = Node371("a", Some(Node371("b", None)))
val result = upickle.default.read[Node371](input)
assert(result == expected)
val written = upickle.default.write(result)
assert(written == input)
}
}
}

case class Node371(head: String, tail: Option[Node371])
case class Node371(head: String, tail: Option[Node371] = None)

object Node371 {
implicit val nodeRW: ReadWriter[Node371] = macroRW[Node371]
Expand Down
2 changes: 1 addition & 1 deletion upickle/test/src/upickle/LegacyTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ object LegacyTests extends TestSuite {

test - rw(
ADTs.ADTf(1, "lol", (1.1, 1.2), ADTs.ADTa(1), List(1.2, 2.1, 3.14), Some(None)),
"""{"i":1,"s":"lol","t":[1.1,1.2],"a":{"i":1},"q":[1.2,2.1,3.14],"o":[[]]}"""
"""{"i":1,"s":"lol","t":[1.1,1.2],"a":{"i":1},"q":[1.2,2.1,3.14],"o":[null]}"""
)
val chunks = for (i <- 1 to 18) yield {
val rhs = if (i % 2 == 1) "1" else "\"1\""
Expand Down
4 changes: 2 additions & 2 deletions upickle/test/src/upickle/MacroTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ object MacroTests extends TestSuite {

test - rw(
ADTs.ADTf(1, "lol", (1.1, 1.2), ADTs.ADTa(1), List(1.2, 2.1, 3.14), Some(None)),
"""{"i":1,"s":"lol","t":[1.1,1.2],"a":{"i":1},"q":[1.2,2.1,3.14],"o":[[]]}""",
"""{"i":1,"s":"lol","t":[1.1,1.2],"a":{"i":1},"q":[1.2,2.1,3.14],"o":[null]}""",
upack.Obj(
upack.Str("i") -> upack.Int32(1),
upack.Str("s") -> upack.Str("lol"),
Expand All @@ -221,7 +221,7 @@ object MacroTests extends TestSuite {
upack.Float64(2.1),
upack.Float64(3.14)
),
upack.Str("o") -> upack.Arr(upack.Arr())
upack.Str("o") -> upack.Arr(upack.Null)
)
)
val chunks = for (i <- 1 to 18) yield {
Expand Down
54 changes: 27 additions & 27 deletions upickle/test/src/upickle/StructTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -444,11 +444,11 @@ object StructTests extends TestSuite {
}

test("option"){
test("Some") - rw(Some(123), "[123]", upack.Arr(upack.Int32(123)))
test("None") - rw(None, "[]", upack.Arr())
test("Some") - rw(Some(123), "123", upack.Int32(123))
test("None") - rw(None, "null", upack.Null)
test("Option"){
rw(Some(123): Option[Int], "[123]", upack.Arr(upack.Int32(123)))
rw(None: Option[Int], "[]", upack.Arr())
rw(Some(123): Option[Int], "123", upack.Int32(123))
rw(None: Option[Int], "null", upack.Null)
}
}

Expand Down Expand Up @@ -480,35 +480,35 @@ object StructTests extends TestSuite {
test("combinations"){
test("SeqListMapOptionString") - rw[Seq[List[Map[Option[String], String]]]](
Seq(Nil, List(Map(Some("omg") -> "omg"), Map(Some("lol") -> "lol", None -> "")), List(Map())),
"""[[],[[[["omg"],"omg"]],[[["lol"],"lol"],[[],""]]],[[]]]""",
"""[[],[[["omg","omg"]],[["lol","lol"],[null,""]]],[[]]]""",
upack.Arr(
upack.Arr(),
upack.Arr(
upack.Obj(upack.Arr(upack.Str("omg")) -> upack.Str("omg")),
upack.Obj(upack.Str("omg") -> upack.Str("omg")),
upack.Obj(
upack.Arr(upack.Str("lol")) -> upack.Str("lol"),
upack.Arr() -> upack.Str("")
upack.Str("lol") -> upack.Str("lol"),
upack.Null -> upack.Str("")
)
),
upack.Arr(upack.Obj())
)
)

test("NullySeqListMapOptionString") - rw[Seq[List[Map[Option[String], String]]]](
Seq(Nil, List(Map(Some(null) -> "omg"), Map(Some("lol") -> null, None -> "")), List(null)),
"""[[],[[[[null],"omg"]],[[["lol"],null],[[],""]]],[null]]""",
upack.Arr(
upack.Arr(),
upack.Arr(
upack.Obj(upack.Arr(upack.Null) -> upack.Str("omg")),
upack.Obj(
upack.Arr(upack.Str("lol")) -> upack.Null,
upack.Arr() -> upack.Str("")
)
),
upack.Arr(upack.Null)
)
)
// test("NullySeqListMapOptionString") - rw[Seq[List[Map[Option[String], String]]]](
lihaoyi marked this conversation as resolved.
Show resolved Hide resolved
// Seq(Nil, List(Map(Some(null) -> "omg"), Map(Some("lol") -> null, None -> "")), List(null)),
// """[[],[[[[null],"omg"]],[[["lol"],null],[[],""]]],[null]]""",
// upack.Arr(
// upack.Arr(),
// upack.Arr(
// upack.Obj(upack.Arr(upack.Null) -> upack.Str("omg")),
// upack.Obj(
// upack.Arr(upack.Str("lol")) -> upack.Null,
// upack.Arr() -> upack.Str("")
// )
// ),
// upack.Arr(upack.Null)
// )
// )

test("tuples") - rw(
(1, (2.0, true), (3.0, 4.0, 5.0)),
Expand All @@ -529,8 +529,8 @@ object StructTests extends TestSuite {
)
rw(
Right(Some(0.33 millis)): Either[Int, Option[Duration]],
"""[1,["330000"]]""",
upack.Arr(upack.Float64(1.0), upack.Arr(upack.Str("330000")))
"""[1,"330000"]""",
upack.Arr(upack.Float64(1.0), upack.Str("330000"))
)
rw(
Left(10 seconds): Either[Duration, Option[Duration]],
Expand All @@ -539,8 +539,8 @@ object StructTests extends TestSuite {
)
rw(
Right(Some(0.33 millis)): Either[Duration, Option[Duration]],
"""[1,["330000"]]""",
upack.Arr(upack.Float64(1.0), upack.Arr(upack.Str("330000")))
"""[1,"330000"]""",
upack.Arr(upack.Float64(1.0), upack.Str("330000"))
)
}
}
Expand Down
123 changes: 123 additions & 0 deletions upickle/test/src/upickle/example/BoxedOptionsTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package upickle.example

import utest._
import upickle.example.Simple.Thing
import scala.language.implicitConversions

case class Opt(a: Option[String], b: Option[Int])
object Opt{
implicit def rw: BoxedOptionsPickler.ReadWriter[Opt] = BoxedOptionsPickler.macroRW
}
object BoxedOptionsPickler extends upickle.AttributeTagged {
override def optionsAsNulls = false
}
// end_ex

object BoxedOptionsTests extends TestSuite {

import BoxedOptionsPickler._
implicit def rw: BoxedOptionsPickler.ReadWriter[Thing] = BoxedOptionsPickler.macroRW
val tests = TestSuite {
test("nullAsNone"){

// Quick check to ensure we didn't break anything
test("primitive"){
write("A String") ==> "\"A String\""
read[String]("\"A String\"") ==> "A String"
write(1) ==> "1"
read[Int]("1") ==> 1
write(Thing(1, "gg")) ==> """{"myFieldA":1,"myFieldB":"gg"}"""
read[Thing]("""{"myFieldA":1,"myFieldB":"gg"}""") ==> Thing(1, "gg")
}

test("none"){
write[None.type](None) ==> "[]"
read[None.type]("[]") ==> None
}

test("some"){
write(Some("abc")) ==> "[\"abc\"]"
read[Some[String]]("[\"abc\"]") ==> Some("abc")
write(Some(1)) ==> "[1]"
read[Some[Int]]("[1]") ==> Some(1)
write(Some(3.14159)) ==> "[3.14159]"
read[Some[Double]]("[3.14159]") ==> Some(3.14159)
}

test("option"){
write(Option("abc")) ==> "[\"abc\"]"
read[Option[String]]("[\"abc\"]") ==> Some("abc")
read[Option[String]]("[]") ==> None
}

test("caseClass"){
write(Opt(None, None)) ==> """{"a":[],"b":[]}"""
read[Opt]("""{"a":[],"b":[]}""") ==> Opt(None, None)
write(Opt(Some("abc"), Some(1))) ==> """{"a":["abc"],"b":[1]}"""
}

test("optionCaseClass"){
write(Opt(None, None)) ==> """{"a":[],"b":[]}"""
read[Opt]("""{"a":[],"b":[]}""") ==> Opt(None, None)
write(Opt(Some("abc"), Some(1))) ==> """{"a":["abc"],"b":[1]}"""

write(Option(Thing(1, "gg"))) ==> """[{"myFieldA":1,"myFieldB":"gg"}]"""
read[Option[Thing]]("""[{"myFieldA":1,"myFieldB":"gg"}]""") ==> Option(Thing(1, "gg"))
}

// New tests. Work as expected.
test("customPickler") {
// Custom pickler copied from the documentation
class CustomThing2(val i: Int, val s: String)

object CustomThing2 {
implicit val rw: BoxedOptionsPickler.ReadWriter[CustomThing2] = BoxedOptionsPickler.readwriter[String].bimap[CustomThing2](
x => s"${x.i} ${x.s}",
str => {
val Array(i, s) = str.split(" ", 2)
new CustomThing2(i.toInt, s)
}
)
}

test("customClass") {
write(new CustomThing2(10, "Custom")) ==> "\"10 Custom\""
val r = read[CustomThing2]("\"10 Custom\"")
assert(r.i == 10, r.s == "Custom")
}

test("optCustomClass_Some") {
write(Some(new CustomThing2(10, "Custom"))) ==> "[\"10 Custom\"]"
val r = read[Option[CustomThing2]]("[\"10 Custom\"]")
assert(r.get.i == 10, r.get.s == "Custom")
}

test("optCustomClass_None") {
read[Option[CustomThing2]]("[]") ==> None
}
}

// Copied from ExampleTests
test("Js") {
import BoxedOptionsPickler._ // changed from upickle.default._
case class Bar(i: Int, s: String)
implicit val fooReadWrite: ReadWriter[Bar] =
readwriter[ujson.Value].bimap[Bar](
x => ujson.Arr(x.s, x.i),
json => new Bar(json(1).num.toInt, json(0).str)
)

write(Bar(123, "abc")) ==> """["abc",123]"""
read[Bar]("""["abc",123]""") ==> Bar(123, "abc")

// New tests. Last one fails. Why?
test("option") {
test("write") {write(Some(Bar(123, "abc"))) ==> """[["abc",123]]"""}
test("readSome") {read[Option[Bar]]("""[["abc",123]]""") ==> Some(Bar(123, "abc"))}
test("readNull") {read[Option[Bar]]("""[]""") ==> None}
}
}

}
}
}
Loading
Loading