mirror of
https://github.com/logos-storage/nim-ethers.git
synced 2026-01-15 12:03:08 +00:00
476 lines
13 KiB
Nim
476 lines
13 KiB
Nim
|
|
import std/json as stdjson except `%`, `%*`
|
|
import std/macros
|
|
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
|
|
import pkg/contractabi
|
|
import pkg/stew/byteutils
|
|
import pkg/stint
|
|
import pkg/questionable
|
|
import pkg/questionable/results
|
|
|
|
import ../../basics
|
|
|
|
export stdjson except `%`, `%*`, parseJson
|
|
export chronicles except toJson
|
|
export sets
|
|
|
|
{.push raises: [].}
|
|
|
|
logScope:
|
|
topics = "json de/serialization"
|
|
|
|
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
|
|
|
|
# template serializeAll* {.pragma.}
|
|
template serialize*(key = "", ignore = false) {.pragma.}
|
|
template deserialize*(key = "", mode = DeserializeMode.Default) {.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)
|
|
|
|
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:
|
|
raiseAssert(msg)
|
|
|
|
proc mapErrTo[E1: ref CatchableError, E2: SerdeError](
|
|
e1: E1,
|
|
_: type E2,
|
|
msg: string = e1.msg): ref E2 =
|
|
|
|
return newException(E2, msg, e1)
|
|
|
|
proc newSerdeError(msg: string): ref SerdeError =
|
|
newException(SerdeError, msg)
|
|
|
|
proc newUnexpectedKindError(
|
|
expectedType: type,
|
|
expectedKinds: string,
|
|
json: JsonNode
|
|
): ref UnexpectedKindError =
|
|
let kind = if json.isNil: "nil"
|
|
else: $json.kind
|
|
newException(UnexpectedKindError,
|
|
"deserialization to " & $expectedType & " failed: expected " &
|
|
expectedKinds & " but got " & $kind)
|
|
|
|
proc newUnexpectedKindError(
|
|
expectedType: type,
|
|
expectedKinds: set[JsonNodeKind],
|
|
json: JsonNode
|
|
): ref UnexpectedKindError =
|
|
newUnexpectedKindError(expectedType, $expectedKinds, json)
|
|
|
|
proc newUnexpectedKindError(
|
|
expectedType: type,
|
|
expectedKind: JsonNodeKind,
|
|
json: JsonNode
|
|
): ref UnexpectedKindError =
|
|
newUnexpectedKindError(expectedType, {expectedKind}, json)
|
|
|
|
template expectJsonKind(
|
|
expectedType: type,
|
|
expectedKinds: set[JsonNodeKind],
|
|
json: JsonNode
|
|
) =
|
|
if json.isNil or json.kind notin expectedKinds:
|
|
return failure(newUnexpectedKindError(expectedType, expectedKinds, json))
|
|
|
|
template expectJsonKind*(
|
|
expectedType: type,
|
|
expectedKind: JsonNodeKind,
|
|
json: JsonNode
|
|
) =
|
|
expectJsonKind(expectedType, {expectedKind}, json)
|
|
|
|
proc fieldKeys[T](obj: T): seq[string] =
|
|
for name, _ in fieldPairs(when type(T) is ref: obj[] else: obj):
|
|
result.add name
|
|
|
|
func keysNotIn[T](json: JsonNode, obj: T): HashSet[string] =
|
|
let jsonKeys = json.keys.toSeq.toHashSet
|
|
let objKeys = obj.fieldKeys.toHashSet
|
|
difference(jsonKeys, objKeys)
|
|
|
|
proc fromJson*(
|
|
T: type enum,
|
|
json: JsonNode
|
|
): ?!T =
|
|
expectJsonKind(string, JString, json)
|
|
without val =? parseEnum[T](json.str).catch, error:
|
|
return failure error.mapErrTo(SerdeError)
|
|
return success val
|
|
|
|
proc fromJson*(
|
|
_: type string,
|
|
json: JsonNode
|
|
): ?!string =
|
|
if json.isNil:
|
|
return failure newSerdeError("'json' expected, but was nil")
|
|
elif json.kind == JNull:
|
|
return success("null")
|
|
elif json.isNil or json.kind != JString:
|
|
return failure newUnexpectedKindError(string, JString, json)
|
|
catch json.getStr
|
|
|
|
proc fromJson*(
|
|
_: type bool,
|
|
json: JsonNode
|
|
): ?!bool =
|
|
expectJsonKind(bool, JBool, json)
|
|
catch json.getBool
|
|
|
|
proc fromJson*(
|
|
_: type int,
|
|
json: JsonNode
|
|
): ?!int =
|
|
expectJsonKind(int, JInt, json)
|
|
catch json.getInt
|
|
|
|
proc fromJson*[T: SomeInteger](
|
|
_: type T,
|
|
json: JsonNode
|
|
): ?!T =
|
|
when T is uint|uint64 or (not defined(js) and int.sizeof == 4):
|
|
expectJsonKind(T, {JInt, JString}, json)
|
|
case json.kind
|
|
of JString:
|
|
without x =? parseBiggestUInt(json.str).catch, error:
|
|
return failure newSerdeError(error.msg)
|
|
return success cast[T](x)
|
|
else:
|
|
return success T(json.num)
|
|
else:
|
|
expectJsonKind(T, {JInt}, json)
|
|
return success cast[T](json.num)
|
|
|
|
proc fromJson*[T: SomeFloat](
|
|
_: type T,
|
|
json: JsonNode
|
|
): ?!T =
|
|
expectJsonKind(T, {JInt, JFloat, JString}, json)
|
|
if json.kind == JString:
|
|
case json.str
|
|
of "nan":
|
|
let b = NaN
|
|
return success T(b)
|
|
# dst = NaN # would fail some tests because range conversions would cause CT error
|
|
# in some cases; but this is not a hot-spot inside this branch and backend can optimize this.
|
|
of "inf":
|
|
let b = Inf
|
|
return success T(b)
|
|
of "-inf":
|
|
let b = -Inf
|
|
return success T(b)
|
|
else:
|
|
let err = newUnexpectedKindError(T, "'nan|inf|-inf'", json)
|
|
return failure(err)
|
|
else:
|
|
if json.kind == JFloat:
|
|
return success T(json.fnum)
|
|
else:
|
|
return success T(json.num)
|
|
|
|
proc fromJson*(
|
|
_: type seq[byte],
|
|
json: JsonNode
|
|
): ?!seq[byte] =
|
|
expectJsonKind(seq[byte], JString, json)
|
|
hexToSeqByte(json.getStr).catch
|
|
|
|
proc fromJson*[N: static[int], T: array[N, byte]](
|
|
_: type T,
|
|
json: JsonNode
|
|
): ?!T =
|
|
expectJsonKind(T, JString, json)
|
|
T.fromHex(json.getStr).catch
|
|
|
|
proc fromJson*[T: distinct](
|
|
_: type T,
|
|
json: JsonNode
|
|
): ?!T =
|
|
success T(? T.distinctBase.fromJson(json))
|
|
|
|
proc fromJson*[N: static[int], T: StUint[N]](
|
|
_: type T,
|
|
json: JsonNode
|
|
): ?!T =
|
|
expectJsonKind(T, JString, json)
|
|
let jsonStr = json.getStr
|
|
let prefix = jsonStr[0..1].toLowerAscii
|
|
case prefix:
|
|
of "0x": catch parse(jsonStr, T, 16)
|
|
of "0o": catch parse(jsonStr, T, 8)
|
|
of "0b": catch parse(jsonStr, T, 2)
|
|
else: catch parse(jsonStr, T)
|
|
|
|
proc fromJson*[T](
|
|
_: type Option[T],
|
|
json: JsonNode
|
|
): ?! Option[T] =
|
|
if json.isNil or json.kind == JNull:
|
|
return success(none T)
|
|
without val =? T.fromJson(json), error:
|
|
return failure(error)
|
|
success(val.some)
|
|
|
|
proc fromJson*[T](
|
|
_: type seq[T],
|
|
json: JsonNode
|
|
): ?! seq[T] =
|
|
expectJsonKind(seq[T], JArray, json)
|
|
var arr: seq[T] = @[]
|
|
for elem in json.elems:
|
|
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
|
|
): ?!T =
|
|
|
|
when T is JsonNode:
|
|
return success T(json)
|
|
|
|
expectJsonKind(T, JObject, json)
|
|
var res = when type(T) is ref: T.new() else: T.default
|
|
let mode = T.getDeserializationMode()
|
|
|
|
for name, value in fieldPairs(when type(T) is ref: res[] else: res):
|
|
logScope:
|
|
field = $T & "." & name
|
|
mode
|
|
|
|
let key = getDeserializationKey(name, value)
|
|
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)
|
|
|
|
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
|
|
skip = true
|
|
elif key notin json:
|
|
return failure newSerdeError("object field missing in json: " & key)
|
|
|
|
if key in json and
|
|
jsonVal =? json{key}.catch and
|
|
not jsonVal.isNil and
|
|
not skip:
|
|
|
|
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
|
|
|
|
elif mode == DeserializeMode.Default:
|
|
debug "object field missing in json, skipping", key, json
|
|
|
|
# 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)
|
|
|
|
success(res)
|
|
|
|
proc parseJson*(json: string): ?!JsonNode =
|
|
## fix for nim raising Exception
|
|
try:
|
|
return stdjson.parseJson(json).catch
|
|
except Exception as e:
|
|
return err newException(CatchableError, e.msg)
|
|
|
|
proc fromJson*[T: ref object or object](
|
|
_: type T,
|
|
bytes: seq[byte]
|
|
): ?!T =
|
|
let json = ? parse(string.fromBytes(bytes))
|
|
T.fromJson(json)
|
|
|
|
proc fromJson*(
|
|
_: type JsonNode,
|
|
jsn: string
|
|
): ?!JsonNode =
|
|
return json.parseJson(jsn)
|
|
|
|
proc fromJson*[T: ref object or object](
|
|
_: type T,
|
|
jsn: string
|
|
): ?!T =
|
|
let jsn = ? json.parseJson(jsn) # full qualification required in-module only
|
|
T.fromJson(jsn)
|
|
|
|
func `%`*(s: string): JsonNode = newJString(s)
|
|
|
|
func `%`*(n: uint): JsonNode =
|
|
if n > cast[uint](int.high):
|
|
newJString($n)
|
|
else:
|
|
newJInt(BiggestInt(n))
|
|
|
|
func `%`*(n: int): JsonNode = newJInt(n)
|
|
|
|
func `%`*(n: BiggestUInt): JsonNode =
|
|
if n > cast[BiggestUInt](BiggestInt.high):
|
|
newJString($n)
|
|
else:
|
|
newJInt(BiggestInt(n))
|
|
|
|
func `%`*(n: BiggestInt): JsonNode = newJInt(n)
|
|
|
|
func `%`*(n: float): JsonNode =
|
|
if n != n: newJString("nan")
|
|
elif n == Inf: newJString("inf")
|
|
elif n == -Inf: newJString("-inf")
|
|
else: newJFloat(n)
|
|
|
|
func `%`*(b: bool): JsonNode = newJBool(b)
|
|
|
|
func `%`*(keyVals: openArray[tuple[key: string, val: JsonNode]]): JsonNode =
|
|
if keyVals.len == 0: return newJArray()
|
|
let jObj = newJObject()
|
|
for key, val in items(keyVals): jObj.fields[key] = val
|
|
jObj
|
|
|
|
template `%`*(j: JsonNode): JsonNode = j
|
|
|
|
func `%`*[T](table: Table[string, T]|OrderedTable[string, T]): JsonNode =
|
|
let jObj = newJObject()
|
|
for k, v in table: jObj[k] = ? %v
|
|
jObj
|
|
|
|
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.")
|
|
|
|
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)
|
|
|
|
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
|
|
|
|
jsonObj
|
|
|
|
proc `%`*(o: enum): JsonNode = % $o
|
|
|
|
func `%`*(stint: StInt|StUint): JsonNode = %stint.toString
|
|
|
|
func `%`*(cstr: cstring): JsonNode = % $cstr
|
|
|
|
func `%`*(arr: openArray[byte]): JsonNode = % arr.to0xHex
|
|
|
|
func `%`*[T](elements: openArray[T]): JsonNode =
|
|
let jObj = newJArray()
|
|
for elem in elements: jObj.add(%elem)
|
|
jObj
|
|
|
|
func `%`*[T: distinct](id: T): JsonNode =
|
|
type baseType = T.distinctBase
|
|
% baseType(id)
|
|
|
|
func toJson*[T](item: T): string = $(%item)
|
|
|
|
proc toJsnImpl(x: NimNode): NimNode =
|
|
case x.kind
|
|
of nnkBracket: # array
|
|
if x.len == 0: return newCall(bindSym"newJArray")
|
|
result = newNimNode(nnkBracket)
|
|
for i in 0 ..< x.len:
|
|
result.add(toJsnImpl(x[i]))
|
|
result = newCall(bindSym("%", brOpen), result)
|
|
of nnkTableConstr: # object
|
|
if x.len == 0: return newCall(bindSym"newJObject")
|
|
result = newNimNode(nnkTableConstr)
|
|
for i in 0 ..< x.len:
|
|
x[i].expectKind nnkExprColonExpr
|
|
result.add newTree(nnkExprColonExpr, x[i][0], toJsnImpl(x[i][1]))
|
|
result = newCall(bindSym("%", brOpen), result)
|
|
of nnkCurly: # empty object
|
|
x.expectLen(0)
|
|
result = newCall(bindSym"newJObject")
|
|
of nnkNilLit:
|
|
result = newCall(bindSym"newJNull")
|
|
of nnkPar:
|
|
if x.len == 1: result = toJsnImpl(x[0])
|
|
else: result = newCall(bindSym("%", brOpen), x)
|
|
else:
|
|
result = newCall(bindSym("%", brOpen), x)
|
|
|
|
macro `%*`*(x: untyped): JsonNode =
|
|
## Convert an expression to a JsonNode directly, without having to specify
|
|
## `%` for every element.
|
|
result = toJsnImpl(x)
|