Basic support for Json flavours without default object serialization (#66)

Other changes:

* Migrate many procs accepting JsonReader to JsonLexer in order to
  reduce the number of generic instantiations and the resulting code
  bloat
This commit is contained in:
zah 2023-12-19 12:00:24 +02:00 committed by GitHub
parent 230e226da0
commit f42567c00c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 303 additions and 176 deletions

View File

@ -3,4 +3,3 @@ import
export
serialization, format, reader, writer

View File

@ -8,3 +8,32 @@ template supports*(_: type Json, T: type): bool =
# The JSON format should support every type
true
template flavorUsesAutomaticObjectSerialization*(T: type DefaultFlavor): bool = true
template flavorOmitsOptionalFields*(T: type DefaultFlavor): bool = false
template flavorRequiresAllFields*(T: type DefaultFlavor): bool = false
template flavorAllowsUnknownFields*(T: type DefaultFlavor): bool = false
# We create overloads of these traits to force the mixin treatment of the symbols
type DummyFlavor* = object
template flavorUsesAutomaticObjectSerialization*(T: type DummyFlavor): bool = true
template flavorOmitsOptionalFields*(T: type DummyFlavor): bool = false
template flavorRequiresAllFields*(T: type DummyFlavor): bool = false
template flavorAllowsUnknownFields*(T: type DummyFlavor): bool = false
template createJsonFlavor*(FlavorName: untyped,
mimeTypeValue = "application/json",
automaticObjectSerialization = false,
requireAllFields = true,
omitOptionalFields = true,
allowUnknownFields = true) {.dirty.} =
type FlavorName* = object
template Reader*(T: type FlavorName): type = Reader(Json, FlavorName)
template Writer*(T: type FlavorName): type = Writer(Json, FlavorName)
template PreferredOutputType*(T: type FlavorName): type = string
template mimeType*(T: type FlavorName): string = mimeTypeValue
template flavorUsesAutomaticObjectSerialization*(T: type FlavorName): bool = automaticObjectSerialization
template flavorOmitsOptionalFields*(T: type FlavorName): bool = omitOptionalFields
template flavorRequiresAllFields*(T: type FlavorName): bool = requireAllFields
template flavorAllowsUnknownFields*(T: type FlavorName): bool = allowUnknownFields

View File

@ -1,13 +1,15 @@
{.push raises: [].}
import
std/[unicode, json],
std/[json, unicode],
faststreams/inputs,
types
from std/strutils import isDigit
export
inputs, types
{.push raises: [].}
type
CustomIntHandler* = ##\
## Custom decimal integer parser, result values need to be captured
@ -113,6 +115,7 @@ proc renderTok*(lexer: var JsonLexer, output: var string)
lexer.scanString
else:
discard
# The real stuff
case lexer.tokKind
of tkError, tkEof, tkNumeric, tkExInt, tkExNegInt, tkQuoted, tkExBlob:
@ -153,23 +156,20 @@ template peek(s: InputStream): char =
template read(s: InputStream): char =
char inputs.read(s)
proc hexCharValue(c: char): int =
func hexCharValue(c: char): int =
case c
of '0'..'9': ord(c) - ord('0')
of 'a'..'f': ord(c) - ord('a') + 10
of 'A'..'F': ord(c) - ord('A') + 10
else: -1
proc isDigit(c: char): bool =
return (c >= '0' and c <= '9')
proc col*(lexer: JsonLexer): int =
func col*(lexer: JsonLexer): int =
lexer.stream.pos - lexer.lineStartPos
proc tokenStartCol*(lexer: JsonLexer): int =
func tokenStartCol*(lexer: JsonLexer): int =
1 + lexer.tokenStart - lexer.lineStartPos
proc init*(T: type JsonLexer, stream: InputStream, mode = defaultJsonMode): T =
func init*(T: type JsonLexer, stream: InputStream, mode = defaultJsonMode): T =
T(stream: stream,
mode: mode,
line: 1,
@ -205,7 +205,7 @@ proc scanHexRune(lexer: var JsonLexer): int
if hexValue == -1: error errHexCharExpected
result = (result shl 4) or hexValue
proc scanString(lexer: var JsonLexer) =
proc scanString(lexer: var JsonLexer) {.raises: [IOError].} =
lexer.tokKind = tkString
lexer.strVal.setLen 0
lexer.tokenStart = lexer.stream.pos
@ -256,7 +256,7 @@ proc scanString(lexer: var JsonLexer) =
else:
lexer.strVal.add c
proc handleLF(lexer: var JsonLexer) =
func handleLF(lexer: var JsonLexer) =
advance lexer.stream
lexer.line += 1
lexer.lineStartPos = lexer.stream.pos
@ -343,7 +343,7 @@ proc scanSign(lexer: var JsonLexer): int
elif c == '+':
requireMoreNumberChars: result = 0
advance lexer.stream
return 1
1
proc scanInt(lexer: var JsonLexer): (uint64,bool)
{.gcsafe, raises: [IOError].} =
@ -371,7 +371,6 @@ proc scanInt(lexer: var JsonLexer): (uint64,bool)
# Fetch next digit
c = eatDigitAndPeek() # implicit auto-return
proc scanNumber(lexer: var JsonLexer)
{.gcsafe, raises: [IOError].} =
var sign = lexer.scanSign()
@ -422,9 +421,10 @@ proc scanNumber(lexer: var JsonLexer)
lexer.floatVal = lexer.floatVal / powersOfTen[exponent]
proc scanIdentifier(lexer: var JsonLexer,
expectedIdent: string, expectedTok: TokKind) =
expectedIdent: string, expectedTok: TokKind)
{.raises: [IOError].} =
for c in expectedIdent:
if c != lexer.stream.read():
if c != requireNextChar():
lexer.tokKind = tkError
return
lexer.tokKind = expectedTok
@ -492,11 +492,10 @@ proc tok*(lexer: var JsonLexer): TokKind
lexer.accept
lexer.tokKind
proc lazyTok*(lexer: JsonLexer): TokKind =
func lazyTok*(lexer: JsonLexer): TokKind =
## Preliminary token state unless accepted, already
lexer.tokKind
proc customIntHandler*(lexer: var JsonLexer; handler: CustomIntHandler)
{.gcsafe, raises: [IOError].} =
## Apply the `handler` argument function for parsing a `tkNumeric` type

View File

@ -97,68 +97,100 @@ method formatMsg*(err: ref IncompleteObjectError, filename: string):
string {.gcsafe, raises: [].} =
tryFmt: fmt"{filename}({err.line}, {err.col}) Not all required fields were specified when reading '{err.objectType}'"
proc assignLineNumber*(ex: ref JsonReaderError, r: JsonReader) =
ex.line = r.lexer.line
ex.col = r.lexer.tokenStartCol
func assignLineNumber*(ex: ref JsonReaderError, lexer: JsonLexer) =
ex.line = lexer.line
ex.col = lexer.tokenStartCol
proc raiseUnexpectedToken*(r: JsonReader, expected: ExpectedTokenCategory)
func raiseUnexpectedToken*(lexer: JsonLexer, expected: ExpectedTokenCategory)
{.noreturn, raises: [JsonReaderError].} =
var ex = new UnexpectedTokenError
ex.assignLineNumber(r)
ex.encountedToken = r.lexer.lazyTok
ex.assignLineNumber(lexer)
ex.encountedToken = lexer.lazyTok
ex.expectedToken = expected
raise ex
proc raiseUnexpectedValue*(r: JsonReader, msg: string) {.noreturn, raises: [JsonReaderError].} =
template raiseUnexpectedToken*(reader: JsonReader, expected: ExpectedTokenCategory) =
raiseUnexpectedToken(reader.lexer, expected)
func raiseUnexpectedValue*(
lexer: JsonLexer, msg: string) {.noreturn, raises: [JsonReaderError].} =
var ex = new UnexpectedValueError
ex.assignLineNumber(r)
ex.assignLineNumber(lexer)
ex.msg = msg
raise ex
proc raiseIntOverflow*(r: JsonReader, absIntVal: BiggestUint, isNegative: bool) {.noreturn, raises: [JsonReaderError].} =
template raiseUnexpectedValue*(r: JsonReader, msg: string) =
raiseUnexpectedValue(r.lexer, msg)
func raiseIntOverflow*(
lexer: JsonLexer, absIntVal: BiggestUint, isNegative: bool)
{.noreturn, raises: [JsonReaderError].} =
var ex = new IntOverflowError
ex.assignLineNumber(r)
ex.assignLineNumber(lexer)
ex.absIntVal = absIntVal
ex.isNegative = isNegative
raise ex
proc raiseUnexpectedField*(r: JsonReader, fieldName: string, deserializedType: cstring) {.noreturn, raises: [JsonReaderError].} =
template raiseIntOverflow*(r: JsonReader, absIntVal: BiggestUint, isNegative: bool) =
raiseIntOverflow(r.lexer, absIntVal, isNegative)
func raiseUnexpectedField*(
lexer: JsonLexer, fieldName: string, deserializedType: cstring)
{.noreturn, raises: [JsonReaderError].} =
var ex = new UnexpectedField
ex.assignLineNumber(r)
ex.assignLineNumber(lexer)
ex.encounteredField = fieldName
ex.deserializedType = deserializedType
raise ex
proc raiseIncompleteObject*(r: JsonReader, objectType: cstring) {.noreturn, raises: [JsonReaderError].} =
template raiseUnexpectedField*(r: JsonReader, fieldName: string, deserializedType: cstring) =
raiseUnexpectedField(r.lexer, fieldName, deserializedType)
func raiseIncompleteObject*(
lexer: JsonLexer, objectType: cstring)
{.noreturn, raises: [JsonReaderError].} =
var ex = new IncompleteObjectError
ex.assignLineNumber(r)
ex.assignLineNumber(lexer)
ex.objectType = objectType
raise ex
proc handleReadException*(r: JsonReader,
template raiseIncompleteObject*(r: JsonReader, objectType: cstring) =
raiseIncompleteObject(r.lexer, objectType)
func handleReadException*(lexer: JsonLexer,
Record: type,
fieldName: string,
field: auto,
err: ref CatchableError) {.raises: [JsonReaderError].} =
var ex = new GenericJsonReaderError
ex.assignLineNumber(r)
ex.assignLineNumber(lexer)
ex.deserializedField = fieldName
ex.innerException = err
raise ex
template handleReadException*(r: JsonReader,
Record: type,
fieldName: string,
field: auto,
err: ref CatchableError) =
handleReadException(r.lexer, Record, fieldName, field, err)
proc init*(T: type JsonReader,
stream: InputStream,
mode = defaultJsonMode,
allowUnknownFields = false,
requireAllFields = false): T {.raises: [IOError].} =
result.allowUnknownFields = allowUnknownFields
result.requireAllFields = requireAllFields
mixin flavorAllowsUnknownFields, flavorRequiresAllFields
type Flavor = T.Flavor
result.allowUnknownFields = allowUnknownFields or flavorAllowsUnknownFields(Flavor)
result.requireAllFields = requireAllFields or flavorRequiresAllFields(Flavor)
result.lexer = JsonLexer.init(stream, mode)
result.lexer.next()
proc requireToken*(r: var JsonReader, tk: TokKind) {.raises: [IOError, JsonReaderError].} =
if r.lexer.tok != tk:
r.raiseUnexpectedToken case tk
proc requireToken*(lexer: var JsonLexer, tk: TokKind) {.raises: [IOError, JsonReaderError].} =
if lexer.tok != tk:
lexer.raiseUnexpectedToken case tk
of tkString: etString
of tkInt, tkNegativeInt: etInt
of tkComma: etComma
@ -169,9 +201,9 @@ proc requireToken*(r: var JsonReader, tk: TokKind) {.raises: [IOError, JsonReade
of tkColon: etColon
else: (doAssert false; etBool)
proc skipToken*(r: var JsonReader, tk: TokKind) {.raises: [IOError, JsonReaderError].} =
r.requireToken tk
r.lexer.next()
proc skipToken*(lexer: var JsonLexer, tk: TokKind) {.raises: [IOError, JsonReaderError].} =
lexer.requireToken tk
lexer.next()
proc parseJsonNode(r: var JsonReader): JsonNode
{.gcsafe, raises: [IOError, JsonReaderError].}
@ -182,7 +214,7 @@ proc readJsonNodeField(r: var JsonReader, field: var JsonNode)
r.raiseUnexpectedValue("Unexpected duplicated field name")
r.lexer.next()
r.skipToken tkColon
r.lexer.skipToken tkColon
field = r.parseJsonNode()
@ -203,7 +235,7 @@ proc parseJsonNode(r: var JsonReader): JsonNode =
r.lexer.next()
else:
break
r.skipToken tkCurlyRi
r.lexer.skipToken tkCurlyRi
of tkBracketLe:
result = JsonNode(kind: JArray)
@ -214,7 +246,7 @@ proc parseJsonNode(r: var JsonReader): JsonNode =
if r.lexer.tok == tkBracketRi:
break
else:
r.skipToken tkComma
r.lexer.skipToken tkComma
# Skip over the last tkBracketRi
r.lexer.next()
@ -260,40 +292,40 @@ proc parseJsonNode(r: var JsonReader): JsonNode =
of tkQuoted, tkExBlob, tkNumeric, tkExInt, tkExNegInt:
raiseAssert "generic type " & $r.lexer.lazyTok & " is not applicable"
proc skipSingleJsValue*(r: var JsonReader) {.raises: [IOError, JsonReaderError].} =
case r.lexer.tok
proc skipSingleJsValue*(lexer: var JsonLexer) {.raises: [IOError, JsonReaderError].} =
case lexer.tok
of tkCurlyLe:
r.lexer.next()
if r.lexer.tok != tkCurlyRi:
lexer.next()
if lexer.tok != tkCurlyRi:
while true:
r.skipToken tkString
r.skipToken tkColon
r.skipSingleJsValue()
if r.lexer.tok == tkCurlyRi:
lexer.skipToken tkString
lexer.skipToken tkColon
lexer.skipSingleJsValue()
if lexer.tok == tkCurlyRi:
break
r.skipToken tkComma
lexer.skipToken tkComma
# Skip over the last tkCurlyRi
r.lexer.next()
lexer.next()
of tkBracketLe:
r.lexer.next()
if r.lexer.tok != tkBracketRi:
lexer.next()
if lexer.tok != tkBracketRi:
while true:
r.skipSingleJsValue()
if r.lexer.tok == tkBracketRi:
lexer.skipSingleJsValue()
if lexer.tok == tkBracketRi:
break
else:
r.skipToken tkComma
lexer.skipToken tkComma
# Skip over the last tkBracketRi
r.lexer.next()
lexer.next()
of tkColon, tkComma, tkEof, tkError, tkBracketRi, tkCurlyRi:
r.raiseUnexpectedToken etValue
lexer.raiseUnexpectedToken etValue
of tkString, tkQuoted, tkExBlob,
tkInt, tkNegativeInt, tkFloat, tkNumeric, tkExInt, tkExNegInt,
tkTrue, tkFalse, tkNull:
r.lexer.next()
lexer.next()
proc captureSingleJsValue(r: var JsonReader, output: var string) {.raises: [IOError, SerializationError].} =
r.lexer.renderTok output
@ -303,15 +335,15 @@ proc captureSingleJsValue(r: var JsonReader, output: var string) {.raises: [IOEr
if r.lexer.tok != tkCurlyRi:
while true:
r.lexer.renderTok output
r.skipToken tkString
r.lexer.skipToken tkString
r.lexer.renderTok output
r.skipToken tkColon
r.lexer.skipToken tkColon
r.captureSingleJsValue(output)
r.lexer.renderTok output
if r.lexer.tok == tkCurlyRi:
break
else:
r.skipToken tkComma
r.lexer.skipToken tkComma
else:
output.add '}'
# Skip over the last tkCurlyRi
@ -326,7 +358,7 @@ proc captureSingleJsValue(r: var JsonReader, output: var string) {.raises: [IOEr
if r.lexer.tok == tkBracketRi:
break
else:
r.skipToken tkComma
r.lexer.skipToken tkComma
else:
output.add ']'
# Skip over the last tkBracketRi
@ -340,16 +372,16 @@ proc captureSingleJsValue(r: var JsonReader, output: var string) {.raises: [IOEr
tkTrue, tkFalse, tkNull:
r.lexer.next()
proc allocPtr[T](p: var ptr T) =
func allocPtr[T](p: var ptr T) =
p = create(T)
proc allocPtr[T](p: var ref T) =
func allocPtr[T](p: var ref T) =
p = new(T)
iterator readArray*(r: var JsonReader, ElemType: typedesc): ElemType {.raises: [IOError, SerializationError].} =
mixin readValue
r.skipToken tkBracketLe
r.lexer.skipToken tkBracketLe
if r.lexer.lazyTok != tkBracketRi:
while true:
var res: ElemType
@ -357,13 +389,13 @@ iterator readArray*(r: var JsonReader, ElemType: typedesc): ElemType {.raises: [
yield res
if r.lexer.tok != tkComma: break
r.lexer.next()
r.skipToken tkBracketRi
r.lexer.skipToken tkBracketRi
iterator readObjectFields*(r: var JsonReader,
KeyType: type): KeyType {.raises: [IOError, SerializationError].} =
mixin readValue
r.skipToken tkCurlyLe
r.lexer.skipToken tkCurlyLe
if r.lexer.lazyTok != tkCurlyRi:
while true:
var key: KeyType
@ -373,7 +405,7 @@ iterator readObjectFields*(r: var JsonReader,
yield key
if r.lexer.lazyTok != tkComma: break
r.lexer.next()
r.skipToken tkCurlyRi
r.lexer.skipToken tkCurlyRi
iterator readObject*(r: var JsonReader,
KeyType: type,
@ -385,8 +417,8 @@ iterator readObject*(r: var JsonReader,
readValue(r, value)
yield (fieldName, value)
proc isNotNilCheck[T](x: ref T not nil) {.compileTime.} = discard
proc isNotNilCheck[T](x: ptr T not nil) {.compileTime.} = discard
func isNotNilCheck[T](x: ref T not nil) {.compileTime.} = discard
func isNotNilCheck[T](x: ptr T not nil) {.compileTime.} = discard
func isFieldExpected*(T: type): bool {.compileTime.} =
T isnot Option
@ -422,7 +454,7 @@ func expectedFieldsBitmask*(TT: type): auto {.compileTime.} =
res[i div bitsPerWord].setBitInWord(i mod bitsPerWord)
inc i
return res
res
template setBitInArray[N](data: var array[N, uint], bitIdx: int) =
when data.len > 1:
@ -486,6 +518,66 @@ proc parseEnum[T](
of EnumStyle.AssociatedStrings:
r.raiseUnexpectedToken etEnumString
proc readRecordValue*[T](r: var JsonReader, value: var T)
{.raises: [SerializationError, IOError].} =
type
ReaderType {.used.} = type r
T = type value
r.lexer.skipToken tkCurlyLe
when T.totalSerializedFields > 0:
let
fieldsTable = T.fieldReadersTable(ReaderType)
const
expectedFields = T.expectedFieldsBitmask
var
encounteredFields: typeof(expectedFields)
mostLikelyNextField = 0
while true:
# Have the assignment parsed of the AVP
if r.lexer.lazyTok == tkQuoted:
r.lexer.accept
if r.lexer.lazyTok != tkString:
break
when T is tuple:
let fieldIdx = mostLikelyNextField
mostLikelyNextField += 1
else:
let fieldIdx = findFieldIdx(fieldsTable[],
r.lexer.strVal,
mostLikelyNextField)
if fieldIdx != -1:
let reader = fieldsTable[][fieldIdx].reader
r.lexer.next()
r.lexer.skipToken tkColon
reader(value, r)
encounteredFields.setBitInArray(fieldIdx)
elif r.allowUnknownFields:
r.lexer.next()
r.lexer.skipToken tkColon
r.lexer.skipSingleJsValue()
else:
const typeName = typetraits.name(T)
r.raiseUnexpectedField(r.lexer.strVal, cstring typeName)
if r.lexer.lazyTok == tkComma:
r.lexer.next()
else:
break
if r.requireAllFields and
not expectedFields.isBitwiseSubsetOf(encounteredFields):
const typeName = typetraits.name(T)
r.raiseIncompleteObject(typeName)
r.lexer.accept
r.lexer.skipToken tkCurlyRi
proc readValue*[T](r: var JsonReader, value: var T)
{.gcsafe, raises: [SerializationError, IOError].} =
## Master filed/object parser. This function relies on customised sub-mixins for particular
@ -523,7 +615,6 @@ proc readValue*[T](r: var JsonReader, value: var T)
## reader.lexer.next
##
mixin readValue
type ReaderType {.used.} = type r
when value is (object or tuple):
let tok {.used.} = r.lexer.lazyTok
@ -537,19 +628,19 @@ proc readValue*[T](r: var JsonReader, value: var T)
value = r.parseJsonNode()
elif value is string:
r.requireToken tkString
r.lexer.requireToken tkString
value = r.lexer.strVal
r.lexer.next()
elif value is seq[char]:
r.requireToken tkString
r.lexer.requireToken tkString
value.setLen(r.lexer.strVal.len)
for i in 0..<r.lexer.strVal.len:
value[i] = r.lexer.strVal[i]
r.lexer.next()
elif isCharArray(value):
r.requireToken tkString
r.lexer.requireToken tkString
if r.lexer.strVal.len != value.len:
# Raise tkString because we expected a `"` earlier
r.raiseUnexpectedToken(etString)
@ -630,7 +721,7 @@ proc readValue*[T](r: var JsonReader, value: var T)
r.lexer.next()
elif value is seq:
r.skipToken tkBracketLe
r.lexer.skipToken tkBracketLe
if r.lexer.tok != tkBracketRi:
while true:
let lastPos = value.len
@ -638,74 +729,30 @@ proc readValue*[T](r: var JsonReader, value: var T)
readValue(r, value[lastPos])
if r.lexer.tok != tkComma: break
r.lexer.next()
r.skipToken tkBracketRi
r.lexer.skipToken tkBracketRi
elif value is array:
r.skipToken tkBracketLe
r.lexer.skipToken tkBracketLe
for i in low(value) ..< high(value):
# TODO: dont's ask. this makes the code compile
if false: value[i] = value[i]
readValue(r, value[i])
r.skipToken tkComma
r.lexer.skipToken tkComma
readValue(r, value[high(value)])
r.skipToken tkBracketRi
r.lexer.skipToken tkBracketRi
elif value is (object or tuple):
type T = type(value)
r.skipToken tkCurlyLe
mixin flavorUsesAutomaticObjectSerialization
when T.totalSerializedFields > 0:
let
fieldsTable = T.fieldReadersTable(ReaderType)
type Flavor = JsonReader.Flavor
const isAutomatic =
flavorUsesAutomaticObjectSerialization(Flavor)
const
expectedFields = T.expectedFieldsBitmask
var
encounteredFields: typeof(expectedFields)
mostLikelyNextField = 0
while true:
# Have the assignment parsed of the AVP
if r.lexer.lazyTok == tkQuoted:
r.lexer.accept
if r.lexer.lazyTok != tkString:
break
when T is tuple:
let fieldIdx = mostLikelyNextField
mostLikelyNextField += 1
else:
let fieldIdx = findFieldIdx(fieldsTable[],
r.lexer.strVal,
mostLikelyNextField)
if fieldIdx != -1:
let reader = fieldsTable[][fieldIdx].reader
r.lexer.next()
r.skipToken tkColon
reader(value, r)
encounteredFields.setBitInArray(fieldIdx)
elif r.allowUnknownFields:
r.lexer.next()
r.skipToken tkColon
r.skipSingleJsValue()
else:
when not isAutomatic:
const typeName = typetraits.name(T)
r.raiseUnexpectedField(r.lexer.strVal, cstring typeName)
if r.lexer.lazyTok == tkComma:
r.lexer.next()
else:
break
if r.requireAllFields and
not expectedFields.isBitwiseSubsetOf(encounteredFields):
const typeName = typetraits.name(T)
r.raiseIncompleteObject(typeName)
r.lexer.accept
r.skipToken tkCurlyRi
{.error: "Please override readValue for the " & typeName & " type (or import the module where the override is provided)".}
readRecordValue(r, value)
else:
const typeName = typetraits.name(T)
{.error: "Failed to convert to JSON an unsupported type: " & typeName.}

View File

@ -13,11 +13,12 @@ template writeObjectField*(w: var JsonWriter,
false
proc writeValue*(writer: var JsonWriter, value: Option) {.raises: [IOError].} =
mixin writeValue
mixin writeValue, flavorOmitsOptionalFields
type Flavor = JsonWriter.Flavor
if value.isSome:
writer.writeValue value.get
else:
elif not flavorOmitsOptionalFields(Flavor):
writer.writeValue JsonString("null")
proc readValue*[T](reader: var JsonReader, value: var Option[T]) =

View File

@ -17,11 +17,12 @@ template writeObjectField*[T](w: var JsonWriter,
proc writeValue*[T](
writer: var JsonWriter, value: Result[T, void]) {.raises: [IOError].} =
mixin writeValue
mixin writeValue, flavorOmitsOptionalFields
type Flavor = JsonWriter.Flavor
if value.isOk:
writer.writeValue value.get
else:
elif not flavorOmitsOptionalFields(Flavor):
writer.writeValue JsonString("null")
proc readValue*[T](reader: var JsonReader, value: var Result[T, void]) =

View File

@ -20,4 +20,3 @@ const
template `==`*(lhs, rhs: JsonString): bool =
string(lhs) == string(rhs)

View File

@ -22,7 +22,7 @@ type
Json.setWriter JsonWriter,
PreferredOutput = string
proc init*(W: type JsonWriter, stream: OutputStream,
func init*(W: type JsonWriter, stream: OutputStream,
pretty = false, typeAnnotations = false): W =
W(stream: stream,
hasPrettyOutput: pretty,
@ -152,18 +152,27 @@ template writeObjectField*[FieldType, RecordType](w: var JsonWriter,
field: FieldType): bool =
mixin writeFieldIMPL, writeValue
type
R = type record
w.writeFieldName(fieldName)
when RecordType is tuple:
w.writeValue(field)
else:
type R = type record
w.writeFieldIMPL(FieldTag[R, 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 enumInstanceSerializedFields, writeValue
mixin writeValue
when value is JsonNode:
append if w.hasPrettyOutput: value.pretty
@ -236,14 +245,17 @@ proc writeValue*(w: var JsonWriter, value: auto) {.gcsafe, raises: [IOError].} =
w.writeArray(value)
elif value is (object or tuple):
type RecordType = type value
w.beginRecord RecordType
value.enumInstanceSerializedFields(fieldName, field):
mixin writeObjectField
if writeObjectField(w, value, fieldName, field):
w.state = AfterField
w.endRecord()
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)".}
writeRecordValue(w, value)
else:
const typeName = typetraits.name(value.type)
{.fatal: "Failed to convert to JSON an unsupported type: " & typeName.}
@ -251,10 +263,11 @@ proc writeValue*(w: var JsonWriter, value: auto) {.gcsafe, raises: [IOError].} =
proc toJson*(v: auto, pretty = false, typeAnnotations = false): string =
mixin writeValue
var s = memoryOutput()
var w = JsonWriter[DefaultFlavor].init(s, pretty, typeAnnotations)
var
s = memoryOutput()
w = JsonWriter[DefaultFlavor].init(s, pretty, typeAnnotations)
w.writeValue v
return s.getOutput(string)
s.getOutput(string)
template serializesAsTextInJson*(T: type[enum]) =
template writeValue*(w: var JsonWriter, val: T) =

View File

@ -1,4 +1,3 @@
import
test_lexer,
test_serialization

View File

@ -1,3 +1,5 @@
{.used.}
import
unittest,
../json_serialization/lexer, ./utils

View File

@ -1,3 +1,5 @@
{.used.}
import
strutils, unittest2, json,
serialization/object_serialization,
@ -305,21 +307,18 @@ Meter.borrowSerialization int
template reject(code) {.used.} =
static: doAssert(not compiles(code))
proc `==`(lhs, rhs: Meter): bool =
func `==`(lhs, rhs: Meter): bool =
int(lhs) == int(rhs)
proc `==`(lhs, rhs: ref Simple): bool =
func `==`(lhs, rhs: ref Simple): bool =
if lhs.isNil: return rhs.isNil
if rhs.isNil: return false
return lhs[] == rhs[]
lhs[] == rhs[]
executeReaderWriterTests Json
proc newSimple(x: int, y: string, d: Meter): ref Simple =
new result
result.x = x
result.y = y
result.distance = d
func newSimple(x: int, y: string, d: Meter): ref Simple =
(ref Simple)(x: x, y: y, distance: d)
var invalid = Invalid(distance: Mile(100))
# The compiler cannot handle this check at the moment
@ -360,6 +359,25 @@ EnumTestO.configureJsonDeserialization(
allowNumericRepr = true,
stringNormalizer = nimIdentNormalize)
createJsonFlavor MyJson
type
HasMyJsonDefaultBehavior = object
x*: int
y*: string
HasMyJsonOverride = object
x*: int
y*: string
HasMyJsonDefaultBehavior.useDefaultSerializationIn MyJson
proc readValue*(r: var JsonReader[MyJson], value: var HasMyJsonOverride) =
r.readRecordValue(value)
proc writeValue*(w: var JsonWriter[MyJson], value: HasMyJsonOverride) =
w.writeRecordValue(value)
suite "toJson tests":
test "encode primitives":
check:
@ -531,6 +549,28 @@ suite "toJson tests":
decoded.y == "test"
decoded.distance.int == 20
test "Custom flavor with explicit serialization":
var s = Simple(x: 10, y: "test", distance: Meter(20))
reject:
discard MyJson.encode(s)
let hasDefaultBehavior = HasMyJsonDefaultBehavior(x: 10, y: "test")
let hasOverride = HasMyJsonOverride(x: 10, y: "test")
let json1 = MyJson.encode(hasDefaultBehavior)
let json2 = MyJson.encode(hasOverride)
reject:
let decodedAsMyJson = MyJson.decode(json2, Simple)
check:
json1 == """{"x":10,"y":"test"}"""
json2 == """{"x":10,"y":"test"}"""
MyJson.decode(json1, HasMyJsonDefaultBehavior) == hasDefaultBehavior
MyJson.decode(json2, HasMyJsonOverride) == hasOverride
test "handle additional fields":
let json = test_dedent"""
{

View File

@ -1,14 +1,12 @@
import
strutils
import strutils
# `dedent` exists in newer nim version
# and doesn't behave the same
proc test_dedent*(s: string): string =
var s = s.strip(leading = false)
var minIndent = high(int)
# `dedent` exists in newer Nim version and doesn't behave the same
func test_dedent*(s: string): string =
var
s = s.strip(leading = false)
minIndent = high(int)
for l in s.splitLines:
let indent = count(l, ' ')
if indent == 0: continue
if indent < minIndent: minIndent = indent
result = s.unindent(minIndent)
s.unindent(minIndent)