diff --git a/ethers/providers/jsonrpc/json.nim b/ethers/providers/jsonrpc/json.nim index a921557..e87c366 100644 --- a/ethers/providers/jsonrpc/json.nim +++ b/ethers/providers/jsonrpc/json.nim @@ -5,7 +5,6 @@ import std/options import std/sequtils import std/sets import std/strutils -# import std/strformat import std/tables import std/typetraits import pkg/chronicles except toJson @@ -29,29 +28,33 @@ logScope: type SerdeError* = object of EthersError UnexpectedKindError* = object of SerdeError - DeserializeMode* = enum - Default, ## objects can have more or less fields than json - OptIn, ## json must have fields marked with {.serialize.} - Strict ## object fields and json fields must match exactly + SerdeMode* = enum + OptOut, ## serialize: all object fields will be serialized, except fields marked with 'ignore' + ## deserialize: all json keys will be deserialized, no error if extra json field + OptIn, ## serialize: only object fields marked with serialize will be serialzied + ## deserialize: only fields marked with deserialize will be deserialized + Strict ## serialize: all object fields will be serialized, regardless if the field is marked with 'ignore' + ## deserialize: object fields and json fields must match exactly + SerdeFieldOptions = object + key: string + ignore: bool -# template serializeAll* {.pragma.} -template serialize*(key = "", ignore = false) {.pragma.} -template deserialize*(key = "", mode = DeserializeMode.Default) {.pragma.} +template serialize*(key = "", ignore = false, mode = SerdeMode.OptOut) {.pragma.} +template deserialize*(key = "", ignore = false, mode = SerdeMode.OptOut) {.pragma.} -template expectEmptyPragma(value, pragma, msg) = - static: - when value.hasCustomPragma(pragma): - const params = value.getCustomPragmaVal(pragma) - for param in params.fields: - if param != typeof(param).default: - raiseAssert(msg) +proc isDefault[T](paramValue: T): bool {.compileTime.} = + var result = paramValue == T.default + when T is SerdeMode: + return paramValue == SerdeMode.OptOut + return result template expectMissingPragmaParam(value, pragma, name, msg) = static: when value.hasCustomPragma(pragma): const params = value.getCustomPragmaVal(pragma) for paramName, paramValue in params.fieldPairs: - if paramName == name and paramValue != typeof(paramValue).default: + + if paramName == name and not paramValue.isDefault: raiseAssert(msg) proc mapErrTo[E1: ref CatchableError, E2: SerdeError]( @@ -113,6 +116,49 @@ func keysNotIn[T](json: JsonNode, obj: T): HashSet[string] = let objKeys = obj.fieldKeys.toHashSet difference(jsonKeys, objKeys) +template getSerdeFieldOptions(pragma, fieldName, fieldValue): SerdeFieldOptions = + var opts = SerdeFieldOptions(key: fieldName, ignore: false) + when fieldValue.hasCustomPragma(pragma): + fieldValue.expectMissingPragmaParam(pragma, "mode", + "Cannot set " & astToStr(pragma) & " 'mode' on '" & fieldName & "' field defintion.") + let (key, ignore, _) = fieldValue.getCustomPragmaVal(pragma) + opts.ignore = ignore + if key != "": + opts.key = key + opts + +template getSerdeMode(T, pragma): SerdeMode = + when T.hasCustomPragma(pragma): + T.expectMissingPragmaParam(pragma, "key", + "Cannot set " & astToStr(pragma) & " 'key' on '" & $T & + "' type definition.") + T.expectMissingPragmaParam(pragma, "ignore", + "Cannot set " & astToStr(pragma) & " 'ignore' on '" & $T & + "' type definition.") + let (_, _, mode) = T.getCustomPragmaVal(pragma) + mode + else: + # Default mode -- when the type is NOT annotated with a + # serialize/deserialize pragma. + # + # NOTE This may be different in the logic branch above, when the type is + # annotated with serialize/deserialize but doesn't specify a mode. The + # default in that case will fallback to the default mode specified in the + # pragma signature (currently OptOut for both serialize and deserialize) + # + # Examples: + # 1. type MyObj = object + # Type is not annotated, mode defaults to OptOut (as specified on the + # pragma signatures) for both serialization and deserializtion + # + # 2. type MyObj {.serialize, deserialize.} = object + # Type is annotated, mode defaults to OptIn for serialization and OptOut + # for deserialization + when pragma == serialize: + SerdeMode.OptIn + elif pragma == deserialize: + SerdeMode.OptOut + proc fromJson*( T: type enum, json: JsonNode @@ -245,31 +291,6 @@ proc fromJson*[T]( arr.add(? T.fromJson(elem)) success arr -template getSerializationKey(fieldName, fieldValue): string = - when fieldValue.hasCustomPragma(serialize): - let (key, _) = fieldValue.getCustomPragmaVal(serialize) - if key != "": key - else: fieldName - else: fieldName - -template getDeserializationKey(fieldName, fieldValue): string = - let serializationKey = getSerializationKey(fieldName, fieldValue) - when fieldValue.hasCustomPragma(deserialize): - fieldValue.expectMissingPragmaParam(deserialize, "mode", - "Cannot set 'mode' on field defintion.") - let (key, mode) = fieldValue.getCustomPragmaVal(deserialize) - if key != "": key - else: serializationKey # defaults to fieldName - else: serializationKey # defaults to fieldName - -template getDeserializationMode(T): DeserializeMode = - when T.hasCustomPragma(deserialize): - T.expectMissingPragmaParam(deserialize, "key", - "Cannot set 'key' on object definition.") - T.getCustomPragmaVal(deserialize)[1] # mode = second pragma param - else: - DeserializeMode.Default - proc fromJson*[T: ref object or object]( _: type T, json: JsonNode @@ -280,48 +301,75 @@ proc fromJson*[T: ref object or object]( expectJsonKind(T, JObject, json) var res = when type(T) is ref: T.new() else: T.default - let mode = T.getDeserializationMode() + let mode = T.getSerdeMode(deserialize) + + # ensure there's no extra fields in json + if mode == SerdeMode.Strict: + let extraFields = json.keysNotIn(res) + if extraFields.len > 0: + return failure newSerdeError("json field(s) missing in object: " & $extraFields) for name, value in fieldPairs(when type(T) is ref: res[] else: res): + logScope: field = $T & "." & name mode - let key = getDeserializationKey(name, value) + let hasDeserializePragma = value.hasCustomPragma(deserialize) + let opts = getSerdeFieldOptions(deserialize, name, value) + let isOptionalValue = typeof(value) is Option var skip = false # workaround for 'continue' not supported in a 'fields' loop - if mode == Strict and key notin json: - return failure newSerdeError("object field missing in json: " & key) + case mode: + of Strict: + if opts.key notin json: + return failure newSerdeError("object field missing in json: " & opts.key) + elif opts.ignore: + # unable to figure out a way to make this a compile time check + warn "object field marked as 'ignore' while in Strict mode, field will be deserialized anyway" - if mode == OptIn: - if not value.hasCustomPragma(deserialize): - debug "object field not marked as 'deserialize', skipping", name = name - # use skip as workaround for 'continue' not supported in a 'fields' loop + of OptIn: + if not hasDeserializePragma: + debug "object field not marked as 'deserialize', skipping" skip = true - elif key notin json: - return failure newSerdeError("object field missing in json: " & key) + elif opts.ignore: + debug "object field marked as 'ignore', skipping" + skip = true + elif opts.key notin json and not isOptionalValue: + return failure newSerdeError("object field missing in json: " & opts.key) - if key in json and - jsonVal =? json{key}.catch and - not jsonVal.isNil and - not skip: + of OptOut: + if opts.ignore: + debug "object field is opted out of deserialization ('igore' is set), skipping" + skip = true + elif hasDeserializePragma and opts.key == name: + warn "object field marked as deserialize in OptOut mode, but 'ignore' not set, field will be deserialized" - without parsed =? type(value).fromJson(jsonVal), e: - warn "failed to deserialize field", - `type` = $typeof(value), - json = jsonVal, - error = e.msg - return failure(e) - value = parsed + if not skip: - elif mode == DeserializeMode.Default: - debug "object field missing in json, skipping", key, json + if isOptionalValue: - # ensure there's no extra fields in json - if mode == DeserializeMode.Strict: - let extraFields = json.keysNotIn(res) - if extraFields.len > 0: - return failure newSerdeError("json field(s) missing in object: " & $extraFields) + let jsonVal = json{opts.key} + without parsed =? typeof(value).fromJson(jsonVal), e: + debug "failed to deserialize field", + `type` = $typeof(value), + json = jsonVal, + error = e.msg + return failure(e) + value = parsed + + # not Option[T] + elif opts.key in json and + jsonVal =? json{opts.key}.catch and + not jsonVal.isNil: + + without parsed =? typeof(value).fromJson(jsonVal), e: + debug "failed to deserialize field", + `type` = $typeof(value), + json = jsonVal, + error = e.msg + return failure(e) + value = parsed success(res) @@ -395,32 +443,46 @@ func `%`*[T](opt: Option[T]): JsonNode = if opt.isSome: %(opt.get) else: newJNull() -func `%`*[T: object or ref object](obj: T): JsonNode = - - # T.expectMissingPragma(serialize, "Invalid pragma on object definition.") +proc `%`*[T: object or ref object](obj: T): JsonNode = let jsonObj = newJObject() let o = when T is ref object: obj[] else: obj - T.expectEmptyPragma(serialize, "Cannot specify 'key' or 'ignore' on object defition") - - const serializeAllFields = T.hasCustomPragma(serialize) + let mode = T.getSerdeMode(serialize) for name, value in o.fieldPairs: - # TODO: move to % - # value.expectMissingPragma(deserializeMode, "Invalid pragma on field definition.") - # static: - const serializeField = value.hasCustomPragma(serialize) - when serializeField: - let (keyOverride, ignore) = value.getCustomPragmaVal(serialize) - if not ignore: - let key = if keyOverride != "": keyOverride - else: name - jsonObj[key] = %value - elif serializeAllFields: - jsonObj[name] = %value + logScope: + field = $T & "." & name + mode + + let opts = getSerdeFieldOptions(serialize, name, value) + const serializeField = value.hasCustomPragma(serialize) + var skip = false # workaround for 'continue' not supported in a 'fields' loop + + case mode: + of OptIn: + if not serializeField: + debug "object field not marked with serialize, skipping" + skip = true + elif opts.ignore: + skip = true + + of OptOut: + if opts.ignore: + debug "object field opted out of serialization ('ignore' is set), skipping" + skip = true + elif serializeField and opts.key == name: # all serialize params are default + warn "object field marked as serialize in OptOut mode, but 'ignore' not set, field will be serialized" + + of Strict: + if opts.ignore: + # unable to figure out a way to make this a compile time check + warn "object field marked as 'ignore' while in Strict mode, field will be serialized anyway" + + if not skip: + jsonObj[opts.key] = %value jsonObj @@ -441,7 +503,7 @@ func `%`*[T: distinct](id: T): JsonNode = type baseType = T.distinctBase % baseType(id) -func toJson*[T](item: T): string = $(%item) +proc toJson*[T](item: T): string = $(%item) proc toJsnImpl(x: NimNode): NimNode = case x.kind diff --git a/testmodule/providers/jsonrpc/testjson.nim b/testmodule/providers/jsonrpc/testjson.nim index 1e9aa73..93718f4 100644 --- a/testmodule/providers/jsonrpc/testjson.nim +++ b/testmodule/providers/jsonrpc/testjson.nim @@ -228,6 +228,7 @@ suite "json serialization - deserialize": check deserialized.mystring == expected.mystring check deserialized.myint == expected.myint + suite "json serialization pragmas": test "fails to compile when object marked with 'serialize' specifies options": @@ -284,12 +285,37 @@ suite "json serialization pragmas": check compiles(%MyObj()) + +suite "json serialization, mode = OptIn": + + test "serializes with default mode OptIn when object not marked with serialize": + type MyObj = object + field1 {.serialize.}: bool + field2: bool + + let obj = MyObj(field1: true, field2: true) + check obj.toJson == """{"field1":true}""" + + test "not marking object with serialize is equivalent to marking it with serialize in OptIn mode": + type MyObj = object + field1 {.serialize.}: bool + field2: bool + + type MyObjMarked {.serialize(mode=OptIn).} = object + field1 {.serialize.}: bool + field2: bool + + let obj = MyObj(field1: true, field2: true) + let objMarked = MyObjMarked(field1: true, field2: true) + check obj.toJson == objMarked.toJson + test "serializes field with key when specified": type MyObj = object - field {.serialize("test").}: bool + field1 {.serialize("test").}: bool + field2 {.serialize.}: bool - let obj = MyObj(field: true) - check obj.toJson == """{"test":true}""" + let obj = MyObj(field1: true, field2: true) + check obj.toJson == """{"test":true,"field2":true}""" test "does not serialize ignored field": type MyObj = object @@ -299,7 +325,29 @@ suite "json serialization pragmas": let obj = MyObj(field1: true, field2: true) check obj.toJson == """{"field1":true}""" - test "serialize on object definition serializes all fields": + +suite "json deserialization, mode = OptIn": + + test "deserializes only fields marked as deserialize when mode is OptIn": + type MyObj {.deserialize(mode=OptIn).} = object + field1: int + field2 {.deserialize.}: bool + + let val = !MyObj.fromJson("""{"field1":true,"field2":true}""") + check val == MyObj(field1: 0, field2: true) + + test "deserializes Optional fields when mode is OptIn": + type MyObj {.deserialize(mode=OptIn).} = object + field1 {.deserialize.}: bool + field2 {.deserialize.}: Option[bool] + + let val = !MyObj.fromJson("""{"field1":true}""") + check val == MyObj(field1: true, field2: none bool) + + +suite "json serialization, mode = OptOut": + + test "serialize on object definition defaults to OptOut mode, serializes all fields": type MyObj {.serialize.} = object field1: bool field2: bool @@ -307,7 +355,20 @@ suite "json serialization pragmas": let obj = MyObj(field1: true, field2: true) check obj.toJson == """{"field1":true,"field2":true}""" - test "ignores field when object has serialize": + test "not specifying serialize mode is equivalent to specifying OptOut mode": + type MyObj {.serialize.} = object + field1: bool + field2: bool + + type MyObjMarked {.serialize(mode=OptOut).} = object + field1: bool + field2: bool + + let obj = MyObj(field1: true, field2: true) + let objMarked = MyObjMarked(field1: true, field2: true) + check obj.toJson == objMarked.toJson + + test "ignores field when marked with ignore": type MyObj {.serialize.} = object field1 {.serialize(ignore=true).}: bool field2: bool @@ -315,7 +376,7 @@ suite "json serialization pragmas": let obj = MyObj(field1: true, field2: true) check obj.toJson == """{"field2":true}""" - test "serializes field with key when object has serialize": + test "serializes field with key instead of field name": type MyObj {.serialize.} = object field1 {.serialize("test").}: bool field2: bool @@ -323,6 +384,81 @@ suite "json serialization pragmas": let obj = MyObj(field1: true, field2: true) check obj.toJson == """{"test":true,"field2":true}""" + +suite "json deserialization, mode = OptOut": + + test "deserializes object in OptOut mode when not marked with deserialize": + type MyObj = object + field1: bool + field2: bool + + let val = !MyObj.fromJson("""{"field1":true,"field3":true}""") + check val == MyObj(field1: true, field2: false) + + test "deserializes object field with marked json key": + type MyObj = object + field1 {.deserialize("test").}: bool + field2: bool + + let val = !MyObj.fromJson("""{"test":true,"field2":true}""") + check val == MyObj(field1: true, field2: true) + + test "fails to deserialize object field with wrong type": + type MyObj = object + field1: int + field2: bool + + let r = MyObj.fromJson("""{"field1":true,"field2":true}""") + check r.isFailure + check r.error of UnexpectedKindError + check r.error.msg == "deserialization to int failed: expected {JInt} but got JBool" + + test "does not deserialize ignored fields in OptOut mode": + type MyObj = object + field1 {.deserialize(ignore=true).}: bool + field2: bool + + let val = !MyObj.fromJson("""{"field1":true,"field2":true}""") + check val == MyObj(field1: false, field2: true) + + test "deserializes fields when marked with deserialize but not ignored": + type MyObj = object + field1 {.deserialize.}: bool + field2: bool + + let val = !MyObj.fromJson("""{"field1":true,"field2":true}""") + check val == MyObj(field1: true, field2: true) + + test "deserializes Optional field": + type MyObj = object + field1: Option[bool] + field2: bool + + let val = !MyObj.fromJson("""{"field2":true}""") + check val == MyObj(field1: none bool, field2: true) + + +suite "json serialization - mode = Strict": + + test "serializes all fields in Strict mode": + type MyObj {.serialize(mode=Strict).} = object + field1: bool + field2: bool + + let obj = MyObj(field1: true, field2: true) + check obj.toJson == """{"field1":true,"field2":true}""" + + test "ignores ignored fields in Strict mode": + type MyObj {.serialize(mode=Strict).} = object + field1 {.serialize(ignore=true).}: bool + field2: bool + + let obj = MyObj(field1: true, field2: true) + check obj.toJson == """{"field1":true,"field2":true}""" + + +suite "json deserialization, mode = Strict": + test "deserializes matching object and json fields when mode is Strict": type MyObj {.deserialize(mode=Strict).} = object field1: bool @@ -350,52 +486,13 @@ suite "json serialization pragmas": check r.error of SerdeError check r.error.msg == "json field(s) missing in object: {\"field1\"}" - test "deserializes only fields marked as deserialize when mode is OptIn": - type MyObj {.deserialize(mode=OptIn).} = object - field1: int - field2 {.deserialize.}: bool + test "deserializes ignored fields in Strict mode": + type MyObj {.deserialize(mode=Strict).} = object + field1 {.deserialize(ignore=true).}: bool + field2: bool let val = !MyObj.fromJson("""{"field1":true,"field2":true}""") - check val == MyObj(field1: 0, field2: true) - - test "can deserialize object in default mode when not marked with deserialize": - type MyObj = object - field1: bool - field2: bool - - let val = !MyObj.fromJson("""{"field1":true,"field3":true}""") - check val == MyObj(field1: true, field2: false) - - test "deserializes object field with marked json key": - type MyObj = object - field1 {.deserialize("test").}: bool - field2: bool - - let val = !MyObj.fromJson("""{"test":true,"field2":true}""") check val == MyObj(field1: true, field2: true) - test "deserialization key can be set using serialize key": - type MyObj = object - field1 {.serialize("test").}: bool - field2: bool - let val = !MyObj.fromJson("""{"test":true,"field2":true}""") - check val == MyObj(field1: true, field2: true) - test "deserialization key takes priority over serialize key": - type MyObj = object - field1 {.serialize("test"), deserialize("test1").}: bool - field2: bool - - let val = !MyObj.fromJson("""{"test":false,"test1":true,"field2":true}""") - check val == MyObj(field1: true, field2: true) - - test "fails to deserialize object field with wrong type": - type MyObj = object - field1: int - field2: bool - - let r = MyObj.fromJson("""{"field1":true,"field2":true}""") - check r.isFailure - check r.error of UnexpectedKindError - check r.error.msg == "deserialization to int failed: expected {JInt} but got JBool"