bugfix: even the Option fields were mandatory under requireAllFields

This commit is contained in:
Zahary Karadjov 2022-07-14 15:14:10 +03:00
parent 090749e577
commit aca66fbd8e
No known key found for this signature in database
GPG Key ID: C8936F8A3073D609
3 changed files with 114 additions and 20 deletions

View File

@ -394,6 +394,55 @@ iterator readObject*(r: var JsonReader,
proc isNotNilCheck[T](x: ref T not nil) {.compileTime.} = discard
proc isNotNilCheck[T](x: ptr T not nil) {.compileTime.} = discard
func isFieldExpected*(T: type): bool {.compileTime.} =
T isnot Option
func totalExpectedFields*(T: type): int {.compileTime.} =
mixin isFieldExpected,
enumAllSerializedFields
enumAllSerializedFields(T):
if isFieldExpected(FieldType):
inc result
func setBit*(x: var uint, bit: int) {.inline.} =
let mask = uint(1) shl bit
x = x or mask
const bitsPerWord = sizeof(uint) * 8
func expectedFieldsBitmask*(TT: type): auto {.compileTime.} =
type T = TT
mixin isFieldExpected,
enumAllSerializedFields
const requiredWords =
(totalExpectedFields(T) + bitsPerWord - 1) div bitsPerWord
var res: array[requiredWords, uint]
var i = 0
enumAllSerializedFields(T):
if isFieldExpected(FieldType):
res[i div bitsPerWord].setBit(i mod bitsPerWord)
inc i
return res
template setBit[N](data: var array[N, uint], bitIdx: int) =
when N > 1:
data[bitIdx div bitsPerWord].setBit(bitIdx mod bitsPerWord)
else:
data[0].setBit(bitIdx)
func isBitwiseSubsetOf[N](lhs, rhs: array[N, uint]): bool =
for i in low(lhs) .. high(lhs):
if (lhs[i] and rhs[i]) != lhs[i]:
return false
true
# this construct catches `array[N, char]` which otherwise won't decompose into
# openArray[char] - we treat any array-like thing-of-characters as a string in
# the output
@ -564,44 +613,54 @@ proc readValue*[T](r: var JsonReader, value: var T)
type T = type(value)
r.skipToken tkCurlyLe
const expectedFields = T.totalSerializedFields
var readFields = 0
when expectedFields > 0:
let fields = T.fieldReadersTable(ReaderType)
var expectedFieldPos = 0
when T.totalSerializedFields > 0:
let
fieldsTable = T.fieldReadersTable(ReaderType)
const
expectedFields = T.expectedFieldsBitmask
var
encounteredFields: typeof(expectedFields)
mostLikelyNextField = 0
while true:
# Have the assignment parsed of the AVP
if r.lexer.lazyTok == tkQuoted:
r.lexer.accept
if r.lexer.lazyTok != tkString:
break
# Calculate/assemble handler
when T is tuple:
var reader = fields[][expectedFieldPos].reader
expectedFieldPos += 1
let fieldIdx = mostLikelyNextField
mostLikelyNextField += 1
else:
var reader = findFieldReader(fields[], r.lexer.strVal, expectedFieldPos)
if reader != nil:
let fieldIdx = findFieldIdx(fieldsTable[],
r.lexer.strVal,
mostLikelyNextField)
if fieldIdx != -1:
let reader = fieldsTable[][fieldIdx].reader
r.lexer.next()
r.skipToken tkColon
reader(value, r)
inc readFields
else:
encounteredFields.setBit(fieldIdx)
elif r.allowUnknownFields:
r.lexer.next()
r.skipToken tkColon
if r.allowUnknownFields:
r.skipSingleJsValue()
else:
const typeName = typetraits.name(T)
r.raiseUnexpectedField(r.lexer.strVal, cstring typeName)
r.skipSingleJsValue()
else:
const typeName = typetraits.name(T)
r.raiseUnexpectedField(r.lexer.strVal, cstring typeName)
if r.lexer.lazyTok == tkComma:
r.lexer.next()
else:
break
if r.requireAllFields and readFields != expectedFields:
const typeName = typetraits.name(T)
r.raiseIncompleteObject(typeName)
if r.requireAllFields and
not expectedFields.isBitwiseSubsetOf(encounteredFields):
const typeName = typetraits.name(T)
r.raiseIncompleteObject(typeName)
r.lexer.accept
r.skipToken tkCurlyRi

View File

@ -30,3 +30,6 @@ proc readValue*[T](reader: var JsonReader, value: var Result[T, void]) =
reader.lexer.next()
else:
value.ok reader.readValue(T)
func isFieldExpected*[T, E](_: type[Result[T, E]]): bool {.compileTime.} =
false

View File

@ -340,6 +340,13 @@ suite "toJson tests":
r == Json.decode("""{"type":"uint8", "renamed":"field"}""", HasUnusualFieldNames)
test "Option types":
check:
2 == static(HoldsOption.totalSerializedFields)
1 == static(HoldsOption.totalExpectedFields)
2 == static(Foo.totalSerializedFields)
2 == static(Foo.totalExpectedFields)
let
h1 = HoldsOption(o: some Simple(x: 1, y: "2", distance: Meter(3)))
h2 = HoldsOption(r: newSimple(1, "2", Meter(3)))
@ -347,7 +354,22 @@ suite "toJson tests":
Json.roundtripTest h1, """{"r":null,"o":{"distance":3,"x":1,"y":"2"}}"""
Json.roundtripTest h2, """{"r":{"distance":3,"x":1,"y":"2"}}"""
let
h3 = Json.decode("""{"r":{"distance":3,"x":1,"y":"2"}}""",
HoldsOption, requireAllFields = true)
check h3 == h2
expect SerializationError:
let h4 = Json.decode("""{"o":{"distance":3,"x":1,"y":"2"}}""",
HoldsOption, requireAllFields = true)
test "Result Opt types":
check:
false == static(isFieldExpected Opt[Simple])
2 == static(HoldsResultOpt.totalSerializedFields)
1 == static(HoldsResultOpt.totalExpectedFields)
let
h1 = HoldsResultOpt(o: Opt[Simple].ok Simple(x: 1, y: "2", distance: Meter(3)))
h2 = HoldsResultOpt(r: newSimple(1, "2", Meter(3)))
@ -355,6 +377,16 @@ suite "toJson tests":
Json.roundtripTest h1, """{"r":null,"o":{"distance":3,"x":1,"y":"2"}}"""
Json.roundtripTest h2, """{"r":{"distance":3,"x":1,"y":"2"}}"""
let
h3 = Json.decode("""{"r":{"distance":3,"x":1,"y":"2"}}""",
HoldsResultOpt, requireAllFields = true)
check h3 == h2
expect SerializationError:
let h4 = Json.decode("""{"o":{"distance":3,"x":1,"y":"2"}}""",
HoldsResultOpt, requireAllFields = true)
test "Custom field serialization":
let obj = WithCustomFieldRule(str: "test", intVal: 10)
Json.roundtripTest obj, """{"str":"test","intVal":"10"}"""