Skip to content

Commit c7a34b8

Browse files
committed
Add array streaming helpers
An implementation of #112 that introduces a rich set of helpers for creating arrays using a backwards-compatible approach that maintains the current semantic model of the array/object writer being responsible for introducing plumbing. Since the "outer" level drives the plumbing, we add writers for every combination of value and context: `beginArray`, `beginArrayElement` and `beginArrayMember` for creating an array in top-level, array and object respectively and the same for other values. In this model, `writeValue` itself performs no begin/end tracking - instead, it is up to the entity that creates the array/object to do so (`writeRecordValue`, `stepwiseArrayCreation` and so on). Here's an example of writing a `writeValue` overload that writes an array nested in an object: ```nim proc writeValue(w: var JsonWriter, t: MyType) = w.beginArray() for i in 0 ..< t.children: writer.beginObjectElement() writer.writeMember("id", i) writer.writeMember("name", "item" & t.childName[i]) writer.endObjectElement() writer.endArray() ``` The writing API is quite regular but requires calling special functions depending on the context which is easy to forget - calling the wrong variation results in invalid JSON! The backwards-compatiblity issue can be seen in a new test case based on a real-world example for writing byte arrays as hex strings. Further examples are available in the documentation.
1 parent 82451aa commit c7a34b8

File tree

9 files changed

+419
-150
lines changed

9 files changed

+419
-150
lines changed

docs/examples/reference0.nim

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,8 @@ type
1818
var conf = defaultJsonReaderConf
1919
conf.nestedDepthLimit = 0
2020

21-
let native =
22-
Json.decode(rawJson, NimServer, flags = defaultJsonReaderFlags, conf = conf)
23-
2421
# decode into native Nim
25-
#let native = Json.decode(rawJson, NimServer)
22+
let native = Json.decode(rawJson, NimServer)
2623

2724
# decode into mixed Nim + JsonValueRef
2825
let mixed = Json.decode(rawJson, MixedServer)

docs/examples/streamwrite0.nim

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import json_serialization, faststreams/outputs
2+
3+
let file = fileOutput("output.json")
4+
var writer = JsonWriter[DefaultFlavor].init(file, pretty = true)
5+
6+
writer.beginArray()
7+
8+
for i in 0 ..< 2:
9+
writer.beginObjectElement()
10+
11+
writer.writeMember("id", i)
12+
writer.writeMember("name", "item" & $i)
13+
14+
writer.endObjectElement()
15+
16+
writer.endArray()
17+
18+
file.close()

docs/examples/streamwrite1.nim

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import json_serialization, faststreams/outputs
2+
3+
let file = fileOutput("output.json")
4+
var writer = JsonWriter[DefaultFlavor].init(file)
5+
6+
# ANCHOR: Nesting
7+
writer.writeObject:
8+
writer.writeMember("status", "ok")
9+
writer.writeArrayMember("data"):
10+
for i in 0 ..< 2:
11+
writer.writeObjectElement:
12+
writer.writeMember("id", i)
13+
writer.writeMember("name", "item" & $i)
14+
# ANCHOR_END: Nesting
15+
16+
file.close()

docs/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# User guide
44

55
- [Getting started](./getting_started.md)
6+
- [Streaming](./streaming.md)
67
- [Reference](./reference.md)
78

89
# Developer guide

docs/src/reference.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ You can adjust these defaults to suit your needs:
8080

8181
### Common API
8282

83-
Similar to parsing, the [common serialization API]() is used to produce JSON documents.
83+
Similar to parsing, the [common serialization API](https://github.com/status-im/nim-serialization?tab=readme-ov-file#common-api) is used to produce JSON documents.
8484

8585
```nim
8686
{{#include ../examples/reference0.nim:Encode}}
@@ -257,9 +257,6 @@ writeField(w: var JsonWriter, name: string, value: auto)
257257
iterator stepwiseArrayCreation[C](w: var JsonWriter, collection: C): auto
258258
writeIterable(w: var JsonWriter, collection: auto)
259259
writeArray[T](w: var JsonWriter, elements: openArray[T])
260-
261-
writeNumber[F,T](w: var JsonWriter[F], value: JsonNumber[T])
262-
writeJsonValueRef[F,T](w: var JsonWriter[F], value: JsonValueRef[T])
263260
```
264261

265262
## Enums

docs/src/streaming.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Streaming
2+
3+
`JsonWriter` can be used to incrementally write JSON data.
4+
5+
Incremental processing is ideal for large documents or when you want to avoid building the entire JSON structure in memory.
6+
7+
<!-- toc -->
8+
9+
## Writing
10+
11+
You can use `JsonWriter` to write JSON objects, arrays, and values step by step, directly to a file or any output stream.
12+
13+
The process is similar to when you override `writeValue` to provide custom serialization.
14+
15+
### Example: Writing a JSON Array of Objects
16+
17+
Suppose you want to write a large array of objects to a file, one at a time:
18+
19+
```nim
20+
{{#include ../examples/streamwrite0.nim}}
21+
```
22+
23+
Resulting file (`output.json`):
24+
```json
25+
[
26+
{
27+
"id": 0,
28+
"name": "item0"
29+
},
30+
{
31+
"id": 1,
32+
"name": "item1"
33+
}
34+
]
35+
```
36+
37+
```admonish warning "Elements in objects and array"
38+
In the example, we see `beginArray`, `beginElement` and `writeMember`. The functions follow a pattern:
39+
* functions without suffix, like `beginArray`, are used at the top-level
40+
* functions with `Element` suffix are used inside arrays
41+
* functions with `Member` suffix and accomanying name are used in objects
42+
43+
Thus, if we were writing an array inside another array, we would have used `beginArray` for the outer array and `beginArrayMember` for the inner array. These rules also apply when implementing `writeValue`.
44+
```
45+
46+
### Example: Writing Nested Structures
47+
48+
Objects and arrays can be nested arbitrarily.
49+
50+
Here is the same array of JSON objects, nested in an envelope containing an additional `status` field.
51+
52+
Instead of manually placing `begin`/`end` pairs, we're using the convenience helpers `writeObjectElement` and `writeArrayMember`, along with `writeElement` to manage the required element markers:
53+
54+
```nim
55+
{{#include ../examples/streamwrite1.nim:Nesting}}
56+
```
57+
58+
This produces a the following output - notice the more compact representation when `pretty = true` is not used:
59+
```json
60+
{"status":"ok","data":[{"id":0,"name":"item0"},{"id":1,"name":"item1"}]}
61+
```
62+
63+
```admonish tip
64+
Similar to `begin`, we're using the `Element` suffix in arrays!
65+
```

json_serialization/types.nim

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
# This file may not be copied, modified, or distributed except according to
88
# those terms.
99

10+
{.push gcsafe, raises: [].}
11+
1012
import
1113
std/tables,
1214
serialization/errors
@@ -18,13 +20,13 @@ export
1820
type
1921
JsonError* = object of SerializationError
2022

21-
# This is a special type to parse whatever
22-
# json value into string.
2323
JsonString* = distinct string
24+
## A string containing valid JSON.
25+
## Used to preserve and pass on parts of a JSON document to another parser
26+
## or layer without interpreting it further
2427

25-
# This is a special type to parse whatever
26-
# json value into nothing/skip it.
2728
JsonVoid* = object
29+
## Marker used for skipping a JSON value during parsing
2830

2931
JsonSign* {.pure.} = enum
3032
None
@@ -63,11 +65,11 @@ type
6365
stringLengthLimit*: int
6466

6567
JsonValueKind* {.pure.} = enum
66-
String,
67-
Number,
68-
Object,
69-
Array,
70-
Bool,
68+
String
69+
Number
70+
Object
71+
Array
72+
Bool
7173
Null
7274

7375
JsonObjectType*[T: string or uint64] = OrderedTable[string, JsonValueRef[T]]
@@ -88,7 +90,6 @@ type
8890
of JsonValueKind.Null:
8991
discard
9092

91-
9293
const
9394
minPortableInt* = -9007199254740991 # -2**53 + 1
9495
maxPortableInt* = 9007199254740991 # +2**53 - 1
@@ -110,8 +111,6 @@ const
110111
stringLengthLimit: 0,
111112
)
112113

113-
{.push gcsafe, raises: [].}
114-
115114
template `==`*(lhs, rhs: JsonString): bool =
116115
string(lhs) == string(rhs)
117116

@@ -172,4 +171,7 @@ func `==`*(lhs, rhs: JsonValueRef): bool =
172171
of JsonValueKind.Null:
173172
true
174173

174+
template `$`*(s: JsonString): string =
175+
string(s)
176+
175177
{.pop.}

0 commit comments

Comments
 (0)