Deconvolute optional fields writer (#77)

This commit is contained in:
andri lim 2024-01-17 07:48:42 +07:00 committed by GitHub
parent 42253591b9
commit b14f5b58e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 63 additions and 53 deletions

View File

@ -21,7 +21,7 @@ template supports*(_: type Json, T: type): bool =
true true
template flavorUsesAutomaticObjectSerialization*(T: type DefaultFlavor): 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 flavorRequiresAllFields*(T: type DefaultFlavor): bool = false
template flavorAllowsUnknownFields*(T: type DefaultFlavor): bool = false template flavorAllowsUnknownFields*(T: type DefaultFlavor): bool = false
template flavorSkipNullFields*(T: type DefaultFlavor): bool = false template flavorSkipNullFields*(T: type DefaultFlavor): bool = false

View File

@ -10,25 +10,15 @@
import std/options, ../../json_serialization/[reader, writer, lexer] import std/options, ../../json_serialization/[reader, writer, lexer]
export options export options
template writeObjectField*(w: var JsonWriter, template shouldWriteObjectField*(field: Option): bool =
record: auto, field.isSome
fieldName: static string,
field: Option): bool =
mixin writeObjectField
if field.isSome:
writeObjectField(w, record, fieldName, field.get)
else:
false
proc writeValue*(writer: var JsonWriter, value: Option) {.raises: [IOError].} = proc writeValue*(writer: var JsonWriter, value: Option) {.raises: [IOError].} =
mixin writeValue, flavorOmitsOptionalFields mixin writeValue
type Flavor = JsonWriter.Flavor
if value.isSome: if value.isSome:
writer.writeValue value.get writer.writeValue value.get
elif not flavorOmitsOptionalFields(Flavor) or else:
writer.nesting != JsonNesting.WriteObject:
writer.writeValue JsonString("null") writer.writeValue JsonString("null")
proc readValue*[T](reader: var JsonReader, value: var Option[T]) = proc readValue*[T](reader: var JsonReader, value: var Option[T]) =

View File

@ -13,26 +13,16 @@ import
export export
results results
template writeObjectField*[T](w: var JsonWriter, template shouldWriteObjectField*[T](field: Result[T, void]): bool =
record: auto, field.isOk
fieldName: static string,
field: Result[T, void]): bool =
mixin writeObjectField
if field.isOk:
writeObjectField(w, record, fieldName, field.get)
else:
false
proc writeValue*[T]( proc writeValue*[T](
writer: var JsonWriter, value: Result[T, void]) {.raises: [IOError].} = writer: var JsonWriter, value: Result[T, void]) {.raises: [IOError].} =
mixin writeValue, flavorOmitsOptionalFields mixin writeValue
type Flavor = JsonWriter.Flavor
if value.isOk: if value.isOk:
writer.writeValue value.get writer.writeValue value.get
elif not flavorOmitsOptionalFields(Flavor) or else:
writer.nesting != JsonNesting.WriteObject:
writer.writeValue JsonString("null") writer.writeValue JsonString("null")
proc readValue*[T](reader: var JsonReader, value: var Result[T, void]) = proc readValue*[T](reader: var JsonReader, value: var Result[T, void]) =

View File

@ -22,19 +22,12 @@ type
RecordStarted RecordStarted
AfterField AfterField
JsonNesting* {.pure.} = enum
TopLevel
WriteObject
WriteArray
JsonWriter*[Flavor = DefaultFlavor] = object JsonWriter*[Flavor = DefaultFlavor] = object
stream*: OutputStream stream*: OutputStream
hasTypeAnnotations: bool hasTypeAnnotations: bool
hasPrettyOutput*: bool # read-only hasPrettyOutput*: bool # read-only
nestingLevel*: int # read-only nestingLevel*: int # read-only
state: JsonWriterState state: JsonWriterState
nesting: JsonNesting
prevNesting: seq[JsonNesting]
Json.setWriter JsonWriter, Json.setWriter JsonWriter,
PreferredOutput = string PreferredOutput = string
@ -45,11 +38,7 @@ func init*(W: type JsonWriter, stream: OutputStream,
hasPrettyOutput: pretty, hasPrettyOutput: pretty,
hasTypeAnnotations: typeAnnotations, hasTypeAnnotations: typeAnnotations,
nestingLevel: if pretty: 0 else: -1, nestingLevel: if pretty: 0 else: -1,
state: RecordExpected, state: RecordExpected)
nesting: JsonNesting.TopLevel)
func nesting*(w: JsonWriter): JsonNesting =
w.nesting
proc beginRecord*(w: var JsonWriter, T: type) proc beginRecord*(w: var JsonWriter, T: type)
proc beginRecord*(w: var JsonWriter) proc beginRecord*(w: var JsonWriter)
@ -101,8 +90,6 @@ template fieldWritten*(w: var JsonWriter) =
proc beginRecord*(w: var JsonWriter) = proc beginRecord*(w: var JsonWriter) =
doAssert w.state == RecordExpected doAssert w.state == RecordExpected
w.prevNesting.add w.nesting
w.nesting = JsonNesting.WriteObject
append '{' append '{'
if w.hasPrettyOutput: if w.hasPrettyOutput:
w.nestingLevel += 2 w.nestingLevel += 2
@ -122,15 +109,12 @@ proc endRecord*(w: var JsonWriter) =
indent() indent()
append '}' append '}'
w.nesting = w.prevNesting.pop()
template endRecordField*(w: var JsonWriter) = template endRecordField*(w: var JsonWriter) =
endRecord(w) endRecord(w)
w.state = AfterField w.state = AfterField
iterator stepwiseArrayCreation*[C](w: var JsonWriter, collection: C): auto = iterator stepwiseArrayCreation*[C](w: var JsonWriter, collection: C): auto =
w.prevNesting.add w.nesting
w.nesting = JsonNesting.WriteArray
append '[' append '['
if w.hasPrettyOutput: if w.hasPrettyOutput:
@ -156,7 +140,6 @@ iterator stepwiseArrayCreation*[C](w: var JsonWriter, collection: C): auto =
indent() indent()
append ']' append ']'
w.nesting = w.prevNesting.pop()
proc writeIterable*(w: var JsonWriter, collection: auto) = proc writeIterable*(w: var JsonWriter, collection: auto) =
mixin writeValue 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[N](v: array[N, char]): bool = true
template isStringLike(v: auto): bool = false 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, template writeObjectField*[FieldType, RecordType](w: var JsonWriter,
record: RecordType, record: RecordType,
fieldName: static string, fieldName: static string,
field: FieldType): bool = field: FieldType) =
mixin writeFieldIMPL, writeValue mixin writeFieldIMPL, writeValue
w.writeFieldName(fieldName) w.writeFieldName(fieldName)
@ -185,17 +172,26 @@ template writeObjectField*[FieldType, RecordType](w: var JsonWriter,
else: else:
type R = type record type R = type record
w.writeFieldIMPL(FieldTag[R, fieldName], field, record) w.writeFieldIMPL(FieldTag[R, fieldName], field, record)
true
proc writeRecordValue*(w: var JsonWriter, value: auto) proc writeRecordValue*(w: var JsonWriter, value: auto)
{.gcsafe, raises: [IOError].} = {.gcsafe, raises: [IOError].} =
mixin enumInstanceSerializedFields, writeObjectField mixin enumInstanceSerializedFields, writeObjectField
mixin flavorOmitsOptionalFields, shouldWriteObjectField
type
Writer = typeof w
Flavor = Writer.Flavor
type RecordType = type value type RecordType = type value
w.beginRecord RecordType w.beginRecord RecordType
value.enumInstanceSerializedFields(fieldName, fieldType): value.enumInstanceSerializedFields(fieldName, fieldValue):
when fieldType isnot JsonVoid: when fieldValue isnot JsonVoid:
if writeObjectField(w, value, fieldName, fieldType): when flavorOmitsOptionalFields(Flavor):
if shouldWriteObjectField(fieldValue):
writeObjectField(w, value, fieldName, fieldValue)
w.state = AfterField
else:
writeObjectField(w, value, fieldName, fieldValue)
w.state = AfterField w.state = AfterField
else: else:
discard fieldName discard fieldName

View File

@ -988,7 +988,7 @@ suite "Custom parser tests":
check dData.name == "FancyUInt" check dData.name == "FancyUInt"
check dData.data.uint == 12345u check dData.data.uint == 12345u
check customVisit.entry == JsonValueKind.String check customVisit.entry == JsonValueKind.String
test "Parser on text blob with embedded quote (backlash escape support)": test "Parser on text blob with embedded quote (backlash escape support)":
customVisit = TokenRegistry.default customVisit = TokenRegistry.default

View File

@ -13,12 +13,21 @@ import
../json_serialization/std/options, ../json_serialization/std/options,
../json_serialization ../json_serialization
type
ObjectWithOptionalFields = object
a: Opt[int]
b: Option[string]
c: int
createJsonFlavor YourJson, createJsonFlavor YourJson,
omitOptionalFields = false omitOptionalFields = false
createJsonFlavor MyJson, createJsonFlavor MyJson,
omitOptionalFields = true omitOptionalFields = true
ObjectWithOptionalFields.useDefaultSerializationIn YourJson
ObjectWithOptionalFields.useDefaultSerializationIn MyJson
suite "Test writer": suite "Test writer":
test "stdlib option top level some YourJson": test "stdlib option top level some YourJson":
var val = some(123) var val = some(123)
@ -99,3 +108,28 @@ suite "Test writer":
var val = [Opt.some(123), Opt.none(int), Opt.some(777)] var val = [Opt.some(123), Opt.none(int), Opt.some(777)]
let json = MyJson.encode(val) let json = MyJson.encode(val)
check json == "[123,null,777]" 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}"""