Skip to content

Commit 7f29216

Browse files
committed
.
1 parent 4b17bc1 commit 7f29216

5 files changed

Lines changed: 134 additions & 171 deletions

File tree

scautable-json/package.mill

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,17 @@ import mill.*
44
import mill.scalalib.*
55
import build.Config
66

7-
object `package` extends Module:
7+
object `package` extends build.Common with ScalaModule with build.PublishModule:
88

9-
// JVM-only module using Java's native JSON streaming API
10-
object jvm extends build.Common with ScalaModule with build.PublishModule:
11-
override def moduleDeps = Seq(build.scautable.jvm)
12-
override def artifactName = "scautable-json"
13-
14-
// Use Java's JSON-P (JSR 374) API for streaming
15-
override def mvnDeps = super.mvnDeps() ++ Seq(
16-
mvn"org.glassfish:jakarta.json::2.0.1"
17-
)
18-
end jvm
9+
override def moduleDeps = Seq(build.scautable.jvm)
10+
override def artifactName = "scautable-json"
11+
12+
// Use Java's JSON-P (JSR 374) API for streaming
13+
override def mvnDeps = super.mvnDeps() ++ Seq(
14+
mvn"org.glassfish:jakarta.json::2.0.1"
15+
)
1916

20-
object test extends Module:
21-
object jvm extends build.Common with ScalaModule with TestModule.Munit with build.CommonTests with build.ShareCompileResources:
22-
override def moduleDeps = Seq(build.`scautable-json`.jvm)
23-
end jvm
17+
object test extends build.Common with ScalaModule with TestModule.Munit with build.CommonTests with build.ShareCompileResources:
18+
override def moduleDeps = Seq(build.`scautable-json`)
2419
end test
2520
end `package`

scautable-json/src/StreamingJsonParser.scala

Lines changed: 48 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@ package io.github.quafadas.scautable.json
33
import scala.io.Source
44
import java.io.{StringReader, InputStream, InputStreamReader}
55
import jakarta.json.Json
6-
import jakarta.json.stream.{JsonParser => JParser}
6+
import jakarta.json.stream.JsonParser as JParser
77
import jakarta.json.stream.JsonParser.Event
88

99
/** Streaming JSON parser using Java's JSON-P (JSR 374) streaming API.
1010
*
11-
* This parser uses the native Java JSON streaming API to read JSON arrays
12-
* incrementally without loading the entire array into memory.
11+
* This parser uses the native Java JSON streaming API to read JSON arrays incrementally without loading the entire array into memory.
1312
*/
1413
private[json] object StreamingJsonParser:
1514

@@ -21,22 +20,22 @@ private[json] object StreamingJsonParser:
2120
case class JsonBool(value: Boolean) extends JsonValue
2221
case class JsonObject(fields: Map[String, JsonValue]) extends JsonValue
2322

24-
/** Parse JSON objects from an InputStream one at a time using Java's streaming API.
23+
/** Creates an iterator over JSON objects from a parser.
2524
*
26-
* @param input The input stream to read from
27-
* @param maxObjects Maximum number of objects to read (for type inference)
28-
* @return Iterator of JSON objects
25+
* @param parser
26+
* The JSON-P parser to read from
27+
* @param maxObjects
28+
* Maximum number of objects to read (for type inference)
29+
* @return
30+
* Iterator of JSON objects
2931
*/
30-
def parseArrayStream(input: InputStream, maxObjects: Int = Int.MaxValue): Iterator[JsonObject] =
31-
val reader = new InputStreamReader(input, "UTF-8")
32-
val parser = Json.createParser(reader)
33-
32+
private def createIterator(parser: JParser, maxObjects: Int): Iterator[JsonObject] =
3433
new Iterator[JsonObject]:
3534
private var objectsRead = 0
3635
private var arrayStarted = false
3736
private var arrayEnded = false
3837
private var nextObj: Option[JsonObject] = None
39-
38+
4039
// Skip to array start
4140
if !arrayEnded && !arrayStarted then
4241
while parser.hasNext && !arrayStarted do
@@ -56,16 +55,19 @@ private[json] object StreamingJsonParser:
5655

5756
def next(): JsonObject =
5857
if !hasNext then throw new NoSuchElementException("No more objects")
58+
end if
5959
val result = nextObj.get
6060
nextObj = None
6161
objectsRead += 1
6262
result
63+
end next
6364

6465
private def tryReadNext(): Option[JsonObject] =
65-
if arrayEnded || !parser.hasNext then
66+
if arrayEnded || !parser.hasNext then
6667
arrayEnded = true
6768
return None
68-
69+
end if
70+
6971
try
7072
parser.next() match
7173
case Event.START_OBJECT =>
@@ -77,14 +79,17 @@ private[json] object StreamingJsonParser:
7779
// Skip other events
7880
tryReadNext()
7981
catch
82+
case e: UnsupportedOperationException =>
83+
throw e // Re-throw nesting errors
8084
case e: Exception =>
8185
arrayEnded = true
8286
None
87+
end try
8388
end tryReadNext
8489

8590
private def readObject(parser: JParser): JsonObject =
8691
val fields = scala.collection.mutable.Map[String, JsonValue]()
87-
92+
8893
var continue = true
8994
while continue && parser.hasNext do
9095
parser.next() match
@@ -96,13 +101,15 @@ private[json] object StreamingJsonParser:
96101
continue = false
97102
case _ =>
98103
()
99-
104+
end while
105+
100106
JsonObject(fields.toMap)
101107
end readObject
102108

103109
private def readValue(parser: JParser): JsonValue =
104110
if !parser.hasNext then return JsonNull
105-
111+
end if
112+
106113
parser.next() match
107114
case Event.VALUE_NULL =>
108115
JsonNull
@@ -115,160 +122,45 @@ private[json] object StreamingJsonParser:
115122
case Event.VALUE_NUMBER =>
116123
JsonNumber(parser.getBigDecimal())
117124
case Event.START_ARRAY =>
118-
// Skip arrays for now
119-
skipArray(parser)
120-
JsonString("[]")
125+
throw new UnsupportedOperationException(
126+
"Nested arrays are not supported. JSON must be a flat array of objects with primitive values only."
127+
)
121128
case Event.START_OBJECT =>
122-
// Skip nested objects for now
123-
skipObject(parser)
124-
JsonString("{}")
129+
throw new UnsupportedOperationException(
130+
"Nested objects are not supported. JSON must be a flat array of objects with primitive values only."
131+
)
125132
case other =>
126133
JsonNull
134+
end match
127135
end readValue
128-
129-
private def skipArray(parser: JParser): Unit =
130-
var depth = 1
131-
while depth > 0 && parser.hasNext do
132-
parser.next() match
133-
case Event.START_ARRAY => depth += 1
134-
case Event.END_ARRAY => depth -= 1
135-
case _ => ()
136-
137-
private def skipObject(parser: JParser): Unit =
138-
var depth = 1
139-
while depth > 0 && parser.hasNext do
140-
parser.next() match
141-
case Event.START_OBJECT => depth += 1
142-
case Event.END_OBJECT => depth -= 1
143-
case _ => ()
144136
end new
137+
end createIterator
138+
139+
/** Parse JSON objects from an InputStream one at a time using Java's streaming API.
140+
*
141+
* @param input
142+
* The input stream to read from
143+
* @param maxObjects
144+
* Maximum number of objects to read (for type inference)
145+
* @return
146+
* Iterator of JSON objects
147+
*/
148+
def parseArrayStream(input: InputStream, maxObjects: Int = Int.MaxValue): Iterator[JsonObject] =
149+
val parser = Json.createParser(new InputStreamReader(input, "UTF-8"))
150+
createIterator(parser, maxObjects)
145151
end parseArrayStream
146152

147153
/** Parse JSON from a Source (for compile-time use) */
148154
def parseArrayFromSource(source: Source, maxObjects: Int = Int.MaxValue): Iterator[JsonObject] =
149155
val content = source.mkString
150156
val input = new java.io.ByteArrayInputStream(content.getBytes("UTF-8"))
151157
parseArrayStream(input, maxObjects)
158+
end parseArrayFromSource
152159

153160
/** Parse JSON from a string */
154161
def parseArrayFromString(json: String, maxObjects: Int = Int.MaxValue): Iterator[JsonObject] =
155-
val reader = new StringReader(json)
156-
val parser = Json.createParser(reader)
157-
158-
// Same implementation as parseArrayStream but with a Reader
159-
val iter = new Iterator[JsonObject]:
160-
private var objectsRead = 0
161-
private var arrayStarted = false
162-
private var arrayEnded = false
163-
private var nextObj: Option[JsonObject] = None
164-
165-
// Skip to array start
166-
if !arrayEnded && !arrayStarted then
167-
while parser.hasNext && !arrayStarted do
168-
parser.next() match
169-
case Event.START_ARRAY =>
170-
arrayStarted = true
171-
case _ =>
172-
()
173-
end if
174-
175-
def hasNext: Boolean =
176-
if arrayEnded || objectsRead >= maxObjects then false
177-
else if nextObj.isDefined then true
178-
else
179-
nextObj = tryReadNext()
180-
nextObj.isDefined
181-
182-
def next(): JsonObject =
183-
if !hasNext then throw new NoSuchElementException("No more objects")
184-
val result = nextObj.get
185-
nextObj = None
186-
objectsRead += 1
187-
result
188-
189-
private def tryReadNext(): Option[JsonObject] =
190-
if arrayEnded || !parser.hasNext then
191-
arrayEnded = true
192-
return None
193-
194-
try
195-
parser.next() match
196-
case Event.START_OBJECT =>
197-
Some(readObject(parser))
198-
case Event.END_ARRAY =>
199-
arrayEnded = true
200-
None
201-
case _ =>
202-
// Skip other events
203-
tryReadNext()
204-
catch
205-
case e: Exception =>
206-
arrayEnded = true
207-
None
208-
end tryReadNext
209-
210-
private def readObject(parser: JParser): JsonObject =
211-
val fields = scala.collection.mutable.Map[String, JsonValue]()
212-
213-
var continue = true
214-
while continue && parser.hasNext do
215-
parser.next() match
216-
case Event.KEY_NAME =>
217-
val key = parser.getString()
218-
val value = readValue(parser)
219-
fields(key) = value
220-
case Event.END_OBJECT =>
221-
continue = false
222-
case _ =>
223-
()
224-
225-
JsonObject(fields.toMap)
226-
end readObject
227-
228-
private def readValue(parser: JParser): JsonValue =
229-
if !parser.hasNext then return JsonNull
230-
231-
parser.next() match
232-
case Event.VALUE_NULL =>
233-
JsonNull
234-
case Event.VALUE_TRUE =>
235-
JsonBool(true)
236-
case Event.VALUE_FALSE =>
237-
JsonBool(false)
238-
case Event.VALUE_STRING =>
239-
JsonString(parser.getString())
240-
case Event.VALUE_NUMBER =>
241-
JsonNumber(parser.getBigDecimal())
242-
case Event.START_ARRAY =>
243-
// Skip arrays for now
244-
skipArray(parser)
245-
JsonString("[]")
246-
case Event.START_OBJECT =>
247-
// Skip nested objects for now
248-
skipObject(parser)
249-
JsonString("{}")
250-
case other =>
251-
JsonNull
252-
end readValue
253-
254-
private def skipArray(parser: JParser): Unit =
255-
var depth = 1
256-
while depth > 0 && parser.hasNext do
257-
parser.next() match
258-
case Event.START_ARRAY => depth += 1
259-
case Event.END_ARRAY => depth -= 1
260-
case _ => ()
261-
262-
private def skipObject(parser: JParser): Unit =
263-
var depth = 1
264-
while depth > 0 && parser.hasNext do
265-
parser.next() match
266-
case Event.START_OBJECT => depth += 1
267-
case Event.END_OBJECT => depth -= 1
268-
case _ => ()
269-
end new
270-
271-
iter
162+
val parser = Json.createParser(new StringReader(json))
163+
createIterator(parser, maxObjects)
272164
end parseArrayFromString
273165

274166
end StreamingJsonParser
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[
2+
{"a": 1, "items": "flat string"},
3+
{"a": 2, "items": [1, 2, 3]}
4+
]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[
2+
{"a": 1, "b": "flat"},
3+
{"a": 2, "b": {"nested": "object"}}
4+
]

0 commit comments

Comments
 (0)