Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Writer produce correct top-level or in-array optional elem when custo… #73

Merged
merged 1 commit into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion json_serialization/std/options.nim
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ proc writeValue*(writer: var JsonWriter, value: Option) {.raises: [IOError].} =

if value.isSome:
writer.writeValue value.get
elif not flavorOmitsOptionalFields(Flavor):
elif not flavorOmitsOptionalFields(Flavor) or
writer.nesting != JsonNesting.WriteObject:
writer.writeValue JsonString("null")

proc readValue*[T](reader: var JsonReader, value: var Option[T]) =
Expand Down
3 changes: 2 additions & 1 deletion json_serialization/stew/results.nim
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ proc writeValue*[T](

if value.isOk:
writer.writeValue value.get
elif not flavorOmitsOptionalFields(Flavor):
elif not flavorOmitsOptionalFields(Flavor) or
writer.nesting != JsonNesting.WriteObject:
writer.writeValue JsonString("null")

proc readValue*[T](reader: var JsonReader, value: var Result[T, void]) =
Expand Down
19 changes: 18 additions & 1 deletion json_serialization/writer.nim
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,19 @@ 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 @@ -38,7 +45,11 @@ func init*(W: type JsonWriter, stream: OutputStream,
hasPrettyOutput: pretty,
hasTypeAnnotations: typeAnnotations,
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)
Expand Down Expand Up @@ -90,6 +101,8 @@ 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 @@ -109,12 +122,15 @@ 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 @@ -140,6 +156,7 @@ 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 Down
3 changes: 2 additions & 1 deletion tests/test_all.nim
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ import
test_spec,
test_parser,
test_line_col,
test_reader
test_reader,
test_writer
101 changes: 101 additions & 0 deletions tests/test_writer.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# json-serialization
# Copyright (c) 2024 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
# at your option.
# This file may not be copied, modified, or distributed except according to
# those terms.

import
unittest2,
../json_serialization/stew/results,
../json_serialization/std/options,
../json_serialization

createJsonFlavor YourJson,
omitOptionalFields = false

createJsonFlavor MyJson,
omitOptionalFields = true

suite "Test writer":
test "stdlib option top level some YourJson":
var val = some(123)
let json = YourJson.encode(val)
check json == "123"

test "stdlib option top level none YourJson":
var val = none(int)
let json = YourJson.encode(val)
check json == "null"

test "stdlib option top level some MyJson":
var val = some(123)
let json = MyJson.encode(val)
check json == "123"

test "stdlib option top level none MyJson":
var val = none(int)
let json = MyJson.encode(val)
check json == "null"

test "results option top level some YourJson":
var val = Opt.some(123)
let json = YourJson.encode(val)
check json == "123"

test "results option top level none YourJson":
var val = Opt.none(int)
let json = YourJson.encode(val)
check json == "null"

test "results option top level some MyJson":
var val = Opt.some(123)
let json = MyJson.encode(val)
check json == "123"

test "results option top level none MyJson":
var val = Opt.none(int)
let json = MyJson.encode(val)
check json == "null"

test "stdlib option array some YourJson":
var val = [some(123), some(345)]
let json = YourJson.encode(val)
check json == "[123,345]"

test "stdlib option array none YourJson":
var val = [some(123), none(int), some(777)]
let json = YourJson.encode(val)
check json == "[123,null,777]"

test "stdlib option array some MyJson":
var val = [some(123), some(345)]
let json = MyJson.encode(val)
check json == "[123,345]"

test "stdlib option array none MyJson":
var val = [some(123), none(int), some(777)]
let json = MyJson.encode(val)
check json == "[123,null,777]"

test "results option array some YourJson":
var val = [Opt.some(123), Opt.some(345)]
let json = YourJson.encode(val)
check json == "[123,345]"

test "results option array none YourJson":
var val = [Opt.some(123), Opt.none(int), Opt.some(777)]
let json = YourJson.encode(val)
check json == "[123,null,777]"

test "results option array some MyJson":
var val = [Opt.some(123), Opt.some(345)]
let json = MyJson.encode(val)
check json == "[123,345]"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hrm, I think the semantics proposed here could be very surprising for the user. Omitting optional fields in objects in one thing, but filtering such values out of array, so the array length is effectively changed is something that feels much more dangerous.

We could have a separate policy setting that controls this aspect perhaps.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I don't change the array length. The old code is wrong. It didn't output anything between two comma which is violating the spec. Now it works correctly by writing null to that position in the array. It's correct according to the json spec and the rpc spec.


test "results option array none MyJson":
var val = [Opt.some(123), Opt.none(int), Opt.some(777)]
let json = MyJson.encode(val)
check json == "[123,null,777]"