nim-json-serialization/json_serialization/writer.nim

288 lines
7.1 KiB
Nim
Raw Normal View History

2023-12-13 09:07:57 +00:00
# json-serialization
# Copyright (c) 2019-2023 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
# at your option.
# This file may not be copied, modified, or distributed except according to
# those terms.
2018-11-10 00:16:09 +00:00
import
2022-02-18 09:26:15 +00:00
std/[json, typetraits],
2023-12-18 04:05:12 +00:00
faststreams/[outputs, textio],
serialization,
2022-02-18 09:26:15 +00:00
"."/[format, types]
export
2022-02-18 09:26:15 +00:00
outputs, format, types, JsonString, DefaultFlavor
2018-11-10 00:16:09 +00:00
type
JsonWriterState = enum
RecordExpected
RecordStarted
AfterField
2021-03-18 11:01:06 +00:00
JsonWriter*[Flavor = DefaultFlavor] = object
2020-04-09 20:14:14 +00:00
stream*: OutputStream
2018-11-10 00:16:09 +00:00
hasTypeAnnotations: bool
hasPrettyOutput*: bool # read-only
nestingLevel*: int # read-only
state: JsonWriterState
Json.setWriter JsonWriter,
PreferredOutput = string
2019-03-24 23:11:54 +00:00
func init*(W: type JsonWriter, stream: OutputStream,
2021-03-18 11:01:06 +00:00
pretty = false, typeAnnotations = false): W =
W(stream: stream,
hasPrettyOutput: pretty,
hasTypeAnnotations: typeAnnotations,
nestingLevel: if pretty: 0 else: -1,
state: RecordExpected)
2018-11-10 00:16:09 +00:00
proc beginRecord*(w: var JsonWriter, T: type)
proc beginRecord*(w: var JsonWriter)
proc writeValue*(w: var JsonWriter, value: auto) {.gcsafe, raises: [IOError].}
2018-11-10 00:16:09 +00:00
2018-12-17 23:01:06 +00:00
template append(x: untyped) =
2020-05-05 17:28:44 +00:00
write w.stream, x
2018-12-17 23:01:06 +00:00
template indent =
2018-11-10 00:16:09 +00:00
for i in 0 ..< w.nestingLevel:
2018-12-17 23:01:06 +00:00
append ' '
2018-11-10 00:16:09 +00:00
template `$`*(s: JsonString): string =
string(s)
2018-11-10 00:16:09 +00:00
proc writeFieldName*(w: var JsonWriter, name: string) =
# this is implemented as a separate proc in order to
# keep the code bloat from `writeField` to a minimum
doAssert w.state != RecordExpected
2018-11-10 00:16:09 +00:00
if w.state == AfterField:
2018-12-17 23:01:06 +00:00
append ','
2018-11-10 00:16:09 +00:00
if w.hasPrettyOutput:
2018-12-17 23:01:06 +00:00
append '\n'
2018-11-10 00:16:09 +00:00
2018-12-17 23:01:06 +00:00
indent()
2018-11-10 00:16:09 +00:00
2018-12-17 23:01:06 +00:00
append '"'
append name
append '"'
append ':'
if w.hasPrettyOutput: append ' '
2018-11-10 00:16:09 +00:00
w.state = RecordExpected
proc writeField*(
w: var JsonWriter, name: string, value: auto) {.raises: [IOError].} =
2018-11-10 00:16:09 +00:00
mixin writeValue
w.writeFieldName(name)
w.writeValue(value)
w.state = AfterField
template fieldWritten*(w: var JsonWriter) =
w.state = AfterField
2018-11-10 00:16:09 +00:00
proc beginRecord*(w: var JsonWriter) =
doAssert w.state == RecordExpected
2018-11-10 00:16:09 +00:00
2018-12-17 23:01:06 +00:00
append '{'
2018-11-10 00:16:09 +00:00
if w.hasPrettyOutput:
w.nestingLevel += 2
w.state = RecordStarted
proc beginRecord*(w: var JsonWriter, T: type) =
w.beginRecord()
if w.hasTypeAnnotations: w.writeField("$type", typetraits.name(T))
proc endRecord*(w: var JsonWriter) =
doAssert w.state != RecordExpected
2018-11-10 00:16:09 +00:00
if w.hasPrettyOutput:
2018-12-17 23:01:06 +00:00
append '\n'
2018-11-10 00:16:09 +00:00
w.nestingLevel -= 2
2018-12-17 23:01:06 +00:00
indent()
2018-11-10 00:16:09 +00:00
2018-12-17 23:01:06 +00:00
append '}'
template endRecordField*(w: var JsonWriter) =
endRecord(w)
w.state = AfterField
2018-11-10 00:16:09 +00:00
2022-06-17 17:16:28 +00:00
iterator stepwiseArrayCreation*[C](w: var JsonWriter, collection: C): auto =
2018-12-17 23:01:06 +00:00
append '['
if w.hasPrettyOutput:
append '\n'
w.nestingLevel += 2
indent()
2019-07-16 10:20:05 +00:00
var first = true
for e in collection:
if not first:
append ','
if w.hasPrettyOutput:
append '\n'
indent()
2018-11-10 00:16:09 +00:00
w.state = RecordExpected
2022-06-17 17:16:28 +00:00
yield e
2019-07-16 10:20:05 +00:00
first = false
if w.hasPrettyOutput:
append '\n'
w.nestingLevel -= 2
indent()
2018-12-17 23:01:06 +00:00
append ']'
2018-11-10 00:16:09 +00:00
2022-06-17 17:16:28 +00:00
proc writeIterable*(w: var JsonWriter, collection: auto) =
mixin writeValue
for e in w.stepwiseArrayCreation(collection):
w.writeValue(e)
2021-12-15 09:34:49 +00:00
proc writeArray*[T](w: var JsonWriter, elements: openArray[T]) =
2019-07-16 10:20:05 +00:00
writeIterable(w, elements)
2020-04-24 13:42:27 +00:00
# this construct catches `array[N, char]` which otherwise won't decompose into
# openArray[char] - we treat any array-like thing-of-characters as a string in
# the output
template isStringLike(v: string|cstring|openArray[char]|seq[char]): bool = true
template isStringLike[N](v: array[N, char]): bool = true
template isStringLike(v: auto): bool = false
template writeObjectField*[FieldType, RecordType](w: var JsonWriter,
record: RecordType,
fieldName: static string,
field: FieldType): bool =
mixin writeFieldIMPL, writeValue
2023-12-13 09:07:57 +00:00
type
R {.used.} = type record
w.writeFieldName(fieldName)
when RecordType is tuple:
w.writeValue(field)
else:
2023-12-18 04:05:12 +00:00
type RR = type record
w.writeFieldIMPL(FieldTag[RR, fieldName], field, record)
true
proc writeRecordValue*(w: var JsonWriter, value: auto)
{.gcsafe, raises: [IOError].} =
mixin enumInstanceSerializedFields, writeObjectField
type RecordType = type value
w.beginRecord RecordType
value.enumInstanceSerializedFields(fieldName, field):
if writeObjectField(w, value, fieldName, field):
w.state = AfterField
w.endRecord()
proc writeValue*(w: var JsonWriter, value: auto) {.gcsafe, raises: [IOError].} =
mixin writeValue
2019-03-24 23:11:54 +00:00
when value is JsonNode:
append if w.hasPrettyOutput: value.pretty
else: $value
2020-07-24 19:49:30 +00:00
2019-03-24 23:11:54 +00:00
elif value is JsonString:
append string(value)
2020-07-24 19:49:30 +00:00
2019-03-24 23:11:54 +00:00
elif value is ref:
if value == nil:
append "null"
else:
writeValue(w, value[])
2020-07-24 19:49:30 +00:00
2020-04-24 13:42:27 +00:00
elif isStringLike(value):
2021-11-01 16:59:12 +00:00
when value is cstring:
if value == nil:
append "null"
return
2019-11-04 18:42:34 +00:00
append '"'
2018-11-10 00:16:09 +00:00
template addPrefixSlash(c) =
2019-11-04 18:42:34 +00:00
append '\\'
append c
2018-11-10 00:16:09 +00:00
for c in value:
case c
of '\L': addPrefixSlash 'n'
of '\b': addPrefixSlash 'b'
of '\f': addPrefixSlash 'f'
of '\t': addPrefixSlash 't'
of '\r': addPrefixSlash 'r'
of '"' : addPrefixSlash '\"'
of '\0'..'\7':
2018-12-17 23:01:06 +00:00
append "\\u000"
append char(ord('0') + ord(c))
2018-11-10 00:16:09 +00:00
of '\14'..'\31':
2018-12-17 23:01:06 +00:00
append "\\u00"
2018-11-10 00:16:09 +00:00
# TODO: Should this really use a decimal representation?
# Or perhaps $ord(c) returns hex?
# This is potentially a bug in Nim's json module.
2018-12-17 23:01:06 +00:00
append $ord(c)
2018-11-10 00:16:09 +00:00
of '\\': addPrefixSlash '\\'
2019-11-04 18:42:34 +00:00
else: append c
2018-11-10 00:16:09 +00:00
2019-11-04 18:42:34 +00:00
append '"'
2020-07-24 19:49:30 +00:00
2018-11-10 00:16:09 +00:00
elif value is bool:
2018-12-17 23:01:06 +00:00
append if value: "true" else: "false"
2020-07-24 19:49:30 +00:00
2018-11-10 00:16:09 +00:00
elif value is enum:
w.writeValue $value
2020-07-24 19:49:30 +00:00
2019-07-18 23:02:15 +00:00
elif value is range:
2022-07-01 17:48:29 +00:00
when low(typeof(value)) < 0:
2020-05-05 17:28:44 +00:00
w.stream.writeText int64(value)
2019-07-18 23:02:15 +00:00
else:
2020-05-05 17:28:44 +00:00
w.stream.writeText uint64(value)
2020-07-24 19:49:30 +00:00
2018-11-10 00:16:09 +00:00
elif value is SomeInteger:
2020-05-05 17:28:44 +00:00
w.stream.writeText value
2020-07-24 19:49:30 +00:00
2018-11-10 00:16:09 +00:00
elif value is SomeFloat:
2020-05-05 17:28:44 +00:00
# TODO Implement writeText for floats
# to avoid the allocation here:
2018-12-17 23:01:06 +00:00
append $value
2020-07-24 19:49:30 +00:00
2020-03-18 18:20:35 +00:00
elif value is (seq or array or openArray):
2018-11-10 00:16:09 +00:00
w.writeArray(value)
2020-07-24 19:49:30 +00:00
2018-11-10 00:16:09 +00:00
elif value is (object or tuple):
mixin flavorUsesAutomaticObjectSerialization
type Flavor = JsonWriter.Flavor
const isAutomatic =
flavorUsesAutomaticObjectSerialization(Flavor)
when not isAutomatic:
const typeName = typetraits.name(type value)
{.error: "Please override writeValue for the " & typeName & " type (or import the module where the override is provided)".}
2020-07-24 19:49:30 +00:00
writeRecordValue(w, value)
2018-11-10 00:16:09 +00:00
else:
const typeName = typetraits.name(value.type)
2019-03-13 21:20:58 +00:00
{.fatal: "Failed to convert to JSON an unsupported type: " & typeName.}
2018-11-10 00:16:09 +00:00
proc toJson*(v: auto, pretty = false, typeAnnotations = false): string =
mixin writeValue
var
s = memoryOutput()
w = JsonWriter[DefaultFlavor].init(s, pretty, typeAnnotations)
w.writeValue v
s.getOutput(string)
template serializesAsTextInJson*(T: type[enum]) =
template writeValue*(w: var JsonWriter, val: T) =
w.writeValue $val