Skip to content

Commit a56fd99

Browse files
committed
Add Scala 3 support
1 parent 2f90632 commit a56fd99

6 files changed

Lines changed: 283 additions & 11 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
matrix:
1414
os: [ubuntu-latest, windows-latest, macOS-latest]
1515
jdk: [zulu@1.8, graalvm-ce-java11@20.3.0, openjdk@1.15]
16-
scala: [2.11.12, 2.12.12, 2.13.5]
16+
scala: [2.11.12, 2.12.12, 2.13.5, 3.0.0-RC2]
1717
name: Test ${{ matrix.os }} -- ${{ matrix.jdk }}
1818
steps:
1919
- uses: actions/checkout@v2
@@ -36,7 +36,7 @@ jobs:
3636
run: |
3737
./bin/scalafmt --test
3838
shell: bash
39-
- name: Compile and test jsonrpc4s
39+
- name: Compile and test
4040
env:
4141
SCALA_VERSION: ${{ matrix.scala }}
4242
run: |

build.sbt

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,14 @@ lazy val commonSettings = Seq(
3636
"-feature",
3737
"-language:existentials",
3838
"-language:dynamics,higherKinds"
39-
),
39+
) ++ (CrossVersion.partialVersion(scalaVersion.value) match {
40+
case Some((3, _)) => Seq("-language:Scala2,implicitConversions")
41+
case _ => Seq("-target:jvm-1.8")
42+
}),
43+
Compile / unmanagedSourceDirectories += (CrossVersion.partialVersion(scalaVersion.value) match {
44+
case Some((3, _)) => baseDirectory.value / "../src/main/scala-3"
45+
case _ => baseDirectory.value / "../src/main/scala-2"
46+
}),
4047
publishTo := sonatypePublishToBundle.value,
4148
releaseEarlyWith := SonatypePublisher
4249
)
@@ -87,7 +94,7 @@ lazy val dijon = crossProject(JVMPlatform, JSPlatform)
8794
.settings(
8895
scalaVersion := "2.13.5", // Update .github/workflows/ci.yml when changing this
8996
libraryDependencies ++= Seq(
90-
"com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.7.1",
97+
("com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.7.1").cross(CrossVersion.for3Use2_13),
9198
"org.scala-lang.modules" %%% "scala-collection-compat" % "2.4.3",
9299
"org.scalatest" %%% "scalatest" % "3.2.7" % Test
93100
)
@@ -96,14 +103,16 @@ lazy val dijon = crossProject(JVMPlatform, JSPlatform)
96103
crossScalaVersions := Seq(
97104
"2.11.12",
98105
"2.12.13",
99-
"2.13.5"
106+
"2.13.5",
107+
"3.0.0-RC2"
100108
) // Update .github/workflows/ci.yml when changing this
101109
)
102110
.jsSettings(
103111
crossScalaVersions := Seq(
104112
"2.11.12",
105113
"2.12.13",
106-
"2.13.5"
114+
"2.13.5",
115+
"3.0.0-RC2"
107116
), // Update .github/workflows/ci.yml when changing this
108117
scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)
109118
.withESFeatures(_.withUseECMAScript2015(false))),
File renamed without changes.

dijon/src/main/scala/dijon/package.scala renamed to dijon/src/main/scala-2/dijon/package.scala

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ package object dijon {
2121

2222
def obj(values: (String, SomeJson)*): SomeJson = {
2323
val len = values.length
24-
var i = 0
2524
val map = new util.LinkedHashMap[String, SomeJson](len)
25+
var i = 0
2626
while (i < len) {
2727
val kv = values(i)
2828
map.put(kv._1, kv._2)
@@ -137,10 +137,10 @@ package object dijon {
137137
res
138138
}
139139
.asScala
140-
case _ => this
140+
case _ => underlying
141141
}
142142

143-
override def toString: String = compact(this)
143+
override def toString: String = compact(underlying)
144144

145145
override def equals(obj: Any): Boolean = underlying == (obj match {
146146
case that: SomeJson => that.underlying
@@ -190,7 +190,10 @@ package object dijon {
190190
if (!in.isNextToken(']')) {
191191
in.rollbackToken()
192192
val dp = depth - 1
193-
do arr += decode(in, dp) while (in.isNextToken(','))
193+
while ({
194+
arr += decode(in, dp)
195+
in.isNextToken(',')
196+
}) ()
194197
if (!in.isCurrentToken(']')) in.arrayEndOrCommaError()
195198
}
196199
arr
@@ -200,7 +203,10 @@ package object dijon {
200203
if (!in.isNextToken('}')) {
201204
in.rollbackToken()
202205
val dp = depth - 1
203-
do obj.put(in.readKeyAsString(), decode(in, dp)) while (in.isNextToken(','))
206+
while ({
207+
obj.put(in.readKeyAsString(), decode(in, dp))
208+
in.isNextToken(',')
209+
}) ()
204210
if (!in.isCurrentToken('}')) in.objectEndOrCommaError()
205211
}
206212
obj.asScala
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
package object dijon {
2+
import java.nio.charset.StandardCharsets._
3+
import java.util
4+
5+
import com.github.plokhotnyuk.jsoniter_scala.core._
6+
7+
import scala.collection.mutable
8+
import scala.jdk.javaapi.CollectionConverters
9+
import scala.util.Try
10+
11+
type JsonPrimitive = String | Int | Double | Boolean | None.type
12+
type Rec[JA[_], JO[_], A] = A match {
13+
case JsonPrimitive => JsonPrimitive | JA[Rec[JA, JO, JsonPrimitive]] | JO[Rec[JA, JO, JsonPrimitive]]
14+
case _ => A | JA[Rec[JA, JO, A]] | JO[Rec[JA, JO, A]]
15+
}
16+
type SomeJson = Rec[[A] =>> mutable.Buffer[A], [A] =>> mutable.Map[String, A], JsonPrimitive]
17+
type JsonObject = mutable.Map[String, SomeJson]
18+
type JsonArray = mutable.Buffer[SomeJson]
19+
20+
def `[]`: SomeJson = new mutable.ArrayBuffer[SomeJson](initArrayCapacity)
21+
22+
def `{}`: SomeJson = CollectionConverters.asScala(new util.LinkedHashMap[String, SomeJson](initMapCapacity))
23+
24+
def obj(values: (String, SomeJson)*): SomeJson = {
25+
val len = values.length
26+
val map = new util.LinkedHashMap[String, SomeJson](len)
27+
var i = 0
28+
while (i < len) {
29+
val kv = values(i)
30+
map.put(kv._1, kv._2)
31+
i += 1
32+
}
33+
CollectionConverters.asScala(map)
34+
}
35+
36+
def arr(values: SomeJson*): SomeJson = mutable.ArrayBuffer[SomeJson](values: _*)
37+
38+
implicit class Json(val underlying: SomeJson) extends Dynamic {
39+
def selectDynamic(key: String): SomeJson = apply(key)
40+
41+
def updateDynamic(key: String)(value: SomeJson): Unit = update(key, value)
42+
43+
def applyDynamic[B](key1: String)(indexOrKey2: B): SomeJson = indexOrKey2 match {
44+
case index: Int => underlying.apply(key1).apply(index)
45+
case key2: String => underlying.apply(key1).apply(key2)
46+
}
47+
48+
def apply(key: String): SomeJson = underlying match {
49+
case obj: JsonObject =>
50+
obj.get(key) match {
51+
case Some(value) => value
52+
case _ => None
53+
}
54+
case _ => None
55+
}
56+
57+
def apply(index: Int): SomeJson = underlying match {
58+
case arr: JsonArray if arr.isDefinedAt(index) => arr(index)
59+
case _ => None
60+
}
61+
62+
def update(key: String, value: SomeJson): Unit = underlying match {
63+
case obj: JsonObject => obj += ((key, value))
64+
case _ => ()
65+
}
66+
67+
def update(index: Int, value: SomeJson): Unit = underlying match {
68+
case arr: JsonArray if index >= 0 =>
69+
while (arr.length <= index) {
70+
arr += None
71+
}
72+
arr(index) = value
73+
case _ => ()
74+
}
75+
76+
def ++(that: SomeJson): SomeJson = (this.underlying, that.underlying) match {
77+
case (a: JsonObject, b: JsonObject) =>
78+
val res = new util.LinkedHashMap[String, SomeJson](a.size + b.size)
79+
a.foreach { case (k, v: SomeJson) =>
80+
res.put(k, if (b.contains(k)) Json(v) ++ b(k) else Json(v).deepCopy)
81+
}
82+
b.foreach { case (k, v: SomeJson) =>
83+
if (!res.containsKey(k)) res.put(k, Json(v).deepCopy)
84+
}
85+
CollectionConverters.asScala(res)
86+
case _ => that.deepCopy
87+
}
88+
89+
def --(keys: String*): SomeJson = underlying match {
90+
case obj: JsonObject =>
91+
val res = obj.clone()
92+
keys.foreach(res -= _)
93+
res.deepCopy
94+
case _ => deepCopy
95+
}
96+
97+
def remove(keys: String*): Unit = underlying match {
98+
case obj: JsonObject => keys.foreach(obj -= _)
99+
case _ => ()
100+
}
101+
102+
def asString: Option[String] = underlying match {
103+
case x: String => new Some(x)
104+
case _ => None
105+
}
106+
107+
def asDouble: Option[Double] = underlying match {
108+
case x: Double => new Some(x)
109+
case _ => None
110+
}
111+
112+
def asInt: Option[Int] = underlying match {
113+
case x: Int => new Some(x)
114+
case _ => None
115+
}
116+
117+
def asBoolean: Option[Boolean] = underlying match {
118+
case x: Boolean => new Some(x)
119+
case _ => None
120+
}
121+
122+
def toSeq: collection.Seq[SomeJson] = underlying match {
123+
case arr: JsonArray => arr
124+
case _ => Nil
125+
}
126+
127+
def toMap: collection.Map[String, SomeJson] = underlying match {
128+
case obj: JsonObject => obj
129+
case _ => Map.empty
130+
}
131+
132+
def deepCopy: SomeJson = underlying match {
133+
case arr: JsonArray =>
134+
arr.foldLeft(new mutable.ArrayBuffer[SomeJson](arr.length))((res, x) => res += x.deepCopy)
135+
case obj: JsonObject =>
136+
CollectionConverters.asScala(obj.foldLeft(new util.LinkedHashMap[String, SomeJson](obj.size)) { (res, kv) =>
137+
res.put(kv._1, kv._2.deepCopy)
138+
res
139+
})
140+
case x => x
141+
}
142+
143+
override def toString: String = compact(underlying)
144+
145+
override def equals(obj: Any): Boolean = underlying == (obj match {
146+
case that: SomeJson => that.underlying
147+
case _ => obj
148+
})
149+
150+
override def hashCode: Int = underlying.hashCode
151+
}
152+
153+
def compact(json: SomeJson): String = new String(writeToArray[SomeJson](json), UTF_8)
154+
155+
def pretty(json: SomeJson): String = new String(writeToArray[SomeJson](json, prettyConfig), UTF_8)
156+
157+
def parse(s: String): SomeJson = readFromArray[SomeJson](s.getBytes(UTF_8))
158+
159+
implicit class JsonStringContext(val sc: StringContext) extends AnyVal {
160+
def json(args: Any*): SomeJson = parse(sc.s(args: _*))
161+
}
162+
163+
implicit val codec: JsonValueCodec[SomeJson] = new JsonValueCodec[SomeJson] {
164+
override def decodeValue(in: JsonReader, default: SomeJson): SomeJson =
165+
decode(in, maxParsingDepth)
166+
167+
override def encodeValue(x: SomeJson, out: JsonWriter): Unit =
168+
encode(x, out, maxSerializationDepth)
169+
170+
override val nullValue: SomeJson = None
171+
172+
private[this] def decode(in: JsonReader, depth: Int): SomeJson = {
173+
val b = in.nextToken()
174+
if (b == 'n') in.readNullOrError(None, "expected `null` value")
175+
else if (b == '"') {
176+
in.rollbackToken()
177+
in.readString(null)
178+
} else if (b == 't' || b == 'f') {
179+
in.rollbackToken()
180+
in.readBoolean()
181+
} else if ((b >= '0' && b <= '9') || b == '-') {
182+
in.rollbackToken()
183+
val d = in.readDouble()
184+
val i = d.toInt
185+
if (i.toDouble == d) i
186+
else d
187+
} else if (b == '[') {
188+
if (depth <= 0) in.decodeError("depth limit exceeded")
189+
val arr = new mutable.ArrayBuffer[SomeJson](initArrayCapacity)
190+
if (!in.isNextToken(']')) {
191+
in.rollbackToken()
192+
val dp = depth - 1
193+
while ({
194+
arr += decode(in, dp)
195+
in.isNextToken(',')
196+
}) ()
197+
if (!in.isCurrentToken(']')) in.arrayEndOrCommaError()
198+
}
199+
(arr: SomeJson)
200+
} else if (b == '{') {
201+
if (depth <= 0) in.decodeError("depth limit exceeded")
202+
val obj = new util.LinkedHashMap[String, SomeJson](initMapCapacity)
203+
if (!in.isNextToken('}')) {
204+
in.rollbackToken()
205+
val dp = depth - 1
206+
while ({
207+
obj.put(in.readKeyAsString(), decode(in, dp))
208+
in.isNextToken(',')
209+
}) ()
210+
if (!in.isCurrentToken('}')) in.objectEndOrCommaError()
211+
}
212+
CollectionConverters.asScala(obj)
213+
} else in.decodeError("expected JSON value")
214+
}
215+
216+
private[this] def encode(x: SomeJson, out: JsonWriter, depth: Int): Unit = x.underlying match {
217+
case None => out.writeNull()
218+
case str: String => out.writeVal(str)
219+
case b: Boolean => out.writeVal(b)
220+
case i: Int => out.writeVal(i)
221+
case d: Double => out.writeVal(d)
222+
case arr: JsonArray =>
223+
if (depth <= 0) out.encodeError("depth limit exceeded")
224+
out.writeArrayStart()
225+
val dp = depth - 1
226+
val l = arr.size
227+
var i = 0
228+
while (i < l) {
229+
encode(arr(i), out, dp)
230+
i += 1
231+
}
232+
out.writeArrayEnd()
233+
case obj: JsonObject =>
234+
if (depth <= 0) out.encodeError("depth limit exceeded")
235+
out.writeObjectStart()
236+
val dp = depth - 1
237+
val it = obj.iterator
238+
while (it.hasNext) {
239+
val (k, v: SomeJson) = it.next()
240+
out.writeKey(k)
241+
encode(v, out, dp)
242+
}
243+
out.writeObjectEnd()
244+
}
245+
}
246+
247+
private[this] val prettyConfig = WriterConfig.withIndentionStep(2)
248+
private[this] val maxParsingDepth =
249+
Try(System.getProperty("dijon.maxParsingDepth", "128").toInt).getOrElse(128)
250+
private[this] val maxSerializationDepth =
251+
Try(System.getProperty("dijon.maxSerializationDepth", "128").toInt).getOrElse(128)
252+
private[this] val initArrayCapacity =
253+
Try(System.getProperty("dijon.initArrayCapacity", "8").toInt).getOrElse(8)
254+
private[this] val initMapCapacity =
255+
Try(System.getProperty("dijon.initMapCapacity", "8").toInt).getOrElse(8)
256+
}

dijon/src/test/scala/dijon/DijonSpec.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package dijon
22

33
import com.github.plokhotnyuk.jsoniter_scala.core.{JsonReaderException, JsonWriterException}
44

5+
import scala.language.dynamics
56
import scala.collection.mutable
67
import org.scalatest.funsuite.AnyFunSuite
78

0 commit comments

Comments
 (0)