2018-11-10 00:16:09 +00:00
|
|
|
import
|
2018-12-17 23:01:06 +00:00
|
|
|
typetraits,
|
2020-06-01 18:15:22 +00:00
|
|
|
faststreams/[outputs, textio], serialization, json,
|
|
|
|
types
|
2018-11-10 00:16:09 +00:00
|
|
|
|
|
|
|
type
|
|
|
|
JsonWriterState = enum
|
|
|
|
RecordExpected
|
|
|
|
RecordStarted
|
|
|
|
AfterField
|
|
|
|
|
2018-12-17 23:01:06 +00:00
|
|
|
JsonWriter* = 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
|
|
|
|
|
2020-06-01 18:15:22 +00:00
|
|
|
export
|
|
|
|
JsonString
|
2019-03-24 23:11:54 +00:00
|
|
|
|
2020-04-09 20:14:14 +00:00
|
|
|
proc init*(T: type JsonWriter, stream: OutputStream,
|
2018-12-17 23:01:06 +00:00
|
|
|
pretty = false, typeAnnotations = false): T =
|
|
|
|
result.stream = stream
|
2018-11-10 00:16:09 +00:00
|
|
|
result.hasPrettyOutput = pretty
|
|
|
|
result.hasTypeAnnotations = typeAnnotations
|
|
|
|
result.nestingLevel = if pretty: 0 else: -1
|
|
|
|
result.state = RecordExpected
|
|
|
|
|
|
|
|
proc beginRecord*(w: var JsonWriter, T: type)
|
|
|
|
proc beginRecord*(w: var JsonWriter)
|
2019-03-24 23:11:54 +00:00
|
|
|
proc writeValue*(w: var JsonWriter, value: auto)
|
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
|
|
|
|
2020-03-18 16:21:18 +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
|
2019-03-13 23:39:10 +00:00
|
|
|
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
|
|
|
|
2018-11-29 01:38:35 +00:00
|
|
|
w.state = RecordExpected
|
|
|
|
|
2018-11-10 00:16:09 +00:00
|
|
|
proc writeField*(w: var JsonWriter, name: string, value: auto) =
|
|
|
|
mixin writeValue
|
|
|
|
|
|
|
|
w.writeFieldName(name)
|
|
|
|
w.writeValue(value)
|
|
|
|
|
|
|
|
w.state = AfterField
|
|
|
|
|
|
|
|
proc beginRecord*(w: var JsonWriter) =
|
2019-03-13 23:39:10 +00:00
|
|
|
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) =
|
2019-03-13 23:39:10 +00:00
|
|
|
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 '}'
|
2018-11-29 01:38:35 +00:00
|
|
|
|
|
|
|
template endRecordField*(w: var JsonWriter) =
|
|
|
|
endRecord(w)
|
|
|
|
w.state = AfterField
|
2018-11-10 00:16:09 +00:00
|
|
|
|
2019-07-16 10:20:05 +00:00
|
|
|
proc writeIterable*(w: var JsonWriter, collection: auto) =
|
2018-11-10 00:16:09 +00:00
|
|
|
mixin writeValue
|
|
|
|
|
2018-12-17 23:01:06 +00:00
|
|
|
append '['
|
2019-03-19 23:54:03 +00:00
|
|
|
|
|
|
|
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:
|
2019-03-19 23:54:03 +00:00
|
|
|
append ','
|
|
|
|
if w.hasPrettyOutput:
|
|
|
|
append '\n'
|
|
|
|
indent()
|
|
|
|
|
2018-11-10 00:16:09 +00:00
|
|
|
w.state = RecordExpected
|
|
|
|
w.writeValue(e)
|
2019-07-16 10:20:05 +00:00
|
|
|
first = false
|
2019-03-19 23:54:03 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
2020-04-24 13:42:27 +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
|
|
|
|
|
2019-03-24 23:11:54 +00:00
|
|
|
proc writeValue*(w: var JsonWriter, value: auto) =
|
2019-08-06 22:20:27 +00:00
|
|
|
mixin enumInstanceSerializedFields, writeValue, writeFieldIMPL
|
2019-07-08 07:57:05 +00:00
|
|
|
|
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):
|
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
|
|
|
|
2020-11-12 19:00:10 +00:00
|
|
|
elif value is TaintedString:
|
|
|
|
writeValue(w, string value)
|
|
|
|
|
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:
|
2020-05-05 17:28:44 +00:00
|
|
|
w.stream.writeText ord(value)
|
2020-07-24 19:49:30 +00:00
|
|
|
|
2019-07-18 23:02:15 +00:00
|
|
|
elif value is range:
|
|
|
|
when low(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):
|
|
|
|
w.beginRecord(type(value))
|
2019-08-06 22:20:27 +00:00
|
|
|
type RecordType = type value
|
|
|
|
value.enumInstanceSerializedFields(fieldName, field):
|
|
|
|
type FieldType = type field
|
|
|
|
w.writeFieldName(fieldName)
|
|
|
|
w.writeFieldIMPL(FieldTag[RecordType, fieldName, FieldType], field, value)
|
|
|
|
w.state = AfterField
|
2018-11-10 00:16:09 +00:00
|
|
|
w.endRecord()
|
2020-07-24 19:49:30 +00:00
|
|
|
|
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
|
|
|
|
2018-11-11 11:45:34 +00:00
|
|
|
proc toJson*(v: auto, pretty = false, typeAnnotations = false): string =
|
2018-12-28 01:02:10 +00:00
|
|
|
mixin writeValue
|
|
|
|
|
2020-04-10 13:46:12 +00:00
|
|
|
var s = memoryOutput()
|
2019-03-11 09:39:19 +00:00
|
|
|
var w = JsonWriter.init(s, pretty, typeAnnotations)
|
2018-11-11 11:45:34 +00:00
|
|
|
w.writeValue v
|
2019-03-11 09:39:19 +00:00
|
|
|
return s.getOutput(string)
|
2018-11-11 11:45:34 +00:00
|
|
|
|
2020-08-02 17:14:59 +00:00
|
|
|
template serializesAsTextInJson*(T: type[enum]) =
|
|
|
|
template writeValue*(w: var JsonWriter, val: T) =
|
|
|
|
w.writeValue $val
|
|
|
|
|