Skip to content

Commit

Permalink
Deconvolute optional fields writer (#77)
Browse files Browse the repository at this point in the history
  • Loading branch information
jangko authored Jan 17, 2024
1 parent 4225359 commit b14f5b5
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 53 deletions.
2 changes: 1 addition & 1 deletion json_serialization/format.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 4 additions & 14 deletions json_serialization/std/options.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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]) =
Expand Down
18 changes: 4 additions & 14 deletions json_serialization/stew/results.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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]) =
Expand Down
42 changes: 19 additions & 23 deletions json_serialization/writer.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/test_serialization.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
34 changes: 34 additions & 0 deletions tests/test_writer.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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}"""

0 comments on commit b14f5b5

Please sign in to comment.