From f42567c00cae04ab4cfce98c8eb47c11c917fa0c Mon Sep 17 00:00:00 2001 From: zah Date: Tue, 19 Dec 2023 12:00:24 +0200 Subject: [PATCH] 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 --- json_serialization.nim | 1 - json_serialization/format.nim | 29 +++ json_serialization/lexer.nim | 35 ++-- json_serialization/reader.nim | 285 ++++++++++++++++------------ json_serialization/std/options.nim | 5 +- json_serialization/stew/results.nim | 5 +- json_serialization/types.nim | 1 - json_serialization/writer.nim | 43 +++-- tests/test_all.nim | 1 - tests/test_lexer.nim | 2 + tests/test_serialization.nim | 56 +++++- tests/utils.nim | 16 +- 12 files changed, 303 insertions(+), 176 deletions(-) diff --git a/json_serialization.nim b/json_serialization.nim index d60bc8f..de62396 100644 --- a/json_serialization.nim +++ b/json_serialization.nim @@ -3,4 +3,3 @@ import export serialization, format, reader, writer - diff --git a/json_serialization/format.nim b/json_serialization/format.nim index 05c889b..0b44d9c 100644 --- a/json_serialization/format.nim +++ b/json_serialization/format.nim @@ -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 diff --git a/json_serialization/lexer.nim b/json_serialization/lexer.nim index b4b3a02..aee603d 100644 --- a/json_serialization/lexer.nim +++ b/json_serialization/lexer.nim @@ -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 diff --git a/json_serialization/reader.nim b/json_serialization/reader.nim index 09d71d7..86787d7 100644 --- a/json_serialization/reader.nim +++ b/json_serialization/reader.nim @@ -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.. 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: - 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 + when not isAutomatic: + const typeName = typetraits.name(T) + {.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.} diff --git a/json_serialization/std/options.nim b/json_serialization/std/options.nim index f36f883..5dd9f24 100644 --- a/json_serialization/std/options.nim +++ b/json_serialization/std/options.nim @@ -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]) = diff --git a/json_serialization/stew/results.nim b/json_serialization/stew/results.nim index f59bee0..0b0a18f 100644 --- a/json_serialization/stew/results.nim +++ b/json_serialization/stew/results.nim @@ -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]) = diff --git a/json_serialization/types.nim b/json_serialization/types.nim index 7e712c7..5a366a5 100644 --- a/json_serialization/types.nim +++ b/json_serialization/types.nim @@ -20,4 +20,3 @@ const template `==`*(lhs, rhs: JsonString): bool = string(lhs) == string(rhs) - diff --git a/json_serialization/writer.nim b/json_serialization/writer.nim index c79ba21..9280809 100644 --- a/json_serialization/writer.nim +++ b/json_serialization/writer.nim @@ -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) = diff --git a/tests/test_all.nim b/tests/test_all.nim index 4de6c23..01ba09b 100644 --- a/tests/test_all.nim +++ b/tests/test_all.nim @@ -1,4 +1,3 @@ import test_lexer, test_serialization - diff --git a/tests/test_lexer.nim b/tests/test_lexer.nim index 211bded..c0554d5 100644 --- a/tests/test_lexer.nim +++ b/tests/test_lexer.nim @@ -1,3 +1,5 @@ +{.used.} + import unittest, ../json_serialization/lexer, ./utils diff --git a/tests/test_serialization.nim b/tests/test_serialization.nim index 1df2efb..40c4e42 100644 --- a/tests/test_serialization.nim +++ b/tests/test_serialization.nim @@ -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""" { diff --git a/tests/utils.nim b/tests/utils.nim index 70d6754..3c19ef7 100644 --- a/tests/utils.nim +++ b/tests/utils.nim @@ -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)