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:
parent
230e226da0
commit
f42567c00c
|
@ -3,4 +3,3 @@ import
|
|||
|
||||
export
|
||||
serialization, format, reader, writer
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.}
|
||||
|
|
|
@ -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]) =
|
||||
|
|
|
@ -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]) =
|
||||
|
|
|
@ -20,4 +20,3 @@ const
|
|||
|
||||
template `==`*(lhs, rhs: JsonString): bool =
|
||||
string(lhs) == string(rhs)
|
||||
|
||||
|
|
|
@ -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) =
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import
|
||||
test_lexer,
|
||||
test_serialization
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
{.used.}
|
||||
|
||||
import
|
||||
unittest,
|
||||
../json_serialization/lexer, ./utils
|
||||
|
|
|
@ -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"""
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue