From b14f5b58e91c702f42c82da006890fdef4b6cc70 Mon Sep 17 00:00:00 2001 From: andri lim Date: Wed, 17 Jan 2024 07:48:42 +0700 Subject: [PATCH] Deconvolute optional fields writer (#77) --- json_serialization/format.nim | 2 +- json_serialization/std/options.nim | 18 +++---------- json_serialization/stew/results.nim | 18 +++---------- json_serialization/writer.nim | 42 +++++++++++++---------------- tests/test_serialization.nim | 2 +- tests/test_writer.nim | 34 +++++++++++++++++++++++ 6 files changed, 63 insertions(+), 53 deletions(-) diff --git a/json_serialization/format.nim b/json_serialization/format.nim index cc808da..2f431c4 100644 --- a/json_serialization/format.nim +++ b/json_serialization/format.nim @@ -21,7 +21,7 @@ template supports*(_: type Json, T: type): bool = true template flavorUsesAutomaticObjectSerialization*(T: type DefaultFlavor): bool = true -template flavorOmitsOptionalFields*(T: type DefaultFlavor): bool = false +template flavorOmitsOptionalFields*(T: type DefaultFlavor): bool = true template flavorRequiresAllFields*(T: type DefaultFlavor): bool = false template flavorAllowsUnknownFields*(T: type DefaultFlavor): bool = false template flavorSkipNullFields*(T: type DefaultFlavor): bool = false diff --git a/json_serialization/std/options.nim b/json_serialization/std/options.nim index b5e4353..83c0d38 100644 --- a/json_serialization/std/options.nim +++ b/json_serialization/std/options.nim @@ -10,25 +10,15 @@ import std/options, ../../json_serialization/[reader, writer, lexer] export options -template writeObjectField*(w: var JsonWriter, - record: auto, - fieldName: static string, - field: Option): bool = - mixin writeObjectField - - if field.isSome: - writeObjectField(w, record, fieldName, field.get) - else: - false +template shouldWriteObjectField*(field: Option): bool = + field.isSome proc writeValue*(writer: var JsonWriter, value: Option) {.raises: [IOError].} = - mixin writeValue, flavorOmitsOptionalFields - type Flavor = JsonWriter.Flavor + mixin writeValue if value.isSome: writer.writeValue value.get - elif not flavorOmitsOptionalFields(Flavor) or - writer.nesting != JsonNesting.WriteObject: + else: 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 baa54ef..4c52978 100644 --- a/json_serialization/stew/results.nim +++ b/json_serialization/stew/results.nim @@ -13,26 +13,16 @@ import export results -template writeObjectField*[T](w: var JsonWriter, - record: auto, - fieldName: static string, - field: Result[T, void]): bool = - mixin writeObjectField - - if field.isOk: - writeObjectField(w, record, fieldName, field.get) - else: - false +template shouldWriteObjectField*[T](field: Result[T, void]): bool = + field.isOk proc writeValue*[T]( writer: var JsonWriter, value: Result[T, void]) {.raises: [IOError].} = - mixin writeValue, flavorOmitsOptionalFields - type Flavor = JsonWriter.Flavor + mixin writeValue if value.isOk: writer.writeValue value.get - elif not flavorOmitsOptionalFields(Flavor) or - writer.nesting != JsonNesting.WriteObject: + else: writer.writeValue JsonString("null") proc readValue*[T](reader: var JsonReader, value: var Result[T, void]) = diff --git a/json_serialization/writer.nim b/json_serialization/writer.nim index 6591af2..b157047 100644 --- a/json_serialization/writer.nim +++ b/json_serialization/writer.nim @@ -22,19 +22,12 @@ type RecordStarted AfterField - JsonNesting* {.pure.} = enum - TopLevel - WriteObject - WriteArray - JsonWriter*[Flavor = DefaultFlavor] = object stream*: OutputStream hasTypeAnnotations: bool hasPrettyOutput*: bool # read-only nestingLevel*: int # read-only state: JsonWriterState - nesting: JsonNesting - prevNesting: seq[JsonNesting] Json.setWriter JsonWriter, PreferredOutput = string @@ -45,11 +38,7 @@ func init*(W: type JsonWriter, stream: OutputStream, hasPrettyOutput: pretty, hasTypeAnnotations: typeAnnotations, nestingLevel: if pretty: 0 else: -1, - state: RecordExpected, - nesting: JsonNesting.TopLevel) - -func nesting*(w: JsonWriter): JsonNesting = - w.nesting + state: RecordExpected) proc beginRecord*(w: var JsonWriter, T: type) proc beginRecord*(w: var JsonWriter) @@ -101,8 +90,6 @@ template fieldWritten*(w: var JsonWriter) = proc beginRecord*(w: var JsonWriter) = doAssert w.state == RecordExpected - w.prevNesting.add w.nesting - w.nesting = JsonNesting.WriteObject append '{' if w.hasPrettyOutput: w.nestingLevel += 2 @@ -122,15 +109,12 @@ proc endRecord*(w: var JsonWriter) = indent() append '}' - w.nesting = w.prevNesting.pop() template endRecordField*(w: var JsonWriter) = endRecord(w) w.state = AfterField iterator stepwiseArrayCreation*[C](w: var JsonWriter, collection: C): auto = - w.prevNesting.add w.nesting - w.nesting = JsonNesting.WriteArray append '[' if w.hasPrettyOutput: @@ -156,7 +140,6 @@ iterator stepwiseArrayCreation*[C](w: var JsonWriter, collection: C): auto = indent() append ']' - w.nesting = w.prevNesting.pop() proc writeIterable*(w: var JsonWriter, collection: auto) = mixin writeValue @@ -173,10 +156,14 @@ 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 +# If it's an optional field, test for it's value before write something. +# If it's non optional field, the field is always written. +template shouldWriteObjectField*[FieldType](field: FieldType): bool = true + template writeObjectField*[FieldType, RecordType](w: var JsonWriter, record: RecordType, fieldName: static string, - field: FieldType): bool = + field: FieldType) = mixin writeFieldIMPL, writeValue w.writeFieldName(fieldName) @@ -185,17 +172,26 @@ template writeObjectField*[FieldType, RecordType](w: var JsonWriter, 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 + mixin flavorOmitsOptionalFields, shouldWriteObjectField + + type + Writer = typeof w + Flavor = Writer.Flavor type RecordType = type value w.beginRecord RecordType - value.enumInstanceSerializedFields(fieldName, fieldType): - when fieldType isnot JsonVoid: - if writeObjectField(w, value, fieldName, fieldType): + value.enumInstanceSerializedFields(fieldName, fieldValue): + when fieldValue isnot JsonVoid: + when flavorOmitsOptionalFields(Flavor): + if shouldWriteObjectField(fieldValue): + writeObjectField(w, value, fieldName, fieldValue) + w.state = AfterField + else: + writeObjectField(w, value, fieldName, fieldValue) w.state = AfterField else: discard fieldName diff --git a/tests/test_serialization.nim b/tests/test_serialization.nim index 472885b..0112712 100644 --- a/tests/test_serialization.nim +++ b/tests/test_serialization.nim @@ -988,7 +988,7 @@ suite "Custom parser tests": check dData.name == "FancyUInt" check dData.data.uint == 12345u check customVisit.entry == JsonValueKind.String - + test "Parser on text blob with embedded quote (backlash escape support)": customVisit = TokenRegistry.default diff --git a/tests/test_writer.nim b/tests/test_writer.nim index 2ebe0d8..7ef15fb 100644 --- a/tests/test_writer.nim +++ b/tests/test_writer.nim @@ -13,12 +13,21 @@ import ../json_serialization/std/options, ../json_serialization +type + ObjectWithOptionalFields = object + a: Opt[int] + b: Option[string] + c: int + createJsonFlavor YourJson, omitOptionalFields = false createJsonFlavor MyJson, omitOptionalFields = true +ObjectWithOptionalFields.useDefaultSerializationIn YourJson +ObjectWithOptionalFields.useDefaultSerializationIn MyJson + suite "Test writer": test "stdlib option top level some YourJson": var val = some(123) @@ -99,3 +108,28 @@ suite "Test writer": var val = [Opt.some(123), Opt.none(int), Opt.some(777)] let json = MyJson.encode(val) check json == "[123,null,777]" + + test "object with optional fields": + let x = ObjectWithOptionalFields( + a: Opt.some(123), + b: some("nano"), + c: 456, + ) + + let y = ObjectWithOptionalFields( + a: Opt.none(int), + b: none(string), + c: 999, + ) + + let u = YourJson.encode(x) + check u.string == """{"a":123,"b":"nano","c":456}""" + + let v = YourJson.encode(y) + check v.string == """{"a":null,"b":null,"c":999}""" + + let xx = MyJson.encode(x) + check xx.string == """{"a":123,"b":"nano","c":456}""" + + let yy = MyJson.encode(y) + check yy.string == """{"c":999}"""