diff --git a/ethers/provider.nim b/ethers/provider.nim index 6cc3058..f660f88 100644 --- a/ethers/provider.nim +++ b/ethers/provider.nim @@ -3,6 +3,7 @@ import pkg/stew/byteutils import ./basics import ./transaction import ./blocktag +import ./providers/jsonrpc/json export basics export transaction @@ -15,19 +16,19 @@ type ProviderError* = object of EthersError Subscription* = ref object of RootObj EventFilter* = ref object of RootObj - address*: Address - topics*: seq[Topic] + address* {.serialize.}: Address + topics* {.serialize.}: seq[Topic] Filter* = ref object of EventFilter - fromBlock*: BlockTag - toBlock*: BlockTag + fromBlock* {.serialize.}: BlockTag + toBlock* {.serialize.}: BlockTag FilterByBlockHash* = ref object of EventFilter - blockHash*: BlockHash + blockHash* {.serialize.}: BlockHash Log* = object - blockNumber*: UInt256 - data*: seq[byte] - logIndex*: UInt256 - removed*: bool - topics*: seq[Topic] + blockNumber* {.serialize.}: UInt256 + data* {.serialize.}: seq[byte] + logIndex* {.serialize.}: UInt256 + removed* {.serialize.}: bool + topics* {.serialize.}: seq[Topic] TransactionHash* = array[32, byte] BlockHash* = array[32, byte] TransactionStatus* = enum @@ -35,45 +36,45 @@ type Success = 1, Invalid = 2 TransactionResponse* = object - provider*: Provider - hash*: TransactionHash + provider* {.serialize.}: Provider + hash* {.serialize.}: TransactionHash TransactionReceipt* = object - sender*: ?Address - to*: ?Address - contractAddress*: ?Address - transactionIndex*: UInt256 - gasUsed*: UInt256 - logsBloom*: seq[byte] - blockHash*: ?BlockHash - transactionHash*: TransactionHash - logs*: seq[Log] - blockNumber*: ?UInt256 - cumulativeGasUsed*: UInt256 - effectiveGasPrice*: ?UInt256 + sender* {.serialize.}: ?Address + to* {.serialize.}: ?Address + contractAddress* {.serialize.}: ?Address + transactionIndex* {.serialize.}: UInt256 + gasUsed* {.serialize.}: UInt256 + logsBloom* {.serialize.}: seq[byte] + blockHash* {.serialize.}: ?BlockHash + transactionHash* {.serialize.}: TransactionHash + logs* {.serialize.}: seq[Log] + blockNumber* {.serialize.}: ?UInt256 + cumulativeGasUsed* {.serialize.}: UInt256 + effectiveGasPrice* {.serialize.}: ?UInt256 status*: TransactionStatus - transactionType*: TransactionType + `type`* {.serialize.}: TransactionType LogHandler* = proc(log: Log) {.gcsafe, raises:[].} BlockHandler* = proc(blck: Block) {.gcsafe, raises:[].} Topic* = array[32, byte] Block* = object - number*: ?UInt256 - timestamp*: UInt256 - hash*: ?BlockHash + number* {.serialize.}: ?UInt256 + timestamp* {.serialize.}: UInt256 + hash* {.serialize.}: ?BlockHash PastTransaction* = object - blockHash*: BlockHash - blockNumber*: UInt256 - sender*: Address - gas*: UInt256 - gasPrice*: UInt256 - hash*: TransactionHash - input*: seq[byte] - nonce*: UInt256 - to*: Address - transactionIndex*: UInt256 - transactionType*: ?TransactionType - chainId*: ?UInt256 - value*: UInt256 - v*, r*, s*: UInt256 + blockHash* {.serialize.}: BlockHash + blockNumber* {.serialize.}: UInt256 + sender* {.serialize.}: Address + gas* {.serialize.}: UInt256 + gasPrice* {.serialize.}: UInt256 + hash* {.serialize.}: TransactionHash + input* {.serialize.}: seq[byte] + nonce* {.serialize.}: UInt256 + to* {.serialize.}: Address + transactionIndex* {.serialize.}: UInt256 + `type`* {.serialize.}: ?TransactionType + chainId* {.serialize.}: ?UInt256 + value* {.serialize.}: UInt256 + v* {.serialize.}, r* {.serialize.}, s* {.serialize.}: UInt256 const EthersDefaultConfirmations* {.intdefine.} = 12 const EthersReceiptTimeoutBlks* {.intdefine.} = 50 # in blocks @@ -91,7 +92,7 @@ func toTransaction*(past: PastTransaction): Transaction = data: past.input, nonce: some past.nonce, to: past.to, - transactionType: past.transactionType, + `type`: past.`type`, gasLimit: some past.gas, chainId: past.chainId ) diff --git a/ethers/providers/jsonrpc/conversions.nim b/ethers/providers/jsonrpc/conversions.nim index 6f89542..1904027 100644 --- a/ethers/providers/jsonrpc/conversions.nim +++ b/ethers/providers/jsonrpc/conversions.nim @@ -1,71 +1,148 @@ -import std/json +# import std/json import std/strformat import std/strutils +import std/sugar +# import pkg/eth/common/eth_types_json_serialization +import pkg/chronicles except fromJson, `%`, `%*`, toJson import pkg/json_rpc/jsonmarshal +import pkg/questionable/results import pkg/stew/byteutils import ../../basics import ../../transaction import ../../blocktag import ../../provider +import ./json export jsonmarshal +export json +export chronicles except fromJson, `%`, `%*`, toJson + +# {.push raises: [].} type JsonSerializationError = object of EthersError +func toException*(v: ref CatchableError): ref SerializationError = (ref SerializationError)(msg: v.msg) + template raiseSerializationError(message: string) = raise newException(JsonSerializationError, message) +proc getOrRaise*[T, E](self: ?!T, exc: typedesc[E]): T {.raises: [E].} = + let val = self.valueOr: + raise newException(E, self.error.msg) + val + proc expectFields(json: JsonNode, expectedFields: varargs[string]) = for fieldName in expectedFields: if not json.hasKey(fieldName): - raiseSerializationError(fmt"'{fieldName}' field not found in ${json}") + raiseSerializationError("'" & fieldName & "' field not found in " & $json) + +# func fromJson*(T: type, json: JsonNode): T +# func fromJson*[T](json: JsonNode, result: var Option[T]) +# func fromJson*[T](json: JsonNode, result: var seq[T]) +# func fromJson*(json: JsonNode, result var T): T + -func fromJson*(T: type, json: JsonNode, name = ""): T = - fromJson(json, name, result) # byte sequence -func `%`*(bytes: seq[byte]): JsonNode = - %("0x" & bytes.toHex) +# func `%`*(bytes: seq[byte]): JsonNode = +# %("0x" & bytes.toHex) -func fromJson*(json: JsonNode, name: string, result: var seq[byte]) = - result = hexToSeqByte(json.getStr()) +# func fromJson*(json: JsonNode, result: var seq[byte]) = +# result = hexToSeqByte(json.getStr()) # byte arrays -func `%`*[N](bytes: array[N, byte]): JsonNode = - %("0x" & bytes.toHex) +# func `%`*[N](bytes: array[N, byte]): JsonNode = +# %("0x" & bytes.toHex) -func fromJson*[N](json: JsonNode, name: string, result: var array[N, byte]) = - hexToByteArray(json.getStr(), result) +# func fromJson*[N](json: JsonNode, result: var array[N, byte]) = +# hexToByteArray(json.getStr(), result) + +# func fromJson*[N](json: JsonNode, result: var seq[array[N, byte]]) = +# for elem in json.getElems: +# var byteArr: array[N, byte] +# fromJson(elem, byteArr) +# result.add byteArr # Address func `%`*(address: Address): JsonNode = %($address) -func fromJson*(json: JsonNode, name: string, result: var Address) = - if address =? Address.init(json.getStr()): - result = address - else: - raise newException(ValueError, "\"" & name & "\"is not an Address") +# func fromJson(jsonVal: string, result: var Address) = +# without address =? Address.init(jsonVal): +# raiseSerializationError "Failed to convert '" & jsonVal & "' to Address" +# result = address + +# func fromJson*(json: JsonNode, result: var Address) = +# let val = json.getStr +# fromJson(val, result) + +proc readValue*(r: var JsonReader[JrpcConv], result: var Address) + {.raises: [SerializationError, IOError].} = + var val = r.readValue(string) + without address =? Address.init(val): + raiseSerializationError "Failed to convert '" & val & "' to Address" + result = address + +proc writeValue*( + writer: var JsonWriter[JrpcConv], + value: Address +) {.raises:[IOError].} = + writer.writeValue(%value) + +# Filter +func `%`*(filter: Filter): JsonNode = + %*{ + "fromBlock": filter.fromBlock, + "toBlock": filter.toBlock + } + +proc writeValue*( + writer: var JsonWriter[JrpcConv], + value: Filter +) {.raises:[IOError].} = + writer.writeValue(%value) + +# EventFilter +func `%`*(filter: EventFilter): JsonNode = + %*{ + "address": filter.address, + "topics": filter.topics + } +proc writeValue*( + writer: var JsonWriter[JrpcConv], + value: EventFilter +) {.raises:[IOError].} = + writer.writeValue(%value) # UInt256 -func `%`*(integer: UInt256): JsonNode = - %("0x" & toHex(integer)) +# func `%`*(integer: UInt256): JsonNode = +# %("0x" & toHex(integer)) -func fromJson*(json: JsonNode, name: string, result: var UInt256) = - result = UInt256.fromHex(json.getStr()) +# func fromJson*(json: JsonNode, result: var UInt256) = +# result = UInt256.fromHex(json.getStr()) -# TransactionType +proc writeValue*( + w: var JsonWriter, value: StUint) {.inline, raises: [IOError].} = + w.writeValue $value -func fromJson*(json: JsonNode, name: string, result: var TransactionType) = - let val = fromHex[int](json.getStr) - result = TransactionType(val) +proc readValue*( + r: var JsonReader, value: var StUint +) {.inline, raises: [IOError, SerializationError].} = + let json = r.readValue(JsonNode) + value = typeof(value).fromJson(json).getOrRaise(SerializationError) -func `%`*(txType: TransactionType): JsonNode = - %("0x" & txType.int.toHex(1)) +# # TransactionType + +# func fromJson*(json: JsonNode, result: var TransactionType) = +# let val = fromHex[int](json.getStr) +# result = TransactionType(val) + +# func `%`*(txType: TransactionType): JsonNode = +# %("0x" & txType.int.toHex(1)) # Transaction @@ -82,107 +159,193 @@ func `%`*(transaction: Transaction): JsonNode = if gasLimit =? transaction.gasLimit: result["gas"] = %gasLimit +proc writeValue*( + writer: var JsonWriter[JrpcConv], + value: Transaction +) {.raises:[IOError].} = + writer.writeValue(%value) + +# Block +# func fromJson*(json: JsonNode, result: var Block) {.raises: [JsonSerializationError].} = +# var number: ?UInt256 +# var timestamp: UInt256 +# var hash: ?BlockHash +# expectFields json, "number", "timestamp", "hash" +# fromJson(json{"number"}, number) +# fromJson(json{"timestamp"}, timestamp) +# fromJson(json{"hash"}, hash) +# result = Block(number: number, timestamp: timestamp, hash: hash) + +proc readValue*(r: var JsonReader[JrpcConv], result: var Option[Block]) + {.raises: [SerializationError, IOError].} = + var json = r.readValue(JsonNode) + if json.isNil or json.kind == JNull: + result = none Block + + # result = some Block() + # result.number = Json.decode(result{"number"}, Option[UInt256]) + result = Option[Block].fromJson(json).getOrRaise(SerializationError) + # without val =? Option[Block].fromJson(json) #.mapErr(e => newException(SerializationError, e.msg)) + # without blk =? Option[Block].fromJson(json), error: + # warn "failed to deserialize into Option[Block]", json + # result = none Block + # result = blk + # if json.isNil or json.kind == JNull: + # result = none Block + # var res: Block + # fromJson(Block, json, res) + # result = some res + # BlockTag -func `%`*(blockTag: BlockTag): JsonNode = - %($blockTag) +# func `%`*(blockTag: BlockTag): JsonNode = +# %($blockTag) + +proc writeValue*( + writer: var JsonWriter[JrpcConv], + value: BlockTag +) {.raises:[IOError].} = + writer.writeValue($value) + +proc readValue*(r: var JsonReader[JrpcConv], + result: var BlockTag) {.raises:[SerializationError, IOError].} = + var json = r.readValue(JsonNode) + result = BlockTag.fromJson(json).getOrRaise(SerializationError) # Log -func fromJson*(json: JsonNode, name: string, result: var Log) = - if not (json.hasKey("data") and json.hasKey("topics")): - raise newException(ValueError, "'data' and/or 'topics' fields not found") +# func fromJson*(json: JsonNode, result: var Log) = +# expectFields json, "data", "topics" - var data: seq[byte] - var topics: seq[Topic] - fromJson(json["data"], "data", data) - fromJson(json["topics"], "topics", topics) - result = Log(data: data, topics: topics) +# var data: seq[byte] +# var topics: seq[Topic] +# fromJson(json["data"], data) +# fromJson(json["topics"], topics) +# result = Log(data: data, topics: topics) # TransactionStatus -func fromJson*(json: JsonNode, name: string, result: var TransactionStatus) = - let val = fromHex[int](json.getStr) - result = TransactionStatus(val) +# func fromJson*(json: JsonNode, result: var TransactionStatus) = +# let val = fromHex[int](json.getStr) +# result = TransactionStatus(val) -func `%`*(status: TransactionStatus): JsonNode = - %("0x" & status.int.toHex(1)) +# func `%`*(status: TransactionStatus): JsonNode = +# %("0x" & status.int.toHex(1)) # PastTransaction -func fromJson*(json: JsonNode, name: string, result: var PastTransaction) = - # Deserializes a past transaction, eg eth_getTransactionByHash. - # Spec: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_gettransactionbyhash - json.expectFields "blockHash", "blockNumber", "from", "gas", "gasPrice", - "hash", "input", "nonce", "to", "transactionIndex", "value", - "v", "r", "s" +proc readValue*(r: var JsonReader[JrpcConv], result: var Option[PastTransaction]) + {.raises: [SerializationError, IOError].} = + var json = r.readValue(JsonNode) + result = Option[PastTransaction].fromJson(json).getOrRaise(SerializationError) - result = PastTransaction( - blockHash: BlockHash.fromJson(json["blockHash"], "blockHash"), - blockNumber: UInt256.fromJson(json["blockNumber"], "blockNumber"), - sender: Address.fromJson(json["from"], "from"), - gas: UInt256.fromJson(json["gas"], "gas"), - gasPrice: UInt256.fromJson(json["gasPrice"], "gasPrice"), - hash: TransactionHash.fromJson(json["hash"], "hash"), - input: seq[byte].fromJson(json["input"], "input"), - nonce: UInt256.fromJson(json["nonce"], "nonce"), - to: Address.fromJson(json["to"], "to"), - transactionIndex: UInt256.fromJson(json["transactionIndex"], "transactionIndex"), - value: UInt256.fromJson(json["value"], "value"), - v: UInt256.fromJson(json["v"], "v"), - r: UInt256.fromJson(json["r"], "r"), - s: UInt256.fromJson(json["s"], "s"), - ) - if json.hasKey("type"): - result.transactionType = fromJson(?TransactionType, json["type"], "type") - if json.hasKey("chainId"): - result.chainId = fromJson(?UInt256, json["chainId"], "chainId") +# func fromJson*(json: JsonNode, result: var PastTransaction) = +# # Deserializes a past transaction, eg eth_getTransactionByHash. +# # Spec: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_gettransactionbyhash +# json.expectFields "blockHash", "blockNumber", "from", "gas", "gasPrice", +# "hash", "input", "nonce", "to", "transactionIndex", "value", +# "v", "r", "s" -func `%`*(tx: PastTransaction): JsonNode = - let json = %*{ - "blockHash": tx.blockHash, - "blockNumber": tx.blockNumber, - "from": tx.sender, - "gas": tx.gas, - "gasPrice": tx.gasPrice, - "hash": tx.hash, - "input": tx.input, - "nonce": tx.nonce, - "to": tx.to, - "transactionIndex": tx.transactionIndex, - "value": tx.value, - "v": tx.v, - "r": tx.r, - "s": tx.s - } - if txType =? tx.transactionType: - json["type"] = %txType - if chainId =? tx.chainId: - json["chainId"] = %chainId - return json +# result = PastTransaction( +# blockHash: BlockHash.fromJson(json["blockHash"]), #, "blockHash"), +# blockNumber: UInt256.fromJson(json["blockNumber"]), #, "blockNumber"), +# sender: Address.fromJson(json["from"]), #, "from"), +# gas: UInt256.fromJson(json["gas"]), #, "gas"), +# gasPrice: UInt256.fromJson(json["gasPrice"]), #, "gasPrice"), +# hash: TransactionHash.fromJson(json["hash"]), #, "hash"), +# input: seq[byte].fromJson(json["input"]), #, "input"), +# nonce: UInt256.fromJson(json["nonce"]), #, "nonce"), +# to: Address.fromJson(json["to"]), #, "to"), +# transactionIndex: UInt256.fromJson(json["transactionIndex"]), #, "transactionIndex"), +# value: UInt256.fromJson(json["value"]), #, "value"), +# v: UInt256.fromJson(json["v"]), #, "v"), +# r: UInt256.fromJson(json["r"]), #, "r"), +# s: UInt256.fromJson(json["s"]) #, "s"), +# ) +# if json.hasKey("type"): +# result.transactionType = fromJson(?TransactionType, json["type"], "type") +# if json.hasKey("chainId"): +# result.chainId = fromJson(?UInt256, json["chainId"], "chainId") + +# func `%`*(tx: PastTransaction): JsonNode = +# let json = %*{ +# "blockHash": tx.blockHash, +# "blockNumber": tx.blockNumber, +# "from": tx.sender, +# "gas": tx.gas, +# "gasPrice": tx.gasPrice, +# "hash": tx.hash, +# "input": tx.input, +# "nonce": tx.nonce, +# "to": tx.to, +# "transactionIndex": tx.transactionIndex, +# "value": tx.value, +# "v": tx.v, +# "r": tx.r, +# "s": tx.s +# } +# if txType =? tx.transactionType: +# json["type"] = %txType +# if chainId =? tx.chainId: +# json["chainId"] = %chainId +# return json # TransactionReceipt -func fromJson*(json: JsonNode, name: string, result: var TransactionReceipt) = - # Deserializes a transaction receipt, eg eth_getTransactionReceipt. - # Spec: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_gettransactionreceipt - json.expectFields "transactionHash", "transactionIndex", "cumulativeGasUsed", - "effectiveGasPrice", "gasUsed", "logs", "logsBloom", "type", - "status" +proc readValue*(r: var JsonReader[JrpcConv], result: var Option[TransactionReceipt]) + {.raises: [SerializationError, IOError].} = + var json = r.readValue(JsonNode) + result = Option[TransactionReceipt].fromJson(json).getOrRaise(SerializationError) - result = TransactionReceipt( - transactionHash: fromJson(TransactionHash, json["transactionHash"], "transactionHash"), - transactionIndex: UInt256.fromJson(json["transactionIndex"], "transactionIndex"), - blockHash: fromJson(?BlockHash, json["blockHash"], "blockHash"), - blockNumber: fromJson(?UInt256, json["blockNumber"], "blockNumber"), - sender: fromJson(?Address, json["from"], "from"), - to: fromJson(?Address, json["to"], "to"), - cumulativeGasUsed: UInt256.fromJson(json["cumulativeGasUsed"], "cumulativeGasUsed"), - effectiveGasPrice: fromJson(?UInt256, json["effectiveGasPrice"], "effectiveGasPrice"), - gasUsed: UInt256.fromJson(json["gasUsed"], "gasUsed"), - contractAddress: fromJson(?Address, json["contractAddress"], "contractAddress"), - logs: seq[Log].fromJson(json["logs"], "logs"), - logsBloom: seq[byte].fromJson(json["logsBloom"], "logsBloom"), - transactionType: TransactionType.fromJson(json["type"], "type"), - status: TransactionStatus.fromJson(json["status"], "status") - ) +# func fromJson*(json: JsonNode, result: var TransactionReceipt) = +# # Deserializes a transaction receipt, eg eth_getTransactionReceipt. +# # Spec: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_gettransactionreceipt +# json.expectFields "transactionHash", "transactionIndex", "cumulativeGasUsed", +# "effectiveGasPrice", "gasUsed", "logs", "logsBloom", "type", +# "status" + +# result = TransactionReceipt( +# transactionHash: fromJson(TransactionHash, json["transactionHash"], "transactionHash"), +# transactionIndex: UInt256.fromJson(json["transactionIndex"], "transactionIndex"), +# blockHash: fromJson(?BlockHash, json["blockHash"], "blockHash"), +# blockNumber: fromJson(?UInt256, json["blockNumber"], "blockNumber"), +# sender: fromJson(?Address, json["from"], "from"), +# to: fromJson(?Address, json["to"], "to"), +# cumulativeGasUsed: UInt256.fromJson(json["cumulativeGasUsed"], "cumulativeGasUsed"), +# effectiveGasPrice: fromJson(?UInt256, json["effectiveGasPrice"], "effectiveGasPrice"), +# gasUsed: UInt256.fromJson(json["gasUsed"], "gasUsed"), +# contractAddress: fromJson(?Address, json["contractAddress"], "contractAddress"), +# logs: seq[Log].fromJson(json["logs"], "logs"), +# logsBloom: seq[byte].fromJson(json["logsBloom"], "logsBloom"), +# transactionType: TransactionType.fromJson(json["type"], "type"), +# status: TransactionStatus.fromJson(json["status"], "status") +# ) + +# func fromJson*[T](json: JsonNode, result: var Option[T]) = +# if json.isNil or json.kind == JNull: +# result = none T +# return + +# var val: T +# fromJson(json, val) +# result = some val +# # if val =? T.fromJson(json): +# # result = some val + +# # # result = none T + +# func fromJson*[T](json: JsonNode, name = "", result: var seq[T]) = +# if json.kind != JArray: +# raiseSerializationError(fmt"Expected JArray to convert to seq, but got {json.kind}") + +# for elem in json.elems: +# var v: T +# fromJson(elem, name, v) +# result.add(v) + +# func fromJson*(T: type, json: JsonNode, name = ""): T = +# fromJson(json, name, result) + + +# func fromJson*(T: type, json: JsonNode, name = ""): T = +# fromJson(json, name, result) \ No newline at end of file diff --git a/ethers/providers/jsonrpc/json.nim b/ethers/providers/jsonrpc/json.nim new file mode 100644 index 0000000..5160442 --- /dev/null +++ b/ethers/providers/jsonrpc/json.nim @@ -0,0 +1,403 @@ + +import std/json except `%`, `%*` +import std/macros +import std/options +import std/strutils +# import std/strformat +import std/tables +import std/typetraits +import pkg/chronicles +import pkg/contractabi +import pkg/stew/byteutils +import pkg/stint +import pkg/questionable/results + +import ../../basics + +export json except `%`, `%*` + +{.push raises: [].} + +logScope: + topics = "json serialization" + +type + SerializationError = object of EthersError + UnexpectedKindError = object of SerializationError + +template serialize* {.pragma.} + +proc mapErrTo[T, E1: CatchableError, E2: CatchableError](r: Result[T, E1], _: type E2): ?!T = + r.mapErr(proc (e: E1): E2 = E2(msg: e.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 fromJson*( + T: type enum, + json: JsonNode +): ?!T = + expectJsonKind(string, JString, json) + catch parseEnum[T](json.str) + +proc fromJson*( + _: type string, + json: JsonNode +): ?!string = + if json.isNil: + let err = newException(ValueError, "'json' expected, but was nil") + return failure(err) + 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: + let x = parseBiggestUInt(json.str) + 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 + +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 + + # Leave this in, it's good for debugging: + # trace "deserializing object", to = $T, json + for name, value in fieldPairs(when type(T) is ref: res[] else: res): + + if jsonVal =? json{name}.catch and not jsonVal.isNil: + without parsed =? type(value).fromJson(jsonVal), e: + error "error deserializing field", + field = $T & "." & name, + json = jsonVal, + error = e.msg + return failure(e) + value = parsed + success(res) + +proc parse*(json: string): ?!JsonNode = + try: + echo "[json.parse] json: ", json + return parseJson(json).catch + except Exception as e: + echo "[json.parse] exception: ", e.msg + 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) + + +# import std/streams +# import std/parsejson + +# type StringStreamFixed = ref object of StringStream +# closeImplFixed: proc (s: StringStreamFixed) +# {.nimcall, raises: [IOError, OSError], tags: [WriteIOEffect], gcsafe.} + +# proc closeImpl*: proc (s: StringStreamFixed) +# {.nimcall, raises: [IOError, OSError], tags: [WriteIOEffect], gcsafe.} = discard + +# proc ssCloseFixed(s: StringStreamFixed) = +# # var s = StringStream(s) +# s.data = "" + +# proc close*(s: StringStreamFixed) {.raises: [IOError, OSError].} = +# ## Closes the stream `s`. +# ## +# ## See also: +# ## * `flush proc <#flush,Stream>`_ +# # runnableExamples: +# # var strm = newStringStream("The first line\nthe second line\nthe third line") +# # ## do something... +# # strm.close() +# if not isNil(s.closeImplFixed): s.closeImplFixed(s) + +# proc newStringStreamFixed*(s: sink string = ""): owned StringStreamFixed {.raises:[].} = +# var ss = StringStreamFixed(newStringStream(s)) +# ss.closeImplFixed = ssCloseFixed +# ss + + +# proc parseJson*(buffer: string; rawIntegers = false, rawFloats = false): JsonNode {.raises: [IOError, OSError, JsonParsingError, ValueError].} = +# ## Parses JSON from `buffer`. +# ## If `buffer` contains extra data, it will raise `JsonParsingError`. +# ## If `rawIntegers` is true, integer literals will not be converted to a `JInt` +# ## field but kept as raw numbers via `JString`. +# ## If `rawFloats` is true, floating point literals will not be converted to a `JFloat` +# ## field but kept as raw numbers via `JString`. +# result = parseJson(newStringStreamFixed(buffer), "input", rawIntegers, rawFloats) + + +proc fromJson*( + _: type JsonNode, + json: string +): ?!JsonNode = + echo "[JsonNode.fromJson] json: ", json + return parse(json) + +proc fromJson*[T: ref object or object]( + _: type T, + json: string +): ?!T = + let json = ? parse(json) + T.fromJson(json) + +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](obj: T): JsonNode = + let jsonObj = newJObject() + for name, value in obj.fieldPairs: + when value.hasCustomPragma(serialize): + jsonObj[name] = %value + jsonObj + +func `%`*[T: ref object](obj: T): JsonNode = + let jsonObj = newJObject() + for name, value in obj[].fieldPairs: + when value.hasCustomPragma(serialize): + 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)