From 819b6fed37900ce8e3d81f90aacd1542245b9d3d Mon Sep 17 00:00:00 2001 From: coffeepots Date: Thu, 14 Dec 2023 01:29:11 +0000 Subject: [PATCH] Use nim-json-serialization for RPCs (#172) * Use nim-json-serialization for RPCs * Add distinct serializers for RPC types * Avoid ConvFromXtoItselfNotNeeded in testing * Remove redundent `==` template * Rename Eth1JsonRpc to JsonRpc * Fix generic instantiation clash --------- Co-authored-by: jangko --- json_rpc/clients/websocketclient.nim | 122 +---------- json_rpc/clients/websocketclientimpl.nim | 119 +++++++++++ json_rpc/jsonmarshal.nim | 251 +++++++++-------------- json_rpc/router.nim | 32 ++- tests/ethhexstrings.nim | 40 ++-- tests/helpers.nim | 3 +- tests/stintjson.nim | 40 ++-- tests/testrpcmacro.nim | 52 ++--- 8 files changed, 311 insertions(+), 348 deletions(-) create mode 100644 json_rpc/clients/websocketclientimpl.nim diff --git a/json_rpc/clients/websocketclient.nim b/json_rpc/clients/websocketclient.nim index f2365df..c5bfe54 100644 --- a/json_rpc/clients/websocketclient.nim +++ b/json_rpc/clients/websocketclient.nim @@ -1,120 +1,10 @@ import - pkg/[chronos, chronos/apps/http/httptable, chronicles], - stew/byteutils, + ./websocketclientimpl, ../client -export client +# this weird arrangement is to avoid clash +# between Json.encode and Base64Pad.encode -{.push raises: [Defect].} - -logScope: - topics = "JSONRPC-WS-CLIENT" - -import std/[uri, strutils] -import pkg/websock/[websock, extensions/compression/deflate] - -type - RpcWebSocketClient* = ref object of RpcClient - transport*: WSSession - uri*: Uri - loop*: Future[void] - getHeaders*: GetJsonRpcRequestHeaders - -proc new*( - T: type RpcWebSocketClient, getHeaders: GetJsonRpcRequestHeaders = nil): T = - T(getHeaders: getHeaders) - -proc newRpcWebSocketClient*( - getHeaders: GetJsonRpcRequestHeaders = nil): RpcWebSocketClient = - ## Creates a new client instance. - RpcWebSocketClient.new(getHeaders) - -method call*(self: RpcWebSocketClient, name: string, - params: JsonNode): Future[Response] {.async, gcsafe.} = - ## Remotely calls the specified RPC method. - let id = self.getNextId() - var value = $rpcCallNode(name, params, id) & "\r\n" - if self.transport.isNil: - raise newException(ValueError, - "Transport is not initialised (missing a call to connect?)") - - # completed by processMessage. - var newFut = newFuture[Response]() - # add to awaiting responses - self.awaiting[id] = newFut - - await self.transport.send(value) - return await newFut - -proc processData(client: RpcWebSocketClient) {.async.} = - var error: ref CatchableError - - let ws = client.transport - try: - while ws.readyState != ReadyState.Closed: - var value = await ws.recvMsg() - - if value.len == 0: - # transmission ends - break - - client.processMessage(string.fromBytes(value)) - except CatchableError as e: - error = e - - await client.transport.close() - - client.transport = nil - - if client.awaiting.len != 0: - if error.isNil: - error = newException(IOError, "Transport was closed while waiting for response") - for k, v in client.awaiting: - v.fail(error) - client.awaiting.clear() - if not client.onDisconnect.isNil: - client.onDisconnect() - -proc addExtraHeaders( - headers: var HttpTable, - client: RpcWebSocketClient, - extraHeaders: HttpTable) = - # Apply client instance overrides - if client.getHeaders != nil: - for header in client.getHeaders(): - headers.add(header[0], header[1]) - - # Apply call specific overrides - for header in extraHeaders.stringItems: - headers.add(header.key, header.value) - - # Apply default origin - discard headers.hasKeyOrPut("Origin", "http://localhost") - -proc connect*( - client: RpcWebSocketClient, - uri: string, - extraHeaders: HttpTable = default(HttpTable), - compression = false, - hooks: seq[Hook] = @[], - flags: set[TLSFlags] = {}) {.async.} = - proc headersHook(ctx: Hook, headers: var HttpTable): Result[void, string] = - headers.addExtraHeaders(client, extraHeaders) - ok() - var ext: seq[ExtFactory] = if compression: @[deflateFactory()] - else: @[] - let uri = parseUri(uri) - let ws = await WebSocket.connect( - uri=uri, - factories=ext, - hooks=hooks & Hook(append: headersHook), - flags=flags) - client.transport = ws - client.uri = uri - client.loop = processData(client) - -method close*(client: RpcWebSocketClient) {.async.} = - await client.loop.cancelAndWait() - if not client.transport.isNil: - await client.transport.close() - client.transport = nil +export + websocketclientimpl, + client diff --git a/json_rpc/clients/websocketclientimpl.nim b/json_rpc/clients/websocketclientimpl.nim new file mode 100644 index 0000000..568b934 --- /dev/null +++ b/json_rpc/clients/websocketclientimpl.nim @@ -0,0 +1,119 @@ +import + std/[uri, strutils], + pkg/websock/[websock, extensions/compression/deflate], + pkg/[chronos, chronos/apps/http/httptable, chronicles], + stew/byteutils + +# avoid clash between Json.encode and Base64Pad.encode +import ../client except encode + +{.push raises: [Defect].} + +logScope: + topics = "JSONRPC-WS-CLIENT" + +type + RpcWebSocketClient* = ref object of RpcClient + transport*: WSSession + uri*: Uri + loop*: Future[void] + getHeaders*: GetJsonRpcRequestHeaders + +proc new*( + T: type RpcWebSocketClient, getHeaders: GetJsonRpcRequestHeaders = nil): T = + T(getHeaders: getHeaders) + +proc newRpcWebSocketClient*( + getHeaders: GetJsonRpcRequestHeaders = nil): RpcWebSocketClient = + ## Creates a new client instance. + RpcWebSocketClient.new(getHeaders) + +method call*(self: RpcWebSocketClient, name: string, + params: JsonNode): Future[Response] {.async, gcsafe.} = + ## Remotely calls the specified RPC method. + let id = self.getNextId() + var value = $rpcCallNode(name, params, id) & "\r\n" + if self.transport.isNil: + raise newException(ValueError, + "Transport is not initialised (missing a call to connect?)") + + # completed by processMessage. + var newFut = newFuture[Response]() + # add to awaiting responses + self.awaiting[id] = newFut + + await self.transport.send(value) + return await newFut + +proc processData(client: RpcWebSocketClient) {.async.} = + var error: ref CatchableError + + let ws = client.transport + try: + while ws.readyState != ReadyState.Closed: + var value = await ws.recvMsg() + + if value.len == 0: + # transmission ends + break + + client.processMessage(string.fromBytes(value)) + except CatchableError as e: + error = e + + await client.transport.close() + + client.transport = nil + + if client.awaiting.len != 0: + if error.isNil: + error = newException(IOError, "Transport was closed while waiting for response") + for k, v in client.awaiting: + v.fail(error) + client.awaiting.clear() + if not client.onDisconnect.isNil: + client.onDisconnect() + +proc addExtraHeaders( + headers: var HttpTable, + client: RpcWebSocketClient, + extraHeaders: HttpTable) = + # Apply client instance overrides + if client.getHeaders != nil: + for header in client.getHeaders(): + headers.add(header[0], header[1]) + + # Apply call specific overrides + for header in extraHeaders.stringItems: + headers.add(header.key, header.value) + + # Apply default origin + discard headers.hasKeyOrPut("Origin", "http://localhost") + +proc connect*( + client: RpcWebSocketClient, + uri: string, + extraHeaders: HttpTable = default(HttpTable), + compression = false, + hooks: seq[Hook] = @[], + flags: set[TLSFlags] = {}) {.async.} = + proc headersHook(ctx: Hook, headers: var HttpTable): Result[void, string] = + headers.addExtraHeaders(client, extraHeaders) + ok() + var ext: seq[ExtFactory] = if compression: @[deflateFactory()] + else: @[] + let uri = parseUri(uri) + let ws = await WebSocket.connect( + uri=uri, + factories=ext, + hooks=hooks & Hook(append: headersHook), + flags=flags) + client.transport = ws + client.uri = uri + client.loop = processData(client) + +method close*(client: RpcWebSocketClient) {.async.} = + await client.loop.cancelAndWait() + if not client.transport.isNil: + await client.transport.close() + client.transport = nil diff --git a/json_rpc/jsonmarshal.nim b/json_rpc/jsonmarshal.nim index 82033df..3685850 100644 --- a/json_rpc/jsonmarshal.nim +++ b/json_rpc/jsonmarshal.nim @@ -1,161 +1,110 @@ import - std/[macros, json, options, typetraits], - stew/[byteutils, objects] + std/[macros, json, typetraits], + stew/[byteutils, objects], + json_serialization, + json_serialization/lexer, + json_serialization/std/[options, sets, tables] -export json, options +export json, options, json_serialization + +Json.createFlavor JsonRpc + +# Avoid templates duplicating the string in the executable. +const errDeserializePrefix = "Error deserializing stream for type '" + +template wrapErrors(reader, value, actions: untyped): untyped = + ## Convert read errors to `UnexpectedValue` for the purpose of marshalling. + try: + actions + except Exception as err: + reader.raiseUnexpectedValue(errDeserializePrefix & $type(value) & "': " & err.msg) + +# Bytes. + +proc readValue*(r: var JsonReader[JsonRpc], value: var byte) = + ## Implement separate read serialization for `byte` to avoid + ## 'can raise Exception' for `readValue(value, uint8)`. + wrapErrors r, value: + case r.lexer.tok + of tkInt: + if r.lexer.absIntVal in 0'u32 .. byte.high: + value = byte(r.lexer.absIntVal) + else: + r.raiseIntOverflow r.lexer.absIntVal, true + of tkNegativeInt: + r.raiseIntOverflow r.lexer.absIntVal, true + else: + r.raiseUnexpectedToken etInt + r.lexer.next() + +proc writeValue*(w: var JsonWriter[JsonRpc], value: byte) = + json_serialization.writeValue(w, uint8(value)) + +# Enums. + +proc readValue*(r: var JsonReader[JsonRpc], value: var (enum)) = + wrapErrors r, value: + value = type(value) json_serialization.readValue(r, uint64) + +proc writeValue*(w: var JsonWriter[JsonRpc], value: (enum)) = + json_serialization.writeValue(w, uint64(value)) + +# Other base types. + +macro genDistinctSerializers(types: varargs[untyped]): untyped = + ## Implements distinct serialization pass-throughs for `types`. + result = newStmtList() + for ty in types: + result.add(quote do: + + proc readValue*(r: var JsonReader[JsonRpc], value: var `ty`) = + wrapErrors r, value: + json_serialization.readValue(r, value) + + proc writeValue*(w: var JsonWriter[JsonRpc], value: `ty`) {.raises: [IOError].} = + json_serialization.writeValue(w, value) + ) + +genDistinctSerializers bool, int, float, string, int64, uint64, uint32, ref int64, ref int + +# Sequences and arrays. + +proc readValue*[T](r: var JsonReader[JsonRpc], value: var seq[T]) = + wrapErrors r, value: + json_serialization.readValue(r, value) + +proc writeValue*[T](w: var JsonWriter[JsonRpc], value: seq[T]) = + json_serialization.writeValue(w, value) + +proc readValue*[N: static[int]](r: var JsonReader[JsonRpc], value: var array[N, byte]) = + ## Read an array while allowing partial data. + wrapErrors r, value: + r.skipToken tkBracketLe + if r.lexer.tok != tkBracketRi: + for i in low(value) .. high(value): + readValue(r, value[i]) + if r.lexer.tok == tkBracketRi: + break + else: + r.skipToken tkComma + r.skipToken tkBracketRi + +# High level generic unpacking. + +proc unpackArg[T](args: JsonNode, argName: string, argType: typedesc[T]): T {.raises: [ValueError].} = + if args.isNil: + raise newException(ValueError, argName & ": unexpected null value") + try: + result = JsonRpc.decode($args, argType) + except CatchableError as err: + raise newException(ValueError, + "Parameter [" & argName & "] of type '" & $argType & "' could not be decoded: " & err.msg) proc expect*(actual, expected: JsonNodeKind, argName: string) = if actual != expected: raise newException( ValueError, "Parameter [" & argName & "] expected " & $expected & " but got " & $actual) -proc `%`*(n: ref SomeInteger): JsonNode = - if n.isNil: - newJNull() - else: - newJInt(n[]) - -# Compiler requires forward decl when processing out of module -proc fromJson*(n: JsonNode, argName: string, result: var bool) -proc fromJson*(n: JsonNode, argName: string, result: var int) -proc fromJson*(n: JsonNode, argName: string, result: var byte) -proc fromJson*(n: JsonNode, argName: string, result: var float) -proc fromJson*(n: JsonNode, argName: string, result: var string) -proc fromJson*[T](n: JsonNode, argName: string, result: var seq[T]) -proc fromJson*[N, T](n: JsonNode, argName: string, result: var array[N, T]) -proc fromJson*(n: JsonNode, argName: string, result: var int64) -proc fromJson*(n: JsonNode, argName: string, result: var uint64) -proc fromJson*(n: JsonNode, argName: string, result: var uint32) -proc fromJson*(n: JsonNode, argName: string, result: var ref int64) -proc fromJson*(n: JsonNode, argName: string, result: var ref int) -proc fromJson*[T](n: JsonNode, argName: string, result: var Option[T]) - -# This can't be forward declared: https://github.com/nim-lang/Nim/issues/7868 -proc fromJson*[T: enum](n: JsonNode, argName: string, result: var T) = - n.kind.expect(JInt, argName) - - let v = n.getBiggestInt() - if not checkedEnumAssign(result, v): - raise (ref ValueError)( - msg: "Unknown enum ordinal for " & name(T) & ": " & $v) - -# This can't be forward declared: https://github.com/nim-lang/Nim/issues/7868 -proc fromJson*[T: object|tuple](n: JsonNode, argName: string, result: var T) = - n.kind.expect(JObject, argName) - for k, v in fieldPairs(result): - if v is Option and not n.hasKey(k): - fromJson(newJNull(), k, v) - else: - fromJson(n[k], k, v) - -# same as `proc `%`*[T: object](o: T): JsonNode` in json.nim from the stdlib -# TODO this PR removes the need for this: https://github.com/nim-lang/Nim/pull/14638 -proc `%`*[T: tuple](o: T): JsonNode = - ## Construct JsonNode from tuples and objects. - result = newJObject() - for k, v in o.fieldPairs: result[k] = %v - -proc fromJson*[T](n: JsonNode, argName: string, result: var Option[T]) = - # Allow JNull for options - if n.kind != JNull: - var val: T - fromJson(n, argName, val) - result = some(val) - -proc fromJson*(n: JsonNode, argName: string, result: var bool) = - n.kind.expect(JBool, argName) - result = n.getBool() - -proc fromJson*(n: JsonNode, argName: string, result: var int) = - n.kind.expect(JInt, argName) - result = n.getInt() - -proc fromJson*[T: ref object](n: JsonNode, argName: string, result: var T) = - if n.kind == JNull: - result = nil - return - n.kind.expect(JObject, argName) - result = new T - fromJson(n, argName, result[]) - -proc fromJson*(n: JsonNode, argName: string, result: var int64) = - n.kind.expect(JInt, argName) - result = n.getBiggestInt().int64 - -proc fromJson*(n: JsonNode, argName: string, result: var uint64) = - n.kind.expect(JInt, argName) - let asInt = n.getBiggestInt() - # signed -> unsigned conversions are unchecked - # https://github.com/nim-lang/RFCs/issues/175 - if asInt < 0: - raise newException( - ValueError, "JSON-RPC input is an unexpected negative value") - result = uint64(asInt) - -proc fromJson*(n: JsonNode, argName: string, result: var uint32) = - n.kind.expect(JInt, argName) - let asInt = n.getBiggestInt() - # signed -> unsigned conversions are unchecked - # https://github.com/nim-lang/RFCs/issues/175 - if asInt < 0: - raise newException( - ValueError, "JSON-RPC input is an unexpected negative value") - if asInt > BiggestInt(uint32.high()): - raise newException( - ValueError, "JSON-RPC input is too large for uint32") - - result = uint32(asInt) - -proc fromJson*(n: JsonNode, argName: string, result: var ref int64) = - n.kind.expect(JInt, argName) - new result - result[] = n.getInt() - -proc fromJson*(n: JsonNode, argName: string, result: var ref int) = - n.kind.expect(JInt, argName) - new result - result[] = n.getInt() - -proc fromJson*(n: JsonNode, argName: string, result: var byte) = - n.kind.expect(JInt, argName) - let v = n.getInt() - if v > 255 or v < 0: - raise newException(ValueError, "Parameter \"" & argName & "\" value out of range for byte: " & $v) - result = byte(v) - -proc fromJson*(n: JsonNode, argName: string, result: var float) = - n.kind.expect(JFloat, argName) - result = n.getFloat() - -proc fromJson*(n: JsonNode, argName: string, result: var string) = - n.kind.expect(JString, argName) - result = n.getStr() - -proc fromJson*[T](n: JsonNode, argName: string, result: var seq[T]) = - when T is byte: - if n.kind == JString: - result = hexToSeqByte n.getStr() - return - - n.kind.expect(JArray, argName) - result = newSeq[T](n.len) - for i in 0 ..< n.len: - fromJson(n[i], argName, result[i]) - -proc fromJson*[N, T](n: JsonNode, argName: string, result: var array[N, T]) = - n.kind.expect(JArray, argName) - if n.len > result.len: - raise newException(ValueError, "Parameter \"" & argName & "\" item count is too big for array") - for i in 0 ..< n.len: - fromJson(n[i], argName, result[i]) - -proc unpackArg[T](args: JsonNode, argName: string, argtype: typedesc[T]): T = - mixin fromJson - if args.isNil: - raise (ref ValueError)(msg: argName & ": unexpected null value") - {.gcsafe.}: - fromJson(args, argName, result) - proc expectArrayLen(node, jsonIdent: NimNode, length: int) = let identStr = jsonIdent.repr @@ -218,7 +167,7 @@ proc jsonToNim*(assignIdent, paramType, jsonIdent: NimNode, paramNameStr: string `unpackArg`(`jsonIdent`, `paramNameStr`, type(`paramType`)) if optional: - result.add(quote do: `assignIdent` = `some`(`unpackNode`)) + result.add(quote do: `assignIdent` = some(`unpackNode`)) else: result.add(quote do: `assignIdent` = `unpackNode`) diff --git a/json_rpc/router.nim b/json_rpc/router.nim index da9add4..42fab15 100644 --- a/json_rpc/router.nim +++ b/json_rpc/router.nim @@ -1,5 +1,5 @@ import - std/[macros, options, strutils, tables], + std/[macros, strutils, tables], chronicles, chronos, json_serialization/writer, ./jsonmarshal, ./errors @@ -122,11 +122,6 @@ proc tryRoute*(router: RpcRouter, data: JsonNode, fut: var Future[StringOfJson]) fut = rpc(jParams) return true -proc makeProcName(s: string): string = - result = "" - for c in s: - if c.isAlphaNumeric: result.add c - proc hasReturnType(params: NimNode): bool = if params != nil and params.len > 0 and params[0] != nil and params[0].kind != nnkEmpty: @@ -162,21 +157,20 @@ macro rpc*(server: RpcRouter, path: string, body: untyped): untyped = `setup` `procBody` - if ReturnType == ident"JsonNode": - # `JsonNode` results don't need conversion - result.add quote do: - proc `rpcProcWrapper`(`paramsIdent`: JsonNode): Future[StringOfJson] {.async, gcsafe.} = - return StringOfJson($(await `rpcProcImpl`(`paramsIdent`))) - elif ReturnType == ident"StringOfJson": - result.add quote do: - proc `rpcProcWrapper`(`paramsIdent`: JsonNode): Future[StringOfJson] {.async, gcsafe.} = - return await `rpcProcImpl`(`paramsIdent`) - else: - result.add quote do: - proc `rpcProcWrapper`(`paramsIdent`: JsonNode): Future[StringOfJson] {.async, gcsafe.} = - return StringOfJson($(%(await `rpcProcImpl`(`paramsIdent`)))) + let + awaitedResult = ident "awaitedResult" + doEncode = quote do: encode(JsonRpc, `awaitedResult`) + maybeWrap = + if ReturnType == ident"StringOfJson": doEncode + else: ident"StringOfJson".newCall doEncode result.add quote do: + proc `rpcProcWrapper`(`paramsIdent`: JsonNode): Future[StringOfJson] {.async, gcsafe.} = + # Avoid 'yield in expr not lowered' with an intermediate variable. + # See: https://github.com/nim-lang/Nim/issues/17849 + let `awaitedResult` = await `rpcProcImpl`(`paramsIdent`) + return `maybeWrap` + `server`.register(`path`, `rpcProcWrapper`) when defined(nimDumpRpcs): diff --git a/tests/ethhexstrings.nim b/tests/ethhexstrings.nim index 4ad2dfa..9c6c82c 100644 --- a/tests/ethhexstrings.nim +++ b/tests/ethhexstrings.nim @@ -61,7 +61,7 @@ template hexQuantityStr*(value: string): HexQuantityStr = value.HexQuantityStr # Converters import json -from ../json_rpc/rpcserver import expect +import ../json_rpc/jsonmarshal proc `%`*(value: HexDataStr): JsonNode = if not value.validate: @@ -75,21 +75,31 @@ proc `%`*(value: HexQuantityStr): JsonNode = else: result = %(value.string) -proc fromJson*(n: JsonNode, argName: string, result: var HexDataStr) = - # Note that '0x' is stripped after validation - n.kind.expect(JString, argName) - let hexStr = n.getStr() - if not hexStr.hexDataStr.validate: - raise newException(ValueError, "Parameter \"" & argName & "\" value is not valid as a Ethereum data \"" & hexStr & "\"") - result = hexStr[2..hexStr.high].hexDataStr +proc writeValue*(w: var JsonWriter[JsonRpc], val: HexDataStr) {.raises: [IOError].} = + writeValue(w, val.string) -proc fromJson*(n: JsonNode, argName: string, result: var HexQuantityStr) = +proc writeValue*(w: var JsonWriter[JsonRpc], val: HexQuantityStr) {.raises: [IOError].} = + writeValue(w, $val.string) + +proc readValue*(r: var JsonReader[JsonRpc], v: var HexDataStr) = # Note that '0x' is stripped after validation - n.kind.expect(JString, argName) - let hexStr = n.getStr() - if not hexStr.hexQuantityStr.validate: - raise newException(ValueError, "Parameter \"" & argName & "\" value is not valid as an Ethereum hex quantity \"" & hexStr & "\"") - result = hexStr[2..hexStr.high].hexQuantityStr + try: + let hexStr = readValue(r, string) + if not hexStr.hexDataStr.validate: + raise newException(ValueError, "Value for '" & $v.type & "' is not valid as a Ethereum data \"" & hexStr & "\"") + v = hexStr[2..hexStr.high].hexDataStr + except Exception as err: + r.raiseUnexpectedValue("Error deserializing for '" & $v.type & "' stream: " & err.msg) + +proc readValue*(r: var JsonReader[JsonRpc], v: var HexQuantityStr) = + # Note that '0x' is stripped after validation + try: + let hexStr = readValue(r, string) + if not hexStr.hexQuantityStr.validate: + raise newException(ValueError, "Value for '" & $v.type & "' is not valid as a Ethereum data \"" & hexStr & "\"") + v = hexStr[2..hexStr.high].hexQuantityStr + except Exception as err: + r.raiseUnexpectedValue("Error deserializing for '" & $v.type & "' stream: " & err.msg) # testing @@ -128,7 +138,7 @@ when isMainModule: source = "x1234" x = hexQuantityStr source check %x != %source - + suite "Hex data": test "Even length": let diff --git a/tests/helpers.nim b/tests/helpers.nim index 3113973..a4c6533 100644 --- a/tests/helpers.nim +++ b/tests/helpers.nim @@ -1,8 +1,7 @@ import ../json_rpc/router -template `==`*(a, b: distinct (string|StringOfJson)): bool = - string(a) == string(b) +converter toStr*(value: distinct (string|StringOfJson)): string = string(value) template `==`*(a: StringOfJson, b: JsonNode): bool = parseJson(string a) == b diff --git a/tests/stintjson.nim b/tests/stintjson.nim index 5edad06..28e77f6 100644 --- a/tests/stintjson.nim +++ b/tests/stintjson.nim @@ -1,5 +1,4 @@ -import json, stint -from ../json_rpc/rpcserver import expect +import stint, ../json_rpc/jsonmarshal template stintStr(n: UInt256|Int256): JsonNode = var s = n.toHex @@ -11,22 +10,25 @@ proc `%`*(n: UInt256): JsonNode = n.stintStr proc `%`*(n: Int256): JsonNode = n.stintStr -# allows UInt256 to be passed as a json string -proc fromJson*(n: JsonNode, argName: string, result: var UInt256) = - # expects base 16 string, starting with "0x" - n.kind.expect(JString, argName) - let hexStr = n.getStr() - if hexStr.len > 64 + 2: # including "0x" - raise newException(ValueError, "Parameter \"" & argName & "\" value too long for UInt256: " & $hexStr.len) - result = hexStr.parse(StUint[256], 16) # TODO: Handle errors +proc writeValue*(w: var JsonWriter[JsonRpc], val: UInt256) = + writeValue(w, val.stintStr) -# allows ref UInt256 to be passed as a json string -proc fromJson*(n: JsonNode, argName: string, result: var ref UInt256) = - # expects base 16 string, starting with "0x" - n.kind.expect(JString, argName) - let hexStr = n.getStr() - if hexStr.len > 64 + 2: # including "0x" - raise newException(ValueError, "Parameter \"" & argName & "\" value too long for UInt256: " & $hexStr.len) - new result - result[] = hexStr.parse(StUint[256], 16) # TODO: Handle errors +proc writeValue*(w: var JsonWriter[JsonRpc], val: ref UInt256) = + writeValue(w, val[].stintStr) + +proc readValue*(r: var JsonReader[JsonRpc], v: var UInt256) = + ## Allows UInt256 to be passed as a json string. + ## Expects base 16 string, starting with "0x". + try: + let hexStr = r.readValue string + if hexStr.len > 64 + 2: # including "0x" + raise newException(ValueError, "Value for '" & $v.type & "' too long for UInt256: " & $hexStr.len) + v = hexStr.parse(StUint[256], 16) # TODO: Handle errors + except Exception as err: + r.raiseUnexpectedValue("Error deserializing for '" & $v.type & "' stream: " & err.msg) + +proc readValue*(r: var JsonReader[JsonRpc], v: var ref UInt256) = + ## Allows ref UInt256 to be passed as a json string. + ## Expects base 16 string, starting with "0x". + readValue(r, v[]) diff --git a/tests/testrpcmacro.nim b/tests/testrpcmacro.nim index b3a45c8..c320c6a 100644 --- a/tests/testrpcmacro.nim +++ b/tests/testrpcmacro.nim @@ -36,7 +36,7 @@ let "y": %"test" } }, - "c": %1.23} + "c": %1.0} var s = newRpcSocketServer(["localhost:8545"]) @@ -201,12 +201,12 @@ suite "Server types": inp2 = MyOptional() r1 = waitFor s.executeMethod("rpc.optional", %[%inp1]) r2 = waitFor s.executeMethod("rpc.optional", %[%inp2]) - check r1 == %inp1 - check r2 == %inp2 + check r1 == JsonRpc.encode inp1 + check r2 == JsonRpc.encode inp2 test "Return statement": let r = waitFor s.executeMethod("rpc.testReturns", %[]) - check r == %1234 + check r == JsonRpc.encode 1234 test "Runtime errors": expect ValueError: @@ -224,7 +224,7 @@ suite "Server types": test "Multiple variables of one type": let r = waitFor s.executeMethod("rpc.multiVarsOfOneType", %[%"hello", %"world"]) - check r == %"hello world" + check r == JsonRpc.encode "hello world" test "Optional arg": let @@ -233,37 +233,37 @@ suite "Server types": r1 = waitFor s.executeMethod("rpc.optionalArg", %[%117, %int1]) r2 = waitFor s.executeMethod("rpc.optionalArg", %[%117]) r3 = waitFor s.executeMethod("rpc.optionalArg", %[%117, newJNull()]) - check r1 == %int1 - check r2 == %int2 - check r3 == %int2 + check r1 == JsonRpc.encode int1 + check r2 == JsonRpc.encode int2 + check r3 == JsonRpc.encode int2 test "Optional arg2": let r1 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B"]) - check r1 == %"AB" + check r1 == JsonRpc.encode "AB" let r2 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B", newJNull()]) - check r2 == %"AB" + check r2 == JsonRpc.encode "AB" let r3 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B", newJNull(), newJNull()]) - check r3 == %"AB" + check r3 == JsonRpc.encode "AB" let r4 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B", newJNull(), %"D"]) - check r4 == %"ABD" + check r4 == JsonRpc.encode "ABD" let r5 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B", %"C", %"D"]) - check r5 == %"ABCD" + check r5 == JsonRpc.encode "ABCD" let r6 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B", %"C", newJNull()]) - check r6 == %"ABC" + check r6 == JsonRpc.encode "ABC" let r7 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B", %"C"]) - check r7 == %"ABC" + check r7 == JsonRpc.encode "ABC" test "Mixed optional arg": var ax = waitFor s.executeMethod("rpc.mixedOptionalArg", %[%10, %11, %"hello", %12, %"world"]) - check ax == %OptionalFields(a: 10, b: some(11), c: "hello", d: some(12), e: some("world")) + check ax == JsonRpc.encode OptionalFields(a: 10, b: some(11), c: "hello", d: some(12), e: some("world")) var bx = waitFor s.executeMethod("rpc.mixedOptionalArg", %[%10, newJNull(), %"hello"]) - check bx == %OptionalFields(a: 10, c: "hello") + check bx == JsonRpc.encode OptionalFields(a: 10, c: "hello") test "Non-built-in optional types": let @@ -271,33 +271,33 @@ suite "Server types": testOpts1 = MyOptionalNotBuiltin(val: some(t2)) testOpts2 = MyOptionalNotBuiltin() var r = waitFor s.executeMethod("rpc.optionalArgNotBuiltin", %[%testOpts1]) - check r == %t2.y + check r == JsonRpc.encode t2.y var r2 = waitFor s.executeMethod("rpc.optionalArgNotBuiltin", %[]) - check r2 == %"Empty1" + check r2 == JsonRpc.encode "Empty1" var r3 = waitFor s.executeMethod("rpc.optionalArgNotBuiltin", %[%testOpts2]) - check r3 == %"Empty2" + check r3 == JsonRpc.encode "Empty2" test "Manually set up JSON for optionals": # Check manual set up json with optionals let opts1 = parseJson("""{"o1": true}""") var r1 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts1]) - check r1 == %1 + check r1 == JsonRpc.encode 1 let opts2 = parseJson("""{"o2": true}""") var r2 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts2]) - check r2 == %2 + check r2 == JsonRpc.encode 2 let opts3 = parseJson("""{"o3": true}""") var r3 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts3]) - check r3 == %4 + check r3 == JsonRpc.encode 4 # Combinations let opts4 = parseJson("""{"o1": true, "o3": true}""") var r4 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts4]) - check r4 == %5 + check r4 == JsonRpc.encode 5 let opts5 = parseJson("""{"o2": true, "o3": true}""") var r5 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts5]) - check r5 == %6 + check r5 == JsonRpc.encode 6 let opts6 = parseJson("""{"o1": true, "o2": true}""") var r6 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts6]) - check r6 == %3 + check r6 == JsonRpc.encode 3 s.stop() waitFor s.closeWait()