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

Add flavor feature: reader will skip null fields #75

Merged
merged 1 commit into from
Jan 15, 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ You can use `useDefaultSerializationIn` to add serializers of a flavor to a spec
requireAllFields = true
omitOptionalFields = true
allowUnknownFields = true
skipNullFields = false
```

```Nim
Expand Down
6 changes: 5 additions & 1 deletion json_serialization/format.nim
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,23 @@ template flavorUsesAutomaticObjectSerialization*(T: type DefaultFlavor): bool =
template flavorOmitsOptionalFields*(T: type DefaultFlavor): bool = false
template flavorRequiresAllFields*(T: type DefaultFlavor): bool = false
template flavorAllowsUnknownFields*(T: type DefaultFlavor): bool = false
template flavorSkipNullFields*(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 flavorSkipNullFields*(T: type DummyFlavor): bool = false

template createJsonFlavor*(FlavorName: untyped,
mimeTypeValue = "application/json",
automaticObjectSerialization = false,
requireAllFields = true,
omitOptionalFields = true,
allowUnknownFields = true) {.dirty.} =
allowUnknownFields = true,
skipNullFields = false) {.dirty.} =
type FlavorName* = object

template Reader*(T: type FlavorName): type = Reader(Json, FlavorName)
Expand All @@ -49,3 +52,4 @@ template createJsonFlavor*(FlavorName: untyped,
template flavorOmitsOptionalFields*(T: type FlavorName): bool = omitOptionalFields
template flavorRequiresAllFields*(T: type FlavorName): bool = requireAllFields
template flavorAllowsUnknownFields*(T: type FlavorName): bool = allowUnknownFields
template flavorSkipNullFields*(T: type FlavorName): bool = skipNullFields
14 changes: 12 additions & 2 deletions json_serialization/lexer.nim
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,11 @@ proc scanString*[T](lex: var JsonLexer, val: var T, limit: int)
proc scanValue*[T](lex: var JsonLexer, val: var T)
{.gcsafe, raises: [IOError].}

proc tokKind*(lex: var JsonLexer): JsonValueKind
{.gcsafe, raises: [IOError].}

template parseObjectImpl*(lex: JsonLexer,
skipNullFields: static[bool],
actionInitial: untyped,
actionClosing: untyped,
actionComma: untyped,
Expand Down Expand Up @@ -645,7 +649,13 @@ template parseObjectImpl*(lex: JsonLexer,
error(lex, errColonExpected, actionError)

lex.advance
actionValue
when skipNullFields:
if lex.tokKind() == JsonValueKind.Null:
lex.scanNull()
else:
actionValue
else:
actionValue
if not lex.ok: actionError
else:
error(lex, errStringExpected, actionError)
Expand All @@ -657,7 +667,7 @@ proc scanObject*[T](lex: var JsonLexer, val: var T)
when T isnot (string or JsonVoid or JsonObjectType):
{.fatal: "`scanObject` only accepts `string` or `JsonVoid` or `JsonObjectType`".}

parseObjectImpl(lex):
parseObjectImpl(lex, false):
# initial action
when T is string:
val.add '{'
Expand Down
36 changes: 30 additions & 6 deletions json_serialization/parser.nim
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ proc parseNumber*(r: var JsonReader, val: var JsonNumber)
r.raiseParserError(errNumberExpected)
r.lex.scanNumber(val)
r.checkError

proc toInt*(r: var JsonReader, val: JsonNumber, T: type SomeSignedInt, portable: bool): T
{.gcsafe, raises: [JsonReaderError].}=
if val.sign == JsonSign.Neg:
Expand Down Expand Up @@ -291,14 +291,20 @@ proc parseFloat*(r: var JsonReader, T: type SomeFloat): T

proc parseAsString*(r: var JsonReader, val: var string)
{.gcsafe, raises: [IOError, JsonReaderError].} =
mixin flavorSkipNullFields
type
Reader = typeof r
Flavor = Reader.Flavor
const skipNullFields = flavorSkipNullFields(Flavor)

case r.tokKind
of JsonValueKind.String:
escapeJson(r.parseString(), val)
of JsonValueKind.Number:
r.lex.scanNumber(val)
r.checkError
of JsonValueKind.Object:
parseObjectImpl(r.lex):
parseObjectImpl(r.lex, skipNullFields):
# initial action
val.add '{'
do: # closing action
Expand Down Expand Up @@ -355,7 +361,7 @@ proc parseValue*(r: var JsonReader, val: var JsonValueRef)
{.gcsafe, raises: [IOError, JsonReaderError].} =
r.lex.scanValue(val)
r.checkError

template parseArray*(r: var JsonReader; body: untyped) =
if r.tokKind != JsonValueKind.Array:
r.raiseParserError(errBracketLeExpected)
Expand All @@ -375,9 +381,15 @@ template parseArray*(r: var JsonReader; idx: untyped; body: untyped) =
do: r.raiseParserError() # error action

template parseObject*(r: var JsonReader, key: untyped, body: untyped) =
mixin flavorSkipNullFields
type
Reader = typeof r
Flavor = Reader.Flavor
const skipNullFields = flavorSkipNullFields(typeof Flavor)

if r.tokKind != JsonValueKind.Object:
r.raiseParserError(errCurlyLeExpected)
parseObjectImpl(r.lex): discard # initial action
parseObjectImpl(r.lex, skipNullFields): discard # initial action
do: discard # closing action
do: discard # comma action
do: # key action
Expand All @@ -388,9 +400,15 @@ template parseObject*(r: var JsonReader, key: untyped, body: untyped) =
r.raiseParserError()

template parseObjectCustomKey*(r: var JsonReader, keyAction: untyped, body: untyped) =
mixin flavorSkipNullFields
type
Reader = typeof r
Flavor = Reader.Flavor
const skipNullFields = flavorSkipNullFields(Flavor)

if r.tokKind != JsonValueKind.Object:
r.raiseParserError(errCurlyLeExpected)
parseObjectImpl(r.lex): discard # initial action
parseObjectImpl(r.lex, skipNullFields): discard # initial action
do: discard # closing action
do: discard # comma action
do: # key action
Expand All @@ -414,6 +432,12 @@ proc readJsonNodeField(r: var JsonReader, field: var JsonNode)
field = r.parseJsonNode()

proc parseJsonNode(r: var JsonReader): JsonNode =
mixin flavorSkipNullFields
type
Reader = typeof r
Flavor = Reader.Flavor
const skipNullFields = flavorSkipNullFields(Flavor)

case r.tokKind
of JsonValueKind.String:
result = JsonNode(kind: JString, str: r.parseString())
Expand All @@ -428,7 +452,7 @@ proc parseJsonNode(r: var JsonReader): JsonNode =
r.toInt(val, typeof(result.num), JsonReaderFlag.portableInt in r.lex.flags))
of JsonValueKind.Object:
result = JsonNode(kind: JObject)
parseObjectImpl(r.lex): discard # initial action
parseObjectImpl(r.lex, skipNullFields): discard # initial action
do: discard # closing action
do: discard # comma action
do: # key action
Expand Down
28 changes: 28 additions & 0 deletions tests/test_json_flavor.nim
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ type
three: JsonNumber[string]
four: JsonValueRef[uint64]

ListOnly = object
list: JsonString

Container.useDefaultSerializationIn StringyJson

createJsonFlavor OptJson
Expand All @@ -71,8 +74,21 @@ const
}
}
}

"""
jsonTextWithNullFields = """
{
"list": null
}
"""

createJsonFlavor NullyFields,
skipNullFields = true,
requireAllFields = false

Container.useDefaultSerializationIn NullyFields
ListOnly.useDefaultSerializationIn NullyFields

suite "Test JsonFlavor":
test "basic test":
let c = Container(name: "c", x: -10, y: 20, list: @[1'i64, 2, 25])
Expand Down Expand Up @@ -104,3 +120,15 @@ suite "Test JsonFlavor":
check:
ww == vv
xx == """{"two":-789.0009e-19,"three":999.776000e33,"four":{"apple":[1,true,"three"],"banana":{"chip":123,"z":null,"v":false}}}"""

test "object with null fields":
expect JsonReaderError:
let x = Json.decode(jsonTextWithNullFields, Container)
discard x

let x = NullyFields.decode(jsonTextWithNullFields, Container)
check x.list.len == 0

# field should not processed at all
let y = NullyFields.decode(jsonTextWithNullFields, ListOnly)
check y.list.string.len == 0