Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions docs/content/reference/named-tuples.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,13 @@ val str = """ { "name": "Mujo", "age": 35 } """
val nt2 = str.parseJson[Person]
// (name = "Mujo", age = 35)
```

Singleton literal members are supported too:

```scala
type Marker = (kind: "abc")

val marker: Marker = (kind = "abc")
assert(marker.toJson(spaces = 0) == """{"kind":"abc"}""")
assert("""{"kind":"abc"}""".parseJson[Marker] == marker)
```
53 changes: 53 additions & 0 deletions tupson/src/ba/sake/tupson/instances.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ba.sake.tupson

import scala.collection.mutable.ArrayDeque
import scala.compiletime.summonInline
import scala.compiletime.summonFrom
import NamedTuple.AnyNamedTuple
import NamedTuple.Names
import NamedTuple.DropNames
Expand All @@ -15,6 +16,58 @@ import org.typelevel.jawn.ast.*

private[tupson] trait LowPriorityJsonRWInstances {

inline given autoderiveLiteral[
T <: String | Char | Boolean | Int | Long | Float | Double
](using valueOf: ValueOf[T]): JsonRW[T] =
summonFrom {
case ev: (T <:< String) => literalRW[T, String](using valueOf, ev, summonInline[JsonRW[String]])
case ev: (T <:< Char) => literalCharRW[T](using valueOf, ev)
case ev: (T <:< Boolean) => literalRW[T, Boolean](using valueOf, ev, summonInline[JsonRW[Boolean]])
case ev: (T <:< Int) => literalRW[T, Int](using valueOf, ev, summonInline[JsonRW[Int]])
case ev: (T <:< Long) => literalRW[T, Long](using valueOf, ev, summonInline[JsonRW[Long]])
case ev: (T <:< Float) => literalRW[T, Float](using valueOf, ev, summonInline[JsonRW[Float]])
case ev: (T <:< Double) => literalRW[T, Double](using valueOf, ev, summonInline[JsonRW[Double]])
}

private inline def literalRW[T, Wide](using
valueOf: ValueOf[T],
ev: T <:< Wide,
rw: JsonRW[Wide]
): JsonRW[T] =
new JsonRW[T] {
private val expectedValue = valueOf.value
private val expectedMsg = s"should be literal value '$expectedValue'"

override def write(value: T): JValue = rw.write(ev(value))

override def parse(path: String, jValue: JValue): T =
val parsed = rw.parse(path, jValue)
if parsed == expectedValue then expectedValue
else throw ParsingException(ParseError(path, expectedMsg, Some(parsed)))
}

private inline def literalCharRW[T](using
valueOf: ValueOf[T],
ev: T <:< Char
): JsonRW[T] =
new JsonRW[T] {
private val expectedValue = valueOf.value
private val expectedChar = ev(expectedValue)
private val expectedMsg = s"should be literal value '$expectedValue'"

override def write(value: T): JValue = JsonRW[Char].write(ev(value))

override def parse(path: String, jValue: JValue): T =
jValue match
case JString(s) if s.length == 1 =>
if s.head == expectedChar then expectedValue
else throw ParsingException(ParseError(path, expectedMsg, Some(s.head)))
case JString(s) =>
throw ParsingException(ParseError(path, expectedMsg, Some(s)))
case other =>
JsonRW.typeMismatchError(path, "Char", other)
}

inline given autoderiveNamedTuple[T <: AnyNamedTuple](using T <:< AnyNamedTuple): JsonRW[T] = {
val fieldNames = compiletime.constValueTuple[Names[T]].productIterator.asInstanceOf[Iterator[String]].toSeq
val fieldJsonRWs =
Expand Down
58 changes: 58 additions & 0 deletions tupson/test/src/ba/sake/tupson/ParseSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,64 @@ class ParseSuite extends munit.FunSuite {
assertEquals(nt2, (name = "Mujo", age = 35))
}

test("parse literal types") {
assertEquals("""{"x":"abc"}""".parseJson[LiteralPerson], (x = "abc"))
assertEquals(
"""{"x":"abc"}""".parseJson[LiteralStringCaseClass],
LiteralStringCaseClass("abc")
)
assertEquals("""{"x":123}""".parseJson[LiteralIntCaseClass], LiteralIntCaseClass(123))
assertEquals(
"""{"x":true}""".parseJson[LiteralBooleanCaseClass],
LiteralBooleanCaseClass(true)
)
assertEquals("""{"x":"a"}""".parseJson[LiteralCharCaseClass], LiteralCharCaseClass('a'))

assertEquals(""" "abc" """.parseJson[LiteralUnion], "abc")
assertEquals(""" 123 """.parseJson[LiteralUnion], 123)
assertEquals(""" true """.parseJson[LiteralUnion], true)

val ex = intercept[ParsingException] {
"""{"x":"def"}""".parseJson[LiteralStringCaseClass]
}
assertEquals(
ex.errors,
Seq(
ParseError("$.x", "should be literal value 'abc'", Some("def"))
)
)

val tupleEx = intercept[ParsingException] {
"""{"x":"def"}""".parseJson[LiteralPerson]
}
assertEquals(
tupleEx.errors,
Seq(
ParseError("$.x", "should be literal value 'abc'", Some("def"))
)
)

val charEx = intercept[ParsingException] {
"""{"x":"ab"}""".parseJson[LiteralCharCaseClass]
}
assertEquals(
charEx.errors,
Seq(
ParseError("$.x", "should be literal value 'a'", Some("ab"))
)
)

val unionEx = intercept[ParsingException] {
""" false """.parseJson[LiteralUnion]
}
assertEquals(
unionEx.errors,
Seq(
ParseError("$", "should be literal value 'true'", Some(false))
)
)
}

/* union type */
test("parse union type") {
// import unionTypes.given
Expand Down
23 changes: 23 additions & 0 deletions tupson/test/src/ba/sake/tupson/WriteSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,29 @@ class WriteSuite extends munit.FunSuite {
)
}

test("write literal types") {
val nt: LiteralPerson = (x = "abc")
assertEquals(nt.toJson(spaces = 0, sort = true), """{"x":"abc"}""")

assertEquals(
LiteralStringCaseClass("abc").toJson(spaces = 0, sort = true),
"""{"x":"abc"}"""
)
assertEquals(LiteralIntCaseClass(123).toJson(spaces = 0, sort = true), """{"x":123}""")
assertEquals(
LiteralBooleanCaseClass(true).toJson(spaces = 0, sort = true),
"""{"x":true}"""
)
assertEquals(LiteralCharCaseClass('a').toJson(spaces = 0, sort = true), """{"x":"a"}""")

val unionString: LiteralUnion = "abc"
assertEquals(unionString.toJson(spaces = 0, sort = true), """"abc"""")
val unionInt: LiteralUnion = 123
assertEquals(unionInt.toJson(spaces = 0, sort = true), "123")
val unionBool: LiteralUnion = true
assertEquals(unionBool.toJson(spaces = 0, sort = true), "true")
}

test("write default spaced output") {
assertEquals(
CaseClass1("str", 123).toJson,
Expand Down
6 changes: 6 additions & 0 deletions tupson/test/src/ba/sake/tupson/types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ case class CaseClassDefault(
// parsed as Some("default") IF THEY KEY IS MISSING (not failing)
str: Option[String] = Some("default")
) derives JsonRW
case class LiteralStringCaseClass(x: "abc") derives JsonRW
case class LiteralIntCaseClass(x: 123) derives JsonRW
case class LiteralBooleanCaseClass(x: true) derives JsonRW
case class LiteralCharCaseClass(x: 'a') derives JsonRW

package rec {
case class Node(children: Seq[Node]) derives JsonRW
Expand All @@ -23,3 +27,5 @@ package weird_named {
}

type Person = (name: String, age: Int)
type LiteralPerson = (x: "abc")
type LiteralUnion = "abc" | 123 | true