mirror of
https://github.com/logos-storage/nim-json-rpc.git
synced 2026-01-05 15:13:11 +00:00
Upgrade rpc router internals (#178)
* Upgrade rpc router internals * use new chronos asyncraises * Fix style mismatch * Fix nim v2 compilation error * Addresing review * Remove unnecessary custom serializer and let the library do the work * fix error message * Update readme.md
This commit is contained in:
parent
c3769f9130
commit
e0b077fea4
103
README.md
103
README.md
@ -49,7 +49,7 @@ router.rpc("hello") do() -> string:
|
||||
```
|
||||
|
||||
As no return type was specified in this example, `result` defaults to the `JsonNode` type.
|
||||
A JSON string is returned by passing a string though the `%` operator, which converts simple types to `JsonNode`.
|
||||
A JSON string is returned by passing a string though the `JrpcConv` converter powered by [nim-json-serialization](https://github.com/status-im/nim-json-serialization).
|
||||
|
||||
The `body` parameters can be defined by using [do notation](https://nim-lang.org/docs/manual.html#procedures-do-notation).
|
||||
This allows full Nim types to be used as RPC parameters.
|
||||
@ -89,7 +89,7 @@ router.rpc("updateData") do(myObj: MyObject, newData: DataBlob) -> DataBlob:
|
||||
myObj.data = newData
|
||||
```
|
||||
|
||||
Behind the scenes, all RPC calls take a single json parameter `param` that must be of kind `JArray`.
|
||||
Behind the scenes, all RPC calls take parameters through `RequestParamsRx` structure.
|
||||
At runtime, the json is checked to ensure that it contains the correct number and type of your parameters to match the `rpc` definition.
|
||||
|
||||
Compiling with `-d:nimDumpRpcs` will show the output code for the RPC call. To see the output of the `async` generation, add `-d:nimDumpAsync`.
|
||||
@ -129,83 +129,71 @@ type
|
||||
|
||||
Note that `array` parameters are explicitly checked for length, and will return an error node if the length differs from their declaration size.
|
||||
|
||||
If you wish to support custom types in a particular way, you can provide matching `fromJson` and `%` procedures.
|
||||
If you wish to support custom types in a particular way, you can provide matching `readValue` and `writeValue` procedures.
|
||||
The custom serializer you write must be using `JrpcConv` flavor.
|
||||
|
||||
### `fromJson`
|
||||
### `readValue`
|
||||
|
||||
This takes a Json type and returns the Nim type.
|
||||
|
||||
#### Parameters
|
||||
|
||||
`n: JsonNode`: The current node being processed
|
||||
`r: var JsonReader[JrpcConv]`: The current JsonReader with JrpcConv flavor.
|
||||
|
||||
`argName: string`: The name of the field in `n`
|
||||
|
||||
`result`: The type of this must be `var X` where `X` is the Nim type you wish to handle
|
||||
`val: var MyInt`: Deserialized value.
|
||||
|
||||
#### Example
|
||||
|
||||
```nim
|
||||
proc fromJson[T](n: JsonNode, argName: string, result: var seq[T]) =
|
||||
n.kind.expect(JArray, argName)
|
||||
result = newSeq[T](n.len)
|
||||
for i in 0 ..< n.len:
|
||||
fromJson(n[i], argName, result[i])
|
||||
proc readValue*(r: var JsonReader[JrpcConv], val: var MyInt)
|
||||
{.gcsafe, raises: [IOError, JsonReaderError].} =
|
||||
let intVal = r.parseInt(int)
|
||||
val = MyInt(intVal)
|
||||
```
|
||||
|
||||
### `%`
|
||||
### `writeValue`
|
||||
|
||||
This is the standard way to provide translations from a Nim type to a `JsonNode`.
|
||||
This is the standard way to provide translations from a Nim type to Json.
|
||||
|
||||
#### Parameters
|
||||
|
||||
`n`: The type you wish to convert
|
||||
`w: var JsonWriter[JrpcConv]`: The current JsonWriter with JrpcConv flavor.
|
||||
|
||||
#### Returns
|
||||
|
||||
`JsonNode`: The newly encoded `JsonNode` type from the parameter type.
|
||||
|
||||
### `expect`
|
||||
|
||||
This is a simple procedure to state your expected type.
|
||||
|
||||
If the actual type doesn't match the expected type, an exception is thrown mentioning which field caused the failure.
|
||||
|
||||
#### Parameters
|
||||
|
||||
`actual: JsonNodeKind`: The actual type of the `JsonNode`.
|
||||
|
||||
`expected: JsonNodeKind`: The desired type.
|
||||
|
||||
`argName: string`: The current field name.
|
||||
`val: MyInt`: The value you want to convert into Json.
|
||||
|
||||
#### Example
|
||||
|
||||
```nim
|
||||
myNode.kind.expect(JArray, argName)
|
||||
proc writeValue*(w: var JsonWriter[JrpcConv], val: MyInt)
|
||||
{.gcsafe, raises: [IOError].} =
|
||||
w.writeValue val.int
|
||||
```
|
||||
|
||||
## JSON Format
|
||||
|
||||
The router expects either a string or `JsonNode` with the following structure:
|
||||
The router expects either a Json document with the following structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": JInt,
|
||||
"id": Int or String,
|
||||
"jsonrpc": "2.0",
|
||||
"method": JString,
|
||||
"params": JArray
|
||||
"method": String,
|
||||
"params": Array or Object
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
If params is an Array, it is a positional parameters. If it is an Object then the rpc method will be called using named parameters.
|
||||
|
||||
|
||||
Return values use the following node structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": JInt,
|
||||
"id": Int Or String,
|
||||
"jsonrpc": "2.0",
|
||||
"result": JsonNode,
|
||||
"error": JsonNode
|
||||
"result": Json document,
|
||||
"error": Json document
|
||||
}
|
||||
```
|
||||
|
||||
@ -215,35 +203,35 @@ To call and RPC through the router, use the `route` procedure.
|
||||
|
||||
There are three variants of `route`.
|
||||
|
||||
Note that once invoked all RPC calls are error trapped and any exceptions raised are passed back with the error message encoded as a `JsonNode`.
|
||||
Note that once invoked all RPC calls are error trapped and any exceptions raised are passed back with the error message encoded as a `Json document`.
|
||||
|
||||
### `route` by string
|
||||
|
||||
This `route` variant will handle all the conversion of `string` to `JsonNode` and check the format and type of the input data.
|
||||
This `route` variant will handle all the conversion of `string` to `Json document` and check the format and type of the input data.
|
||||
|
||||
#### Parameters
|
||||
|
||||
`router: RpcRouter`: The router object that contains the RPCs.
|
||||
|
||||
`data: string`: A string ready to be processed into a `JsonNode`.
|
||||
`data: string`: A string ready to be processed into a `Json document`.
|
||||
|
||||
#### Returns
|
||||
|
||||
`Future[string]`: This will be the stringified JSON response, which can be the JSON RPC result or a JSON wrapped error.
|
||||
|
||||
### `route` by `JsonNode`
|
||||
### `route` by `Json document`
|
||||
|
||||
This variant allows simplified processing if you already have a `JsonNode`. However if the required fields are not present within `node`, exceptions will be raised.
|
||||
This variant allows simplified processing if you already have a `Json document`. However if the required fields are not present within `data`, exceptions will be raised.
|
||||
|
||||
#### Parameters
|
||||
|
||||
`router: RpcRouter`: The router object that contains the RPCs.
|
||||
|
||||
`node: JsonNode`: A pre-processed `JsonNode` that matches the expected format as defined above.
|
||||
`req: RequestTx`: A pre-processed `Json document` that matches the expected format as defined above.
|
||||
|
||||
#### Returns
|
||||
|
||||
`Future[JsonNode]`: The JSON RPC result or a JSON wrapped error.
|
||||
`Future[ResponseTx]`: The JSON RPC result or a JSON wrapped error.
|
||||
|
||||
### `tryRoute`
|
||||
|
||||
@ -253,13 +241,13 @@ This `route` variant allows you to invoke a call if possible, without raising an
|
||||
|
||||
`router: RpcRouter`: The router object that contains the RPCs.
|
||||
|
||||
`node: JsonNode`: A pre-processed `JsonNode` that matches the expected format as defined above.
|
||||
`data: StringOfJson`: A raw `Json document` that matches the expected format as defined above.
|
||||
|
||||
`fut: var Future[JsonNode]`: The JSON RPC result or a JSON wrapped error.
|
||||
`fut: var Future[StringOfJson]`: The stringified JSON RPC result or a JSON wrapped error.
|
||||
|
||||
#### Returns
|
||||
|
||||
`bool`: `true` if the `method` field provided in `node` matches an available route. Returns `false` when the `method` cannot be found, or if `method` or `params` field cannot be found within `node`.
|
||||
`Result[void, string]` `isOk` if the `method` field provided in `data` matches an available route. Returns `isErr` when the `method` cannot be found, or if `method` or `params` field cannot be found within `data`.
|
||||
|
||||
|
||||
To see the result of a call, we need to provide Json in the expected format.
|
||||
@ -326,7 +314,7 @@ Below is the most basic way to use a remote call on the client.
|
||||
Here we manually supply the name and json parameters for the call.
|
||||
|
||||
The `call` procedure takes care of the basic format of the JSON to send to the server.
|
||||
However you still need to provide `params` as a `JsonNode`, which must exactly match the parameters defined in the equivalent `rpc` definition.
|
||||
However you still need to provide `params` as a `JsonNode` or `RequestParamsTx`, which must exactly match the parameters defined in the equivalent `rpc` definition.
|
||||
|
||||
```nim
|
||||
import json_rpc/[rpcclient, rpcserver], chronos, json
|
||||
@ -362,6 +350,11 @@ Because the signatures are parsed at compile time, the file will be error checke
|
||||
|
||||
`path`: The path to the Nim module that contains the RPC header signatures.
|
||||
|
||||
#### Variants of createRpcSigs
|
||||
- `createRpcSigsFromString`, generate rpc wrapper from string instead load it from file.
|
||||
- `createSingleRpcSig`, generate rpc wrapper from single Nim proc signature, with alias. e.g. calling same rpc method using different return type.
|
||||
- `createRpcSigsFromNim`, generate rpc wrapper from a list Nim proc signature, without loading any file.
|
||||
|
||||
#### Example
|
||||
|
||||
For example, to support this remote call:
|
||||
@ -404,7 +397,7 @@ Additionally, the following two procedures are useful:
|
||||
`name: string`: the method to be called
|
||||
`params: JsonNode`: The parameters to the RPC call
|
||||
Returning
|
||||
`Future[Response]`: A wrapper for the result `JsonNode` and a flag to indicate if this contains an error.
|
||||
`Future[StringOfJson]`: A wrapper for the result `Json document` and a flag to indicate if this contains an error.
|
||||
|
||||
Note: Although `call` isn't necessary for a client to function, it allows RPC signatures to be used by the `createRpcSigs`.
|
||||
|
||||
@ -416,9 +409,9 @@ Note: Although `call` isn't necessary for a client to function, it allows RPC si
|
||||
|
||||
### `processMessage`
|
||||
|
||||
To simplify and unify processing within the client, the `processMessage` procedure can be used to perform conversion and error checking from the received string originating from the transport to the `JsonNode` representation that is passed to the RPC.
|
||||
To simplify and unify processing within the client, the `processMessage` procedure can be used to perform conversion and error checking from the received string originating from the transport to the `Json document` representation that is passed to the RPC.
|
||||
|
||||
After a RPC returns, this procedure then completes the futures set by `call` invocations using the `id` field of the processed `JsonNode` from `line`.
|
||||
After a RPC returns, this procedure then completes the futures set by `call` invocations using the `id` field of the processed `Json document` from `line`.
|
||||
|
||||
#### Parameters
|
||||
|
||||
|
||||
@ -21,8 +21,8 @@ requires "nim >= 1.6.0",
|
||||
"stew",
|
||||
"nimcrypto",
|
||||
"stint",
|
||||
"chronos",
|
||||
"httputils",
|
||||
"chronos#head",
|
||||
"httputils#head",
|
||||
"chronicles",
|
||||
"websock",
|
||||
"json_serialization",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# json-rpc
|
||||
# Copyright (c) 2019-2023 Status Research & Development GmbH
|
||||
# Copyright (c) 2019-2024 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
@ -8,195 +8,129 @@
|
||||
# those terms.
|
||||
|
||||
import
|
||||
std/[tables, macros],
|
||||
std/[json, tables, macros],
|
||||
chronos,
|
||||
./jsonmarshal
|
||||
results,
|
||||
./private/jrpc_conv,
|
||||
./private/jrpc_sys,
|
||||
./private/client_handler_wrapper,
|
||||
./private/shared_wrapper,
|
||||
./private/errors
|
||||
|
||||
from strutils import toLowerAscii, replace
|
||||
from strutils import replace
|
||||
|
||||
export
|
||||
chronos, jsonmarshal, tables
|
||||
chronos,
|
||||
tables,
|
||||
jrpc_conv,
|
||||
RequestParamsTx,
|
||||
results
|
||||
|
||||
type
|
||||
ClientId* = int64
|
||||
MethodHandler* = proc (j: JsonNode) {.gcsafe, raises: [Defect, CatchableError].}
|
||||
RpcClient* = ref object of RootRef
|
||||
awaiting*: Table[ClientId, Future[Response]]
|
||||
lastId: ClientId
|
||||
methodHandlers: Table[string, MethodHandler]
|
||||
onDisconnect*: proc() {.gcsafe, raises: [Defect].}
|
||||
awaiting*: Table[RequestId, Future[StringOfJson]]
|
||||
lastId: int
|
||||
onDisconnect*: proc() {.gcsafe, raises: [].}
|
||||
|
||||
Response* = JsonNode
|
||||
GetJsonRpcRequestHeaders* = proc(): seq[(string, string)] {.gcsafe, raises: [].}
|
||||
|
||||
GetJsonRpcRequestHeaders* = proc(): seq[(string, string)] {.gcsafe, raises: [Defect].}
|
||||
{.push gcsafe, raises: [].}
|
||||
|
||||
proc getNextId*(client: RpcClient): ClientId =
|
||||
# ------------------------------------------------------------------------------
|
||||
# Public helpers
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
func requestTxEncode*(name: string, params: RequestParamsTx, id: RequestId): string =
|
||||
let req = requestTx(name, params, id)
|
||||
JrpcSys.encode(req)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Public functions
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
proc getNextId*(client: RpcClient): RequestId =
|
||||
client.lastId += 1
|
||||
client.lastId
|
||||
|
||||
proc rpcCallNode*(path: string, params: JsonNode, id: ClientId): JsonNode =
|
||||
%{"jsonrpc": %"2.0", "method": %path, "params": params, "id": %id}
|
||||
RequestId(kind: riNumber, num: client.lastId)
|
||||
|
||||
method call*(client: RpcClient, name: string,
|
||||
params: JsonNode): Future[Response] {.base, async.} =
|
||||
discard
|
||||
params: RequestParamsTx): Future[StringOfJson]
|
||||
{.base, gcsafe, async.} =
|
||||
doAssert(false, "`RpcClient.call` not implemented")
|
||||
|
||||
method close*(client: RpcClient): Future[void] {.base, async.} =
|
||||
discard
|
||||
method call*(client: RpcClient, name: string,
|
||||
params: JsonNode): Future[StringOfJson]
|
||||
{.base, gcsafe, async.} =
|
||||
|
||||
template `or`(a: JsonNode, b: typed): JsonNode =
|
||||
if a.isNil: b else: a
|
||||
await client.call(name, params.paramsTx)
|
||||
|
||||
proc processMessage*(self: RpcClient, line: string) =
|
||||
method close*(client: RpcClient): Future[void] {.base, gcsafe, async.} =
|
||||
doAssert(false, "`RpcClient.close` not implemented")
|
||||
|
||||
proc processMessage*(client: RpcClient, line: string): Result[void, string] =
|
||||
# Note: this doesn't use any transport code so doesn't need to be
|
||||
# differentiated.
|
||||
let node = try: parseJson(line)
|
||||
except CatchableError as exc: raise exc
|
||||
# TODO https://github.com/status-im/nimbus-eth2/issues/2430
|
||||
except Exception as exc: raise (ref ValueError)(msg: exc.msg, parent: exc)
|
||||
try:
|
||||
let response = JrpcSys.decode(line, ResponseRx)
|
||||
|
||||
if "id" in node:
|
||||
let id = node{"id"} or newJNull()
|
||||
if response.jsonrpc.isNone:
|
||||
return err("missing or invalid `jsonrpc`")
|
||||
|
||||
var requestFut: Future[Response]
|
||||
if not self.awaiting.pop(id.getInt(-1), requestFut):
|
||||
raise newException(ValueError, "Cannot find message id \"" & $id & "\"")
|
||||
if response.id.isNone:
|
||||
return err("missing or invalid response id")
|
||||
|
||||
let version = node{"jsonrpc"}.getStr()
|
||||
if version != "2.0":
|
||||
requestFut.fail(newException(ValueError,
|
||||
"Unsupported version of JSON, expected 2.0, received \"" & version & "\""))
|
||||
else:
|
||||
let result = node{"result"}
|
||||
if result.isNil:
|
||||
let error = node{"error"} or newJNull()
|
||||
requestFut.fail(newException(ValueError, $error))
|
||||
else:
|
||||
requestFut.complete(result)
|
||||
elif "method" in node:
|
||||
# This could be subscription notification
|
||||
let name = node["method"].getStr()
|
||||
let handler = self.methodHandlers.getOrDefault(name)
|
||||
if not handler.isNil:
|
||||
handler(node{"params"} or newJArray())
|
||||
else:
|
||||
raise newException(ValueError, "Invalid jsonrpc message: " & $node)
|
||||
var requestFut: Future[StringOfJson]
|
||||
let id = response.id.get
|
||||
if not client.awaiting.pop(id, requestFut):
|
||||
return err("Cannot find message id \"" & $id & "\"")
|
||||
|
||||
if response.error.isSome:
|
||||
let error = JrpcSys.encode(response.error.get)
|
||||
requestFut.fail(newException(JsonRpcError, error))
|
||||
return ok()
|
||||
|
||||
if response.result.isNone:
|
||||
return err("missing or invalid response result")
|
||||
|
||||
requestFut.complete(response.result.get)
|
||||
return ok()
|
||||
|
||||
except CatchableError as exc:
|
||||
return err(exc.msg)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Signature processing
|
||||
|
||||
proc createRpcProc(procName, parameters, callBody: NimNode): NimNode =
|
||||
# parameters come as a tree
|
||||
var paramList = newSeq[NimNode]()
|
||||
for p in parameters: paramList.add(p)
|
||||
|
||||
let body = quote do:
|
||||
{.gcsafe.}:
|
||||
`callBody`
|
||||
|
||||
# build proc
|
||||
result = newProc(procName, paramList, body)
|
||||
|
||||
# make proc async
|
||||
result.addPragma ident"async"
|
||||
# export this proc
|
||||
result[0] = nnkPostfix.newTree(ident"*", newIdentNode($procName))
|
||||
|
||||
proc toJsonArray(parameters: NimNode): NimNode =
|
||||
# outputs an array of jsonified parameters
|
||||
# ie; %[%a, %b, %c]
|
||||
parameters.expectKind nnkFormalParams
|
||||
var items = newNimNode(nnkBracket)
|
||||
for i in 2 ..< parameters.len:
|
||||
let curParam = parameters[i][0]
|
||||
if curParam.kind != nnkEmpty:
|
||||
items.add(nnkPrefix.newTree(ident"%", curParam))
|
||||
result = nnkPrefix.newTree(bindSym("%", brForceOpen), items)
|
||||
|
||||
proc createRpcFromSig*(clientType, rpcDecl: NimNode): NimNode =
|
||||
# Each input parameter in the rpc signature is converted
|
||||
# to json with `%`.
|
||||
# Return types are then converted back to native Nim types.
|
||||
let iJsonNode = newIdentNode("JsonNode")
|
||||
|
||||
var parameters = rpcDecl.findChild(it.kind == nnkFormalParams).copy
|
||||
# ensure we have at least space for a return parameter
|
||||
if parameters.isNil or parameters.kind == nnkEmpty or parameters.len == 0:
|
||||
parameters = nnkFormalParams.newTree(iJsonNode)
|
||||
|
||||
let
|
||||
procName = rpcDecl.name
|
||||
pathStr = $procName
|
||||
returnType =
|
||||
# if no return type specified, defaults to JsonNode
|
||||
if parameters[0].kind == nnkEmpty: iJsonNode
|
||||
else: parameters[0]
|
||||
customReturnType = returnType != iJsonNode
|
||||
|
||||
# insert rpc client as first parameter
|
||||
parameters.insert(1, nnkIdentDefs.newTree(ident"client", ident($clientType),
|
||||
newEmptyNode()))
|
||||
|
||||
let
|
||||
# variable used to send json to the server
|
||||
jsonParamIdent = genSym(nskVar, "jsonParam")
|
||||
# json array of marshalled parameters
|
||||
jsonParamArray = parameters.toJsonArray()
|
||||
var
|
||||
# populate json params - even rpcs with no parameters have an empty json
|
||||
# array node sent
|
||||
callBody = newStmtList().add(quote do:
|
||||
var `jsonParamIdent` = `jsonParamArray`
|
||||
)
|
||||
|
||||
# convert return type to Future
|
||||
parameters[0] = nnkBracketExpr.newTree(ident"Future", returnType)
|
||||
|
||||
let
|
||||
# temporary variable to hold `Response` from rpc call
|
||||
rpcResult = genSym(nskLet, "res")
|
||||
clientIdent = newIdentNode("client")
|
||||
# proc return variable
|
||||
procRes = ident"result"
|
||||
|
||||
# perform rpc call
|
||||
callBody.add(quote do:
|
||||
# `rpcResult` is of type `Response`
|
||||
let `rpcResult` = await `clientIdent`.call(`pathStr`, `jsonParamIdent`)
|
||||
)
|
||||
|
||||
if customReturnType:
|
||||
# marshal json to native Nim type
|
||||
callBody.add(jsonToNim(procRes, returnType, rpcResult, "result"))
|
||||
else:
|
||||
# native json expected so no work
|
||||
callBody.add quote do:
|
||||
`procRes` = if `rpcResult`.isNil:
|
||||
newJNull()
|
||||
else:
|
||||
`rpcResult`
|
||||
|
||||
# create rpc proc
|
||||
result = createRpcProc(procName, parameters, callBody)
|
||||
when defined(nimDumpRpcs):
|
||||
echo pathStr, ":\n", result.repr
|
||||
|
||||
proc processRpcSigs(clientType, parsedCode: NimNode): NimNode =
|
||||
result = newStmtList()
|
||||
|
||||
for line in parsedCode:
|
||||
if line.kind == nnkProcDef:
|
||||
var procDef = createRpcFromSig(clientType, line)
|
||||
result.add(procDef)
|
||||
|
||||
proc setMethodHandler*(cl: RpcClient, name: string, callback: MethodHandler) =
|
||||
cl.methodHandlers[name] = callback
|
||||
|
||||
proc delMethodHandler*(cl: RpcClient, name: string) =
|
||||
cl.methodHandlers.del(name)
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
macro createRpcSigs*(clientType: untyped, filePath: static[string]): untyped =
|
||||
## Takes a file of forward declarations in Nim and builds them into RPC
|
||||
## calls, based on their parameters.
|
||||
## Inputs are marshalled to json, and results are put into the signature's
|
||||
## Nim type.
|
||||
result = processRpcSigs(clientType, staticRead($filePath.replace('\\', '/')).parseStmt())
|
||||
cresteSignaturesFromString(clientType, staticRead($filePath.replace('\\', '/')))
|
||||
|
||||
macro createRpcSigsFromString*(clientType: untyped, sigString: static[string]): untyped =
|
||||
## Takes a string of forward declarations in Nim and builds them into RPC
|
||||
## calls, based on their parameters.
|
||||
## Inputs are marshalled to json, and results are put into the signature's
|
||||
## Nim type.
|
||||
cresteSignaturesFromString(clientType, sigString)
|
||||
|
||||
macro createSingleRpcSig*(clientType: untyped, alias: static[string], procDecl: typed): untyped =
|
||||
## Takes a single forward declarations in Nim and builds them into RPC
|
||||
## calls, based on their parameters.
|
||||
## Inputs are marshalled to json, and results are put into the signature's
|
||||
## Nim type.
|
||||
doAssert procDecl.len == 1, "Only accept single proc definition"
|
||||
let procDecl = procDecl[0]
|
||||
procDecl.expectKind nnkProcDef
|
||||
result = createRpcFromSig(clientType, procDecl, ident(alias))
|
||||
|
||||
macro createRpcSigsFromNim*(clientType: untyped, procList: typed): untyped =
|
||||
## Takes a list of forward declarations in Nim and builds them into RPC
|
||||
## calls, based on their parameters.
|
||||
## Inputs are marshalled to json, and results are put into the signature's
|
||||
## Nim type.
|
||||
processRpcSigs(clientType, procList)
|
||||
|
||||
{.pop.}
|
||||
|
||||
|
||||
@ -9,16 +9,17 @@
|
||||
|
||||
import
|
||||
std/[tables, uri],
|
||||
stew/[byteutils, results],
|
||||
stew/byteutils,
|
||||
results,
|
||||
chronos/apps/http/httpclient as chronosHttpClient,
|
||||
chronicles, httputils, json_serialization/std/net,
|
||||
".."/[client, errors]
|
||||
../client,
|
||||
../private/errors,
|
||||
../private/jrpc_sys
|
||||
|
||||
export
|
||||
client, HttpClientFlag, HttpClientFlags
|
||||
|
||||
{.push raises: [Defect].}
|
||||
|
||||
logScope:
|
||||
topics = "JSONRPC-HTTP-CLIENT"
|
||||
|
||||
@ -35,6 +36,8 @@ type
|
||||
const
|
||||
MaxHttpRequestSize = 128 * 1024 * 1024 # maximum size of HTTP body in octets
|
||||
|
||||
{.push gcsafe, raises: [].}
|
||||
|
||||
proc new(
|
||||
T: type RpcHttpClient, maxBodySize = MaxHttpRequestSize, secure = false,
|
||||
getHeaders: GetJsonRpcRequestHeaders = nil, flags: HttpClientFlags = {}): T =
|
||||
@ -51,7 +54,7 @@ proc newRpcHttpClient*(
|
||||
RpcHttpClient.new(maxBodySize, secure, getHeaders, flags)
|
||||
|
||||
method call*(client: RpcHttpClient, name: string,
|
||||
params: JsonNode): Future[Response]
|
||||
params: RequestParamsTx): Future[StringOfJson]
|
||||
{.async, gcsafe.} =
|
||||
doAssert client.httpSession != nil
|
||||
if client.httpAddress.isErr:
|
||||
@ -66,7 +69,7 @@ method call*(client: RpcHttpClient, name: string,
|
||||
|
||||
let
|
||||
id = client.getNextId()
|
||||
reqBody = $rpcCallNode(name, params, id)
|
||||
reqBody = requestTxEncode(name, params, id)
|
||||
|
||||
var req: HttpClientRequestRef
|
||||
var res: HttpClientResponseRef
|
||||
@ -128,19 +131,18 @@ method call*(client: RpcHttpClient, name: string,
|
||||
|
||||
# completed by processMessage - the flow is quite weird here to accomodate
|
||||
# socket and ws clients, but could use a more thorough refactoring
|
||||
var newFut = newFuture[Response]()
|
||||
var newFut = newFuture[StringOfJson]()
|
||||
# add to awaiting responses
|
||||
client.awaiting[id] = newFut
|
||||
|
||||
try:
|
||||
# Might raise for all kinds of reasons
|
||||
client.processMessage(resText)
|
||||
except CatchableError as e:
|
||||
# Might error for all kinds of reasons
|
||||
let msgRes = client.processMessage(resText)
|
||||
if msgRes.isErr:
|
||||
# Need to clean up in case the answer was invalid
|
||||
debug "Failed to process POST Response for JSON-RPC", e = e.msg
|
||||
debug "Failed to process POST Response for JSON-RPC", msg = msgRes.error
|
||||
client.awaiting.del(id)
|
||||
closeRefs()
|
||||
raise e
|
||||
raise newException(JsonRpcError, msgRes.error)
|
||||
|
||||
client.awaiting.del(id)
|
||||
|
||||
@ -175,3 +177,5 @@ proc connect*(client: RpcHttpClient, address: string, port: Port, secure: bool)
|
||||
method close*(client: RpcHttpClient) {.async.} =
|
||||
if not client.httpSession.isNil:
|
||||
await client.httpSession.closeWait()
|
||||
|
||||
{.pop.}
|
||||
|
||||
@ -9,10 +9,12 @@
|
||||
|
||||
import
|
||||
std/tables,
|
||||
chronicles,
|
||||
results,
|
||||
chronos,
|
||||
../client
|
||||
|
||||
{.push raises: [Defect].}
|
||||
../client,
|
||||
../private/errors,
|
||||
../private/jrpc_sys
|
||||
|
||||
export client
|
||||
|
||||
@ -24,6 +26,8 @@ type
|
||||
|
||||
const defaultMaxRequestLength* = 1024 * 128
|
||||
|
||||
{.push gcsafe, raises: [].}
|
||||
|
||||
proc new*(T: type RpcSocketClient): T =
|
||||
T()
|
||||
|
||||
@ -32,16 +36,16 @@ proc newRpcSocketClient*: RpcSocketClient =
|
||||
RpcSocketClient.new()
|
||||
|
||||
method call*(self: RpcSocketClient, name: string,
|
||||
params: JsonNode): Future[Response] {.async, gcsafe.} =
|
||||
params: RequestParamsTx): Future[StringOfJson] {.async, gcsafe.} =
|
||||
## Remotely calls the specified RPC method.
|
||||
let id = self.getNextId()
|
||||
var value = $rpcCallNode(name, params, id) & "\r\n"
|
||||
var value = requestTxEncode(name, params, id) & "\r\n"
|
||||
if self.transport.isNil:
|
||||
raise newException(ValueError,
|
||||
raise newException(JsonRpcError,
|
||||
"Transport is not initialised (missing a call to connect?)")
|
||||
|
||||
# completed by processMessage.
|
||||
var newFut = newFuture[Response]()
|
||||
var newFut = newFuture[StringOfJson]()
|
||||
# add to awaiting responses
|
||||
self.awaiting[id] = newFut
|
||||
|
||||
@ -60,8 +64,10 @@ proc processData(client: RpcSocketClient) {.async.} =
|
||||
await client.transport.closeWait()
|
||||
break
|
||||
|
||||
# TODO handle exceptions
|
||||
client.processMessage(value)
|
||||
let res = client.processMessage(value)
|
||||
if res.isErr:
|
||||
error "error when processing message", msg=res.error
|
||||
raise newException(JsonRpcError, res.error)
|
||||
|
||||
# async loop reconnection and waiting
|
||||
client.transport = await connect(client.address)
|
||||
|
||||
@ -11,13 +11,12 @@ import
|
||||
std/[uri, strutils],
|
||||
pkg/websock/[websock, extensions/compression/deflate],
|
||||
pkg/[chronos, chronos/apps/http/httptable, chronicles],
|
||||
stew/byteutils
|
||||
stew/byteutils,
|
||||
../private/errors
|
||||
|
||||
# avoid clash between Json.encode and Base64Pad.encode
|
||||
import ../client except encode
|
||||
|
||||
{.push raises: [Defect].}
|
||||
|
||||
logScope:
|
||||
topics = "JSONRPC-WS-CLIENT"
|
||||
|
||||
@ -28,6 +27,8 @@ type
|
||||
loop*: Future[void]
|
||||
getHeaders*: GetJsonRpcRequestHeaders
|
||||
|
||||
{.push gcsafe, raises: [].}
|
||||
|
||||
proc new*(
|
||||
T: type RpcWebSocketClient, getHeaders: GetJsonRpcRequestHeaders = nil): T =
|
||||
T(getHeaders: getHeaders)
|
||||
@ -38,16 +39,16 @@ proc newRpcWebSocketClient*(
|
||||
RpcWebSocketClient.new(getHeaders)
|
||||
|
||||
method call*(self: RpcWebSocketClient, name: string,
|
||||
params: JsonNode): Future[Response] {.async, gcsafe.} =
|
||||
params: RequestParamsTx): Future[StringOfJson] {.async, gcsafe.} =
|
||||
## Remotely calls the specified RPC method.
|
||||
let id = self.getNextId()
|
||||
var value = $rpcCallNode(name, params, id) & "\r\n"
|
||||
var value = requestTxEncode(name, params, id) & "\r\n"
|
||||
if self.transport.isNil:
|
||||
raise newException(ValueError,
|
||||
raise newException(JsonRpcError,
|
||||
"Transport is not initialised (missing a call to connect?)")
|
||||
|
||||
# completed by processMessage.
|
||||
var newFut = newFuture[Response]()
|
||||
var newFut = newFuture[StringOfJson]()
|
||||
# add to awaiting responses
|
||||
self.awaiting[id] = newFut
|
||||
|
||||
@ -66,7 +67,10 @@ proc processData(client: RpcWebSocketClient) {.async.} =
|
||||
# transmission ends
|
||||
break
|
||||
|
||||
client.processMessage(string.fromBytes(value))
|
||||
let res = client.processMessage(string.fromBytes(value))
|
||||
if res.isErr:
|
||||
raise newException(JsonRpcError, res.error)
|
||||
|
||||
except CatchableError as e:
|
||||
error = e
|
||||
|
||||
|
||||
@ -1,242 +0,0 @@
|
||||
# json-rpc
|
||||
# Copyright (c) 2019-2023 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
# at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
import
|
||||
std/[macros, json, typetraits],
|
||||
stew/[byteutils, objects],
|
||||
json_serialization,
|
||||
json_serialization/lexer,
|
||||
json_serialization/std/[options, sets, tables]
|
||||
|
||||
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 expectArrayLen(node, jsonIdent: NimNode, length: int) =
|
||||
let
|
||||
identStr = jsonIdent.repr
|
||||
expectedStr = "Expected " & $length & " Json parameter(s) but got "
|
||||
node.add(quote do:
|
||||
`jsonIdent`.kind.expect(JArray, `identStr`)
|
||||
if `jsonIdent`.len != `length`:
|
||||
raise newException(ValueError, `expectedStr` & $`jsonIdent`.len)
|
||||
)
|
||||
|
||||
iterator paramsIter(params: NimNode): tuple[name, ntype: NimNode] =
|
||||
for i in 1 ..< params.len:
|
||||
let arg = params[i]
|
||||
let argType = arg[^2]
|
||||
for j in 0 ..< arg.len-2:
|
||||
yield (arg[j], argType)
|
||||
|
||||
iterator paramsRevIter(params: NimNode): tuple[name, ntype: NimNode] =
|
||||
for i in countdown(params.len-1,1):
|
||||
let arg = params[i]
|
||||
let argType = arg[^2]
|
||||
for j in 0 ..< arg.len-2:
|
||||
yield (arg[j], argType)
|
||||
|
||||
proc isOptionalArg(typeNode: NimNode): bool =
|
||||
typeNode.kind == nnkBracketExpr and
|
||||
typeNode[0].kind == nnkIdent and
|
||||
typeNode[0].strVal == "Option"
|
||||
|
||||
proc expectOptionalArrayLen(node, parameters, jsonIdent: NimNode, maxLength: int): int =
|
||||
var minLength = maxLength
|
||||
|
||||
for arg, typ in paramsRevIter(parameters):
|
||||
if not typ.isOptionalArg: break
|
||||
dec minLength
|
||||
|
||||
let
|
||||
identStr = jsonIdent.repr
|
||||
expectedStr = "Expected at least " & $minLength & " and maximum " & $maxLength & " Json parameter(s) but got "
|
||||
|
||||
node.add(quote do:
|
||||
`jsonIdent`.kind.expect(JArray, `identStr`)
|
||||
if `jsonIdent`.len < `minLength`:
|
||||
raise newException(ValueError, `expectedStr` & $`jsonIdent`.len)
|
||||
)
|
||||
|
||||
minLength
|
||||
|
||||
proc containsOptionalArg(params: NimNode): bool =
|
||||
for n, t in paramsIter(params):
|
||||
if t.isOptionalArg:
|
||||
return true
|
||||
|
||||
proc jsonToNim*(assignIdent, paramType, jsonIdent: NimNode, paramNameStr: string, optional = false): NimNode =
|
||||
# verify input and load a Nim type from json data
|
||||
# note: does not create `assignIdent`, so can be used for `result` variables
|
||||
result = newStmtList()
|
||||
# unpack each parameter and provide assignments
|
||||
let unpackNode = quote do:
|
||||
`unpackArg`(`jsonIdent`, `paramNameStr`, type(`paramType`))
|
||||
|
||||
if optional:
|
||||
result.add(quote do: `assignIdent` = some(`unpackNode`))
|
||||
else:
|
||||
result.add(quote do: `assignIdent` = `unpackNode`)
|
||||
|
||||
proc calcActualParamCount(params: NimNode): int =
|
||||
# this proc is needed to calculate the actual parameter count
|
||||
# not matter what is the declaration form
|
||||
# e.g. (a: U, b: V) vs. (a, b: T)
|
||||
for n, t in paramsIter(params):
|
||||
inc result
|
||||
|
||||
proc jsonToNim*(params, jsonIdent: NimNode): NimNode =
|
||||
# Add code to verify input and load params into Nim types
|
||||
result = newStmtList()
|
||||
if not params.isNil:
|
||||
var minLength = 0
|
||||
if params.containsOptionalArg():
|
||||
# more elaborate parameters array check
|
||||
minLength = result.expectOptionalArrayLen(params, jsonIdent,
|
||||
calcActualParamCount(params))
|
||||
else:
|
||||
# simple parameters array length check
|
||||
result.expectArrayLen(jsonIdent, calcActualParamCount(params))
|
||||
|
||||
# unpack each parameter and provide assignments
|
||||
var pos = 0
|
||||
for paramIdent, paramType in paramsIter(params):
|
||||
# processing multiple variables of one type
|
||||
# e.g. (a, b: T), including common (a: U, b: V) form
|
||||
let
|
||||
paramName = $paramIdent
|
||||
jsonElement = quote do:
|
||||
`jsonIdent`.elems[`pos`]
|
||||
|
||||
# declare variable before assignment
|
||||
result.add(quote do:
|
||||
var `paramIdent`: `paramType`
|
||||
)
|
||||
|
||||
# e.g. (A: int, B: Option[int], C: string, D: Option[int], E: Option[string])
|
||||
if paramType.isOptionalArg:
|
||||
let
|
||||
innerType = paramType[1]
|
||||
innerNode = jsonToNim(paramIdent, innerType, jsonElement, paramName, true)
|
||||
|
||||
if pos >= minLength:
|
||||
# allow both empty and null after mandatory args
|
||||
# D & E fall into this category
|
||||
result.add(quote do:
|
||||
if `jsonIdent`.len > `pos` and `jsonElement`.kind != JNull: `innerNode`
|
||||
)
|
||||
else:
|
||||
# allow null param for optional args between/before mandatory args
|
||||
# B fall into this category
|
||||
result.add(quote do:
|
||||
if `jsonElement`.kind != JNull: `innerNode`
|
||||
)
|
||||
else:
|
||||
# mandatory args
|
||||
# A and C fall into this category
|
||||
# unpack Nim type and assign from json
|
||||
result.add jsonToNim(paramIdent, paramType, jsonElement, paramName)
|
||||
|
||||
inc pos
|
||||
109
json_rpc/private/client_handler_wrapper.nim
Normal file
109
json_rpc/private/client_handler_wrapper.nim
Normal file
@ -0,0 +1,109 @@
|
||||
# json-rpc
|
||||
# Copyright (c) 2024 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
# at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
import
|
||||
macros,
|
||||
./shared_wrapper
|
||||
|
||||
{.push gcsafe, raises: [].}
|
||||
|
||||
proc createRpcProc(procName, parameters, callBody: NimNode): NimNode =
|
||||
# parameters come as a tree
|
||||
var paramList = newSeq[NimNode]()
|
||||
for p in parameters: paramList.add(p)
|
||||
|
||||
let body = quote do:
|
||||
{.gcsafe.}:
|
||||
`callBody`
|
||||
|
||||
# build proc
|
||||
result = newProc(procName, paramList, body)
|
||||
|
||||
# make proc async
|
||||
result.addPragma ident"async"
|
||||
result.addPragma ident"gcsafe"
|
||||
# export this proc
|
||||
result[0] = nnkPostfix.newTree(ident"*", newIdentNode($procName))
|
||||
|
||||
proc setupConversion(reqParams, params: NimNode): NimNode =
|
||||
# populate json params
|
||||
# even rpcs with no parameters have an empty json array node sent
|
||||
|
||||
params.expectKind nnkFormalParams
|
||||
result = newStmtList()
|
||||
result.add quote do:
|
||||
var `reqParams` = RequestParamsTx(kind: rpPositional)
|
||||
|
||||
for parName, parType in paramsIter(params):
|
||||
result.add quote do:
|
||||
`reqParams`.positional.add encode(JrpcConv, `parName`).StringOfJson
|
||||
|
||||
proc createRpcFromSig*(clientType, rpcDecl: NimNode, alias = NimNode(nil)): NimNode =
|
||||
# Each input parameter in the rpc signature is converted
|
||||
# to json using JrpcConv.encode.
|
||||
# Return types are then converted back to native Nim types.
|
||||
|
||||
let
|
||||
params = rpcDecl.findChild(it.kind == nnkFormalParams).ensureReturnType
|
||||
procName = if alias.isNil: rpcDecl.name else: alias
|
||||
pathStr = $rpcDecl.name
|
||||
returnType = params[0]
|
||||
reqParams = genSym(nskVar, "reqParams")
|
||||
setup = setupConversion(reqParams, params)
|
||||
clientIdent = ident"client"
|
||||
# temporary variable to hold `Response` from rpc call
|
||||
rpcResult = genSym(nskLet, "res")
|
||||
# proc return variable
|
||||
procRes = ident"result"
|
||||
doDecode = quote do:
|
||||
`procRes` = decode(JrpcConv, `rpcResult`.string, typeof `returnType`)
|
||||
maybeWrap =
|
||||
if returnType.noWrap: quote do:
|
||||
`procRes` = `rpcResult`
|
||||
else: doDecode
|
||||
|
||||
# insert rpc client as first parameter
|
||||
params.insert(1, nnkIdentDefs.newTree(
|
||||
clientIdent,
|
||||
ident($clientType),
|
||||
newEmptyNode()
|
||||
))
|
||||
|
||||
# convert return type to Future
|
||||
params[0] = nnkBracketExpr.newTree(ident"Future", returnType)
|
||||
|
||||
# perform rpc call
|
||||
let callBody = quote do:
|
||||
# populate request params
|
||||
`setup`
|
||||
|
||||
# `rpcResult` is of type `StringOfJson`
|
||||
let `rpcResult` = await `clientIdent`.call(`pathStr`, `reqParams`)
|
||||
`maybeWrap`
|
||||
|
||||
# create rpc proc
|
||||
result = createRpcProc(procName, params, callBody)
|
||||
when defined(nimDumpRpcs):
|
||||
echo pathStr, ":\n", result.repr
|
||||
|
||||
proc processRpcSigs*(clientType, parsedCode: NimNode): NimNode =
|
||||
result = newStmtList()
|
||||
|
||||
for line in parsedCode:
|
||||
if line.kind == nnkProcDef:
|
||||
var procDef = createRpcFromSig(clientType, line)
|
||||
result.add(procDef)
|
||||
|
||||
proc cresteSignaturesFromString*(clientType: NimNode, sigStrings: string): NimNode =
|
||||
try:
|
||||
result = processRpcSigs(clientType, sigStrings.parseStmt())
|
||||
except ValueError as exc:
|
||||
doAssert(false, exc.msg)
|
||||
|
||||
{.pop.}
|
||||
@ -31,3 +31,10 @@ type
|
||||
## This could be raised by request handlers when the server
|
||||
## needs to respond with a custom error code.
|
||||
code*: int
|
||||
|
||||
RequestDecodeError* = object of JsonRpcError
|
||||
## raised when fail to decode RequestRx
|
||||
|
||||
ParamsEncodeError* = object of JsonRpcError
|
||||
## raised when fail to encode RequestParamsTx
|
||||
|
||||
23
json_rpc/private/jrpc_conv.nim
Normal file
23
json_rpc/private/jrpc_conv.nim
Normal file
@ -0,0 +1,23 @@
|
||||
# json-rpc
|
||||
# Copyright (c) 2023 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
# at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
import
|
||||
json_serialization
|
||||
|
||||
export
|
||||
json_serialization
|
||||
|
||||
type
|
||||
StringOfJson* = JsonString
|
||||
|
||||
createJsonFlavor JrpcConv,
|
||||
requireAllFields = false
|
||||
|
||||
# JrpcConv is a namespace/flavor for encoding and decoding
|
||||
# parameters and return value of a rpc method.
|
||||
306
json_rpc/private/jrpc_sys.nim
Normal file
306
json_rpc/private/jrpc_sys.nim
Normal file
@ -0,0 +1,306 @@
|
||||
# json-rpc
|
||||
# Copyright (c) 2023 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
# at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
import
|
||||
std/hashes,
|
||||
results,
|
||||
json_serialization,
|
||||
json_serialization/stew/results as jser_results
|
||||
|
||||
export
|
||||
results,
|
||||
json_serialization
|
||||
|
||||
# This module implements JSON-RPC 2.0 Specification
|
||||
# https://www.jsonrpc.org/specification
|
||||
|
||||
type
|
||||
# Special object of Json-RPC 2.0
|
||||
JsonRPC2* = object
|
||||
|
||||
RequestParamKind* = enum
|
||||
rpPositional
|
||||
rpNamed
|
||||
|
||||
ParamDescRx* = object
|
||||
kind* : JsonValueKind
|
||||
param*: JsonString
|
||||
|
||||
ParamDescNamed* = object
|
||||
name*: string
|
||||
value*: JsonString
|
||||
|
||||
# Request params received by server
|
||||
RequestParamsRx* = object
|
||||
case kind*: RequestParamKind
|
||||
of rpPositional:
|
||||
positional*: seq[ParamDescRx]
|
||||
of rpNamed:
|
||||
named*: seq[ParamDescNamed]
|
||||
|
||||
# Request params sent by client
|
||||
RequestParamsTx* = object
|
||||
case kind*: RequestParamKind
|
||||
of rpPositional:
|
||||
positional*: seq[JsonString]
|
||||
of rpNamed:
|
||||
named*: seq[ParamDescNamed]
|
||||
|
||||
RequestIdKind* = enum
|
||||
riNull
|
||||
riNumber
|
||||
riString
|
||||
|
||||
RequestId* = object
|
||||
case kind*: RequestIdKind
|
||||
of riNumber:
|
||||
num*: int
|
||||
of riString:
|
||||
str*: string
|
||||
of riNull:
|
||||
discard
|
||||
|
||||
# Request received by server
|
||||
RequestRx* = object
|
||||
jsonrpc* : results.Opt[JsonRPC2]
|
||||
id* : RequestId
|
||||
`method`*: results.Opt[string]
|
||||
params* : RequestParamsRx
|
||||
|
||||
# Request sent by client
|
||||
RequestTx* = object
|
||||
jsonrpc* : JsonRPC2
|
||||
id* : results.Opt[RequestId]
|
||||
`method`*: string
|
||||
params* : RequestParamsTx
|
||||
|
||||
ResponseError* = object
|
||||
code* : int
|
||||
message*: string
|
||||
data* : results.Opt[JsonString]
|
||||
|
||||
ResponseKind* = enum
|
||||
rkResult
|
||||
rkError
|
||||
|
||||
# Response sent by server
|
||||
ResponseTx* = object
|
||||
jsonrpc* : JsonRPC2
|
||||
id* : RequestId
|
||||
case kind*: ResponseKind
|
||||
of rkResult:
|
||||
result* : JsonString
|
||||
of rkError:
|
||||
error* : ResponseError
|
||||
|
||||
# Response received by client
|
||||
ResponseRx* = object
|
||||
jsonrpc*: results.Opt[JsonRPC2]
|
||||
id* : results.Opt[RequestId]
|
||||
result* : results.Opt[JsonString]
|
||||
error* : results.Opt[ResponseError]
|
||||
|
||||
ReBatchKind* = enum
|
||||
rbkSingle
|
||||
rbkMany
|
||||
|
||||
RequestBatchRx* = object
|
||||
case kind*: ReBatchKind
|
||||
of rbkMany:
|
||||
many* : seq[RequestRx]
|
||||
of rbkSingle:
|
||||
single*: RequestRx
|
||||
|
||||
RequestBatchTx* = object
|
||||
case kind*: ReBatchKind
|
||||
of rbkMany:
|
||||
many* : seq[RequestTx]
|
||||
of rbkSingle:
|
||||
single*: RequestTx
|
||||
|
||||
ResponseBatchRx* = object
|
||||
case kind*: ReBatchKind
|
||||
of rbkMany:
|
||||
many* : seq[ResponseRx]
|
||||
of rbkSingle:
|
||||
single*: ResponseRx
|
||||
|
||||
ResponseBatchTx* = object
|
||||
case kind*: ReBatchKind
|
||||
of rbkMany:
|
||||
many* : seq[ResponseTx]
|
||||
of rbkSingle:
|
||||
single*: ResponseTx
|
||||
|
||||
# don't mix the json-rpc system encoding with the
|
||||
# actual response/params encoding
|
||||
createJsonFlavor JrpcSys,
|
||||
requireAllFields = false
|
||||
|
||||
ResponseError.useDefaultSerializationIn JrpcSys
|
||||
RequestTx.useDefaultWriterIn JrpcSys
|
||||
ResponseRx.useDefaultReaderIn JrpcSys
|
||||
RequestRx.useDefaultReaderIn JrpcSys
|
||||
|
||||
const
|
||||
JsonRPC2Literal = JsonString("\"2.0\"")
|
||||
|
||||
{.push gcsafe, raises: [].}
|
||||
|
||||
func hash*(x: RequestId): hashes.Hash =
|
||||
var h = 0.Hash
|
||||
case x.kind:
|
||||
of riNumber: h = h !& hash(x.num)
|
||||
of riString: h = h !& hash(x.str)
|
||||
of riNull: h = h !& hash("null")
|
||||
result = !$(h)
|
||||
|
||||
func `$`*(x: RequestId): string =
|
||||
case x.kind:
|
||||
of riNumber: $x.num
|
||||
of riString: x.str
|
||||
of riNull: "null"
|
||||
|
||||
func `==`*(a, b: RequestId): bool =
|
||||
if a.kind != b.kind:
|
||||
return false
|
||||
case a.kind
|
||||
of riNumber: a.num == b.num
|
||||
of riString: a.str == b.str
|
||||
of riNull: true
|
||||
|
||||
func meth*(rx: RequestRx): Opt[string] =
|
||||
rx.`method`
|
||||
|
||||
proc readValue*(r: var JsonReader[JrpcSys], val: var JsonRPC2)
|
||||
{.gcsafe, raises: [IOError, JsonReaderError].} =
|
||||
let version = r.parseAsString()
|
||||
if version != JsonRPC2Literal:
|
||||
r.raiseUnexpectedValue("Invalid JSON-RPC version, want=" &
|
||||
JsonRPC2Literal.string & " got=" & version.string)
|
||||
|
||||
proc writeValue*(w: var JsonWriter[JrpcSys], val: JsonRPC2)
|
||||
{.gcsafe, raises: [IOError].} =
|
||||
w.writeValue JsonRPC2Literal
|
||||
|
||||
proc readValue*(r: var JsonReader[JrpcSys], val: var RequestId)
|
||||
{.gcsafe, raises: [IOError, JsonReaderError].} =
|
||||
let tok = r.tokKind
|
||||
case tok
|
||||
of JsonValueKind.Number:
|
||||
val = RequestId(kind: riNumber, num: r.parseInt(int))
|
||||
of JsonValueKind.String:
|
||||
val = RequestId(kind: riString, str: r.parseString())
|
||||
of JsonValueKind.Null:
|
||||
val = RequestId(kind: riNull)
|
||||
r.parseNull()
|
||||
else:
|
||||
r.raiseUnexpectedValue("Invalid RequestId, must be Number, String, or Null, got=" & $tok)
|
||||
|
||||
proc writeValue*(w: var JsonWriter[JrpcSys], val: RequestId)
|
||||
{.gcsafe, raises: [IOError].} =
|
||||
case val.kind
|
||||
of riNumber: w.writeValue val.num
|
||||
of riString: w.writeValue val.str
|
||||
of riNull: w.writeValue JsonString("null")
|
||||
|
||||
proc readValue*(r: var JsonReader[JrpcSys], val: var RequestParamsRx)
|
||||
{.gcsafe, raises: [IOError, SerializationError].} =
|
||||
let tok = r.tokKind
|
||||
case tok
|
||||
of JsonValueKind.Array:
|
||||
val = RequestParamsRx(kind: rpPositional)
|
||||
r.parseArray:
|
||||
val.positional.add ParamDescRx(
|
||||
kind: r.tokKind(),
|
||||
param: r.parseAsString(),
|
||||
)
|
||||
of JsonValueKind.Object:
|
||||
val = RequestParamsRx(kind: rpNamed)
|
||||
for key in r.readObjectFields():
|
||||
val.named.add ParamDescNamed(
|
||||
name: key,
|
||||
value: r.parseAsString(),
|
||||
)
|
||||
else:
|
||||
r.raiseUnexpectedValue("RequestParam must be either array or object, got=" & $tok)
|
||||
|
||||
proc writeValue*(w: var JsonWriter[JrpcSys], val: RequestParamsTx)
|
||||
{.gcsafe, raises: [IOError].} =
|
||||
case val.kind
|
||||
of rpPositional:
|
||||
w.writeArray val.positional
|
||||
of rpNamed:
|
||||
w.beginRecord RequestParamsTx
|
||||
for x in val.named:
|
||||
w.writeField(x.name, x.value)
|
||||
w.endRecord()
|
||||
|
||||
proc writeValue*(w: var JsonWriter[JrpcSys], val: ResponseTx)
|
||||
{.gcsafe, raises: [IOError].} =
|
||||
w.beginRecord ResponseTx
|
||||
w.writeField("jsonrpc", val.jsonrpc)
|
||||
w.writeField("id", val.id)
|
||||
if val.kind == rkResult:
|
||||
w.writeField("result", val.result)
|
||||
else:
|
||||
w.writeField("error", val.error)
|
||||
w.endRecord()
|
||||
|
||||
proc writeValue*(w: var JsonWriter[JrpcSys], val: RequestBatchTx)
|
||||
{.gcsafe, raises: [IOError].} =
|
||||
if val.kind == rbkMany:
|
||||
w.writeArray(val.many)
|
||||
else:
|
||||
w.writeValue(val.single)
|
||||
|
||||
proc readValue*(r: var JsonReader[JrpcSys], val: var RequestBatchRx)
|
||||
{.gcsafe, raises: [IOError, SerializationError].} =
|
||||
let tok = r.tokKind
|
||||
case tok
|
||||
of JsonValueKind.Array:
|
||||
val = RequestBatchRx(kind: rbkMany)
|
||||
r.readValue(val.many)
|
||||
of JsonValueKind.Object:
|
||||
val = RequestBatchRx(kind: rbkSingle)
|
||||
r.readValue(val.single)
|
||||
else:
|
||||
r.raiseUnexpectedValue("RequestBatch must be either array or object, got=" & $tok)
|
||||
|
||||
proc writeValue*(w: var JsonWriter[JrpcSys], val: ResponseBatchTx)
|
||||
{.gcsafe, raises: [IOError].} =
|
||||
if val.kind == rbkMany:
|
||||
w.writeArray(val.many)
|
||||
else:
|
||||
w.writeValue(val.single)
|
||||
|
||||
proc readValue*(r: var JsonReader[JrpcSys], val: var ResponseBatchRx)
|
||||
{.gcsafe, raises: [IOError, SerializationError].} =
|
||||
let tok = r.tokKind
|
||||
case tok
|
||||
of JsonValueKind.Array:
|
||||
val = ResponseBatchRx(kind: rbkMany)
|
||||
r.readValue(val.many)
|
||||
of JsonValueKind.Object:
|
||||
val = ResponseBatchRx(kind: rbkSingle)
|
||||
r.readValue(val.single)
|
||||
else:
|
||||
r.raiseUnexpectedValue("ResponseBatch must be either array or object, got=" & $tok)
|
||||
|
||||
proc toTx*(params: RequestParamsRx): RequestParamsTx =
|
||||
case params.kind:
|
||||
of rpPositional:
|
||||
result = RequestParamsTx(kind: rpPositional)
|
||||
for x in params.positional:
|
||||
result.positional.add x.param
|
||||
of rpNamed:
|
||||
result = RequestParamsTx(kind: rpNamed)
|
||||
result.named = params.named
|
||||
|
||||
{.pop.}
|
||||
301
json_rpc/private/server_handler_wrapper.nim
Normal file
301
json_rpc/private/server_handler_wrapper.nim
Normal file
@ -0,0 +1,301 @@
|
||||
# json-rpc
|
||||
# Copyright (c) 2019-2023 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
# at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
import
|
||||
std/[macros, typetraits],
|
||||
stew/[byteutils, objects],
|
||||
json_serialization,
|
||||
json_serialization/std/[options],
|
||||
./errors,
|
||||
./jrpc_sys,
|
||||
./jrpc_conv,
|
||||
./shared_wrapper
|
||||
|
||||
export
|
||||
jrpc_conv
|
||||
|
||||
{.push gcsafe, raises: [].}
|
||||
|
||||
proc unpackArg(args: JsonString, argName: string, argType: type): argType
|
||||
{.gcsafe, raises: [JsonRpcError].} =
|
||||
## This where input parameters are decoded from JSON into
|
||||
## Nim data types
|
||||
try:
|
||||
result = JrpcConv.decode(args.string, argType)
|
||||
except CatchableError as err:
|
||||
raise newException(RequestDecodeError,
|
||||
"Parameter [" & argName & "] of type '" &
|
||||
$argType & "' could not be decoded: " & err.msg)
|
||||
|
||||
proc expectArrayLen(node, paramsIdent: NimNode, length: int) =
|
||||
## Make sure positional params meets the handler expectation
|
||||
let
|
||||
expected = "Expected " & $length & " Json parameter(s) but got "
|
||||
node.add quote do:
|
||||
if `paramsIdent`.positional.len != `length`:
|
||||
raise newException(RequestDecodeError, `expected` &
|
||||
$`paramsIdent`.positional.len)
|
||||
|
||||
iterator paramsRevIter(params: NimNode): tuple[name, ntype: NimNode] =
|
||||
## Bacward iterator of handler parameters
|
||||
for i in countdown(params.len-1,1):
|
||||
let arg = params[i]
|
||||
let argType = arg[^2]
|
||||
for j in 0 ..< arg.len-2:
|
||||
yield (arg[j], argType)
|
||||
|
||||
proc isOptionalArg(typeNode: NimNode): bool =
|
||||
typeNode.kind == nnkBracketExpr and
|
||||
typeNode[0].kind == nnkIdent and
|
||||
typeNode[0].strVal == "Option"
|
||||
|
||||
proc expectOptionalArrayLen(node: NimNode,
|
||||
parameters: NimNode,
|
||||
paramsIdent: NimNode,
|
||||
maxLength: int): int =
|
||||
## Validate if parameters sent by client meets
|
||||
## minimum expectation of server
|
||||
var minLength = maxLength
|
||||
|
||||
for arg, typ in paramsRevIter(parameters):
|
||||
if not typ.isOptionalArg: break
|
||||
dec minLength
|
||||
|
||||
let
|
||||
expected = "Expected at least " & $minLength & " and maximum " &
|
||||
$maxLength & " Json parameter(s) but got "
|
||||
|
||||
node.add quote do:
|
||||
if `paramsIdent`.positional.len < `minLength`:
|
||||
raise newException(RequestDecodeError, `expected` &
|
||||
$`paramsIdent`.positional.len)
|
||||
|
||||
minLength
|
||||
|
||||
proc containsOptionalArg(params: NimNode): bool =
|
||||
## Is one of handler parameters an optional?
|
||||
for n, t in paramsIter(params):
|
||||
if t.isOptionalArg:
|
||||
return true
|
||||
|
||||
proc jsonToNim(paramVar: NimNode,
|
||||
paramType: NimNode,
|
||||
paramVal: NimNode,
|
||||
paramName: string): NimNode =
|
||||
## Convert a positional parameter from Json into Nim
|
||||
result = quote do:
|
||||
`paramVar` = `unpackArg`(`paramVal`, `paramName`, `paramType`)
|
||||
|
||||
proc calcActualParamCount(params: NimNode): int =
|
||||
## this proc is needed to calculate the actual parameter count
|
||||
## not matter what is the declaration form
|
||||
## e.g. (a: U, b: V) vs. (a, b: T)
|
||||
for n, t in paramsIter(params):
|
||||
inc result
|
||||
|
||||
proc makeType(typeName, params: NimNode): NimNode =
|
||||
## Generate type section contains an object definition
|
||||
## with fields of handler params
|
||||
let typeSec = quote do:
|
||||
type `typeName` = object
|
||||
|
||||
let obj = typeSec[0][2]
|
||||
let recList = newNimNode(nnkRecList)
|
||||
if params.len > 1:
|
||||
for i in 1..<params.len:
|
||||
recList.add params[i]
|
||||
obj[2] = recList
|
||||
typeSec
|
||||
|
||||
proc setupPositional(params, paramsIdent: NimNode): (NimNode, int) =
|
||||
## Generate code to check positional params length
|
||||
var
|
||||
minLength = 0
|
||||
code = newStmtList()
|
||||
|
||||
if params.containsOptionalArg():
|
||||
# more elaborate parameters array check
|
||||
minLength = code.expectOptionalArrayLen(params, paramsIdent,
|
||||
calcActualParamCount(params))
|
||||
else:
|
||||
# simple parameters array length check
|
||||
code.expectArrayLen(paramsIdent, calcActualParamCount(params))
|
||||
|
||||
(code, minLength)
|
||||
|
||||
proc setupPositional(code: NimNode;
|
||||
paramsObj, paramsIdent, paramIdent, paramType: NimNode;
|
||||
pos, minLength: int) =
|
||||
## processing multiple params of one type
|
||||
## e.g. (a, b: T), including common (a: U, b: V) form
|
||||
let
|
||||
paramName = $paramIdent
|
||||
paramVal = quote do:
|
||||
`paramsIdent`.positional[`pos`].param
|
||||
paramKind = quote do:
|
||||
`paramsIdent`.positional[`pos`].kind
|
||||
paramVar = quote do:
|
||||
`paramsObj`.`paramIdent`
|
||||
|
||||
# e.g. (A: int, B: Option[int], C: string, D: Option[int], E: Option[string])
|
||||
if paramType.isOptionalArg:
|
||||
let
|
||||
innerNode = jsonToNim(paramVar, paramType, paramVal, paramName)
|
||||
if pos >= minLength:
|
||||
# allow both empty and null after mandatory args
|
||||
# D & E fall into this category
|
||||
code.add quote do:
|
||||
if `paramsIdent`.positional.len > `pos` and
|
||||
`paramKind` != JsonValueKind.Null:
|
||||
`innerNode`
|
||||
else:
|
||||
# allow null param for optional args between/before mandatory args
|
||||
# B fall into this category
|
||||
code.add quote do:
|
||||
if `paramKind` != JsonValueKind.Null:
|
||||
`innerNode`
|
||||
else:
|
||||
# mandatory args
|
||||
# A and C fall into this category
|
||||
# unpack Nim type and assign from json
|
||||
code.add jsonToNim(paramVar, paramType, paramVal, paramName)
|
||||
|
||||
proc makeParams(retType: NimNode, params: NimNode): seq[NimNode] =
|
||||
## Convert rpc params into handler params
|
||||
result.add retType
|
||||
if params.len > 1:
|
||||
for i in 1..<params.len:
|
||||
result.add params[i]
|
||||
|
||||
proc makeHandler(procName, params, procBody, returnInner: NimNode): NimNode =
|
||||
## Generate rpc handler proc
|
||||
let
|
||||
returnType = quote do: Future[`returnInner`]
|
||||
paramList = makeParams(returnType, params)
|
||||
pragmas = quote do: {.async.}
|
||||
|
||||
result = newProc(
|
||||
name = procName,
|
||||
params = paramList,
|
||||
body = procBody,
|
||||
pragmas = pragmas
|
||||
)
|
||||
|
||||
proc ofStmt(x, paramsObj, paramName, paramType: NimNode): NimNode =
|
||||
let caseStr = $paramName
|
||||
result = nnkOfBranch.newTree(
|
||||
quote do: `caseStr`,
|
||||
quote do:
|
||||
`paramsObj`.`paramName` = unpackArg(`x`.value, `caseStr`, `paramType`)
|
||||
)
|
||||
|
||||
proc setupNamed(paramsObj, paramsIdent, params: NimNode): NimNode =
|
||||
let x = ident"x"
|
||||
|
||||
var caseStmt = nnkCaseStmt.newTree(
|
||||
quote do: `x`.name
|
||||
)
|
||||
|
||||
for paramName, paramType in paramsIter(params):
|
||||
caseStmt.add ofStmt(x, paramsObj, paramName, paramType)
|
||||
|
||||
caseStmt.add nnkElse.newTree(
|
||||
quote do: discard
|
||||
)
|
||||
|
||||
result = quote do:
|
||||
for `x` in `paramsIdent`.named:
|
||||
`caseStmt`
|
||||
|
||||
proc wrapServerHandler*(methName: string, params, procBody, procWrapper: NimNode): NimNode =
|
||||
## This proc generate something like this:
|
||||
##
|
||||
## proc rpcHandler(paramA: ParamAType, paramB: ParamBType): Future[ReturnType] =
|
||||
## procBody
|
||||
## return retVal
|
||||
##
|
||||
## proc rpcWrapper(params: RequestParamsRx): Future[StringOfJson] =
|
||||
## type
|
||||
## RpcType = object
|
||||
## paramA: ParamAType
|
||||
## paramB: ParamBType
|
||||
##
|
||||
## var rpcVar: RpcType
|
||||
##
|
||||
## if params.isPositional:
|
||||
## if params.positional.len < expectedLen:
|
||||
## raise exception
|
||||
## rpcVar.paramA = params.unpack(paramA of ParamAType)
|
||||
## rpcVar.paramB = params.unpack(paramB of ParamBType)
|
||||
## else:
|
||||
## rpcVar = params.unpack(named of RpcType)
|
||||
##
|
||||
## let res = await rpcHandler(rpcVar.paramA, rpcVar.paramB)
|
||||
## return JrpcConv.encode(res).StringOfJson
|
||||
|
||||
let
|
||||
params = params.ensureReturnType()
|
||||
setup = newStmtList()
|
||||
typeName = genSym(nskType, "RpcType")
|
||||
paramsObj = ident"rpcVar"
|
||||
handlerName = genSym(nskProc, methName & "_rpcHandler")
|
||||
paramsIdent = genSym(nskParam, "rpcParams")
|
||||
returnType = params[0]
|
||||
hasParams = params.len > 1 # not including return type
|
||||
(posSetup, minLength) = setupPositional(params, paramsIdent)
|
||||
handler = makeHandler(handlerName, params, procBody, returnType)
|
||||
named = setupNamed(paramsObj, paramsIdent, params)
|
||||
|
||||
if hasParams:
|
||||
setup.add makeType(typeName, params)
|
||||
setup.add quote do:
|
||||
var `paramsObj`: `typeName`
|
||||
|
||||
# unpack each parameter and provide assignments
|
||||
var
|
||||
pos = 0
|
||||
positional = newStmtList()
|
||||
executeParams: seq[NimNode]
|
||||
|
||||
for paramIdent, paramType in paramsIter(params):
|
||||
positional.setupPositional(paramsObj, paramsIdent,
|
||||
paramIdent, paramType, pos, minLength)
|
||||
executeParams.add quote do:
|
||||
`paramsObj`.`paramIdent`
|
||||
inc pos
|
||||
|
||||
if hasParams:
|
||||
setup.add quote do:
|
||||
if `paramsIdent`.kind == rpPositional:
|
||||
`posSetup`
|
||||
`positional`
|
||||
else:
|
||||
`named`
|
||||
else:
|
||||
setup.add quote do:
|
||||
if `paramsIdent`.kind == rpPositional:
|
||||
`posSetup`
|
||||
|
||||
let
|
||||
awaitedResult = ident "awaitedResult"
|
||||
doEncode = quote do: encode(JrpcConv, `awaitedResult`)
|
||||
maybeWrap =
|
||||
if returnType.noWrap: awaitedResult
|
||||
else: ident"StringOfJson".newCall doEncode
|
||||
executeCall = newCall(handlerName, executeParams)
|
||||
|
||||
result = newStmtList()
|
||||
result.add handler
|
||||
result.add quote do:
|
||||
proc `procWrapper`(`paramsIdent`: RequestParamsRx): Future[StringOfJson] {.async, gcsafe.} =
|
||||
# Avoid 'yield in expr not lowered' with an intermediate variable.
|
||||
# See: https://github.com/nim-lang/Nim/issues/17849
|
||||
`setup`
|
||||
let `awaitedResult` = await `executeCall`
|
||||
return `maybeWrap`
|
||||
70
json_rpc/private/shared_wrapper.nim
Normal file
70
json_rpc/private/shared_wrapper.nim
Normal file
@ -0,0 +1,70 @@
|
||||
# json-rpc
|
||||
# Copyright (c) 2024 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
# at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
import
|
||||
std/[json, macros],
|
||||
./jrpc_sys,
|
||||
./jrpc_conv
|
||||
|
||||
iterator paramsIter*(params: NimNode): tuple[name, ntype: NimNode] =
|
||||
## Forward iterator of handler parameters
|
||||
for i in 1 ..< params.len:
|
||||
let arg = params[i]
|
||||
let argType = arg[^2]
|
||||
for j in 0 ..< arg.len-2:
|
||||
yield (arg[j], argType)
|
||||
|
||||
func ensureReturnType*(params: NimNode): NimNode =
|
||||
let retType = ident"JsonNode"
|
||||
if params.isNil or params.kind == nnkEmpty or params.len == 0:
|
||||
return nnkFormalParams.newTree(retType)
|
||||
|
||||
if params.len >= 1 and params[0].kind == nnkEmpty:
|
||||
params[0] = retType
|
||||
|
||||
params
|
||||
|
||||
func noWrap*(returnType: NimNode): bool =
|
||||
## Condition when return type should not be encoded
|
||||
## to Json
|
||||
returnType.repr == "StringOfJson" or
|
||||
returnType.repr == "JsonString"
|
||||
|
||||
func paramsTx*(params: JsonNode): RequestParamsTx =
|
||||
if params.kind == JArray:
|
||||
var args: seq[JsonString]
|
||||
for x in params:
|
||||
args.add JrpcConv.encode(x).JsonString
|
||||
RequestParamsTx(
|
||||
kind: rpPositional,
|
||||
positional: system.move(args),
|
||||
)
|
||||
elif params.kind == JObject:
|
||||
var args: seq[ParamDescNamed]
|
||||
for k, v in params:
|
||||
args.add ParamDescNamed(
|
||||
name: k,
|
||||
value: JrpcConv.encode(v).JsonString,
|
||||
)
|
||||
RequestParamsTx(
|
||||
kind: rpNamed,
|
||||
named: system.move(args),
|
||||
)
|
||||
else:
|
||||
RequestParamsTx(
|
||||
kind: rpPositional,
|
||||
positional: @[JrpcConv.encode(params).JsonString],
|
||||
)
|
||||
|
||||
func requestTx*(name: string, params: RequestParamsTx, id: RequestId): RequestTx =
|
||||
RequestTx(
|
||||
id: Opt.some(id),
|
||||
`method`: name,
|
||||
params: params,
|
||||
)
|
||||
@ -8,135 +8,204 @@
|
||||
# those terms.
|
||||
|
||||
import
|
||||
std/[macros, strutils, tables],
|
||||
chronicles, chronos, json_serialization/writer,
|
||||
./jsonmarshal, ./errors
|
||||
std/[macros, tables, json],
|
||||
chronicles,
|
||||
chronos,
|
||||
./private/server_handler_wrapper,
|
||||
./private/errors,
|
||||
./private/jrpc_sys
|
||||
|
||||
export
|
||||
chronos, jsonmarshal
|
||||
chronos,
|
||||
jrpc_conv,
|
||||
json
|
||||
|
||||
type
|
||||
StringOfJson* = JsonString
|
||||
|
||||
# Procedure signature accepted as an RPC call by server
|
||||
RpcProc* = proc(input: JsonNode): Future[StringOfJson] {.gcsafe, raises: [Defect].}
|
||||
RpcProc* = proc(params: RequestParamsRx): Future[StringOfJson]
|
||||
{.gcsafe, raises: [CatchableError].}
|
||||
|
||||
RpcRouter* = object
|
||||
procs*: Table[string, RpcProc]
|
||||
|
||||
const
|
||||
methodField = "method"
|
||||
paramsField = "params"
|
||||
|
||||
JSON_PARSE_ERROR* = -32700
|
||||
INVALID_REQUEST* = -32600
|
||||
METHOD_NOT_FOUND* = -32601
|
||||
INVALID_PARAMS* = -32602
|
||||
INTERNAL_ERROR* = -32603
|
||||
SERVER_ERROR* = -32000
|
||||
JSON_ENCODE_ERROR* = -32001
|
||||
|
||||
defaultMaxRequestLength* = 1024 * 128
|
||||
|
||||
{.push gcsafe, raises: [].}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Private helpers
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
func invalidRequest(msg: string): ResponseError =
|
||||
ResponseError(code: INVALID_REQUEST, message: msg)
|
||||
|
||||
func methodNotFound(msg: string): ResponseError =
|
||||
ResponseError(code: METHOD_NOT_FOUND, message: msg)
|
||||
|
||||
func serverError(msg: string, data: StringOfJson): ResponseError =
|
||||
ResponseError(code: SERVER_ERROR, message: msg, data: Opt.some(data))
|
||||
|
||||
func somethingError(code: int, msg: string): ResponseError =
|
||||
ResponseError(code: code, message: msg)
|
||||
|
||||
proc validateRequest(router: RpcRouter, req: RequestRx):
|
||||
Result[RpcProc, ResponseError] =
|
||||
if req.jsonrpc.isNone:
|
||||
return invalidRequest("'jsonrpc' missing or invalid").err
|
||||
|
||||
if req.id.kind == riNull:
|
||||
return invalidRequest("'id' missing or invalid").err
|
||||
|
||||
if req.meth.isNone:
|
||||
return invalidRequest("'method' missing or invalid").err
|
||||
|
||||
let
|
||||
methodName = req.meth.get
|
||||
rpcProc = router.procs.getOrDefault(methodName)
|
||||
|
||||
if rpcProc.isNil:
|
||||
return methodNotFound("'" & methodName &
|
||||
"' is not a registered RPC method").err
|
||||
|
||||
ok(rpcProc)
|
||||
|
||||
proc wrapError(err: ResponseError, id: RequestId): ResponseTx =
|
||||
ResponseTx(
|
||||
id: id,
|
||||
kind: rkError,
|
||||
error: err,
|
||||
)
|
||||
|
||||
proc wrapError(code: int, msg: string, id: RequestId): ResponseTx =
|
||||
ResponseTx(
|
||||
id: id,
|
||||
kind: rkError,
|
||||
error: somethingError(code, msg),
|
||||
)
|
||||
|
||||
proc wrapReply(res: StringOfJson, id: RequestId): ResponseTx =
|
||||
ResponseTx(
|
||||
id: id,
|
||||
kind: rkResult,
|
||||
result: res,
|
||||
)
|
||||
|
||||
proc wrapError(code: int, msg: string): string =
|
||||
"""{"jsonrpc":"2.0","id":null,"error":{"code":""" & $code &
|
||||
""","message":""" & escapeJson(msg) & "}}"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Public functions
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
proc init*(T: type RpcRouter): T = discard
|
||||
|
||||
proc newRpcRouter*: RpcRouter {.deprecated.} =
|
||||
RpcRouter.init()
|
||||
|
||||
proc register*(router: var RpcRouter, path: string, call: RpcProc) =
|
||||
proc register*(router: var RpcRouter, path: string, call: RpcProc)
|
||||
{.gcsafe, raises: [CatchableError].} =
|
||||
router.procs[path] = call
|
||||
|
||||
proc clear*(router: var RpcRouter) =
|
||||
router.procs.clear
|
||||
|
||||
proc hasMethod*(router: RpcRouter, methodName: string): bool = router.procs.hasKey(methodName)
|
||||
proc hasMethod*(router: RpcRouter, methodName: string): bool =
|
||||
router.procs.hasKey(methodName)
|
||||
|
||||
func isEmpty(node: JsonNode): bool = node.isNil or node.kind == JNull
|
||||
proc route*(router: RpcRouter, req: RequestRx):
|
||||
Future[ResponseTx] {.gcsafe, async: (raises: []).} =
|
||||
let rpcProc = router.validateRequest(req).valueOr:
|
||||
return wrapError(error, req.id)
|
||||
|
||||
# Json reply wrappers
|
||||
try:
|
||||
let res = await rpcProc(req.params)
|
||||
return wrapReply(res, req.id)
|
||||
except InvalidRequest as err:
|
||||
return wrapError(err.code, err.msg, req.id)
|
||||
except CatchableError as err:
|
||||
let methodName = req.meth.get # this Opt already validated
|
||||
debug "Error occurred within RPC",
|
||||
methodName = methodName, err = err.msg
|
||||
return serverError(methodName & " raised an exception",
|
||||
escapeJson(err.msg).StringOfJson).
|
||||
wrapError(req.id)
|
||||
|
||||
# https://www.jsonrpc.org/specification#response_object
|
||||
proc wrapReply*(id: JsonNode, value: StringOfJson): StringOfJson =
|
||||
# Success response carries version, id and result fields only
|
||||
StringOfJson(
|
||||
"""{"jsonrpc":"2.0","id":$1,"result":$2}""" % [$id, string(value)] & "\r\n")
|
||||
proc wrapErrorAsync*(code: int, msg: string):
|
||||
Future[StringOfJson] {.gcsafe, async: (raises: []).} =
|
||||
return wrapError(code, msg).StringOfJson
|
||||
|
||||
proc wrapError*(code: int, msg: string, id: JsonNode = newJNull(),
|
||||
data: JsonNode = newJNull()): StringOfJson =
|
||||
# Error reply that carries version, id and error object only
|
||||
StringOfJson(
|
||||
"""{"jsonrpc":"2.0","id":$1,"error":{"code":$2,"message":$3,"data":$4}}""" % [
|
||||
$id, $code, escapeJson(msg), $data
|
||||
] & "\r\n")
|
||||
|
||||
proc route*(router: RpcRouter, node: JsonNode): Future[StringOfJson] {.async, gcsafe.} =
|
||||
if node{"jsonrpc"}.getStr() != "2.0":
|
||||
return wrapError(INVALID_REQUEST, "'jsonrpc' missing or invalid")
|
||||
|
||||
let id = node{"id"}
|
||||
if id == nil:
|
||||
return wrapError(INVALID_REQUEST, "'id' missing or invalid")
|
||||
|
||||
let methodName = node{"method"}.getStr()
|
||||
if methodName.len == 0:
|
||||
return wrapError(INVALID_REQUEST, "'method' missing or invalid")
|
||||
|
||||
let rpcProc = router.procs.getOrDefault(methodName)
|
||||
let params = node.getOrDefault("params")
|
||||
|
||||
if rpcProc == nil:
|
||||
return wrapError(METHOD_NOT_FOUND, "'" & methodName & "' is not a registered RPC method", id)
|
||||
else:
|
||||
try:
|
||||
let res = await rpcProc(if params == nil: newJArray() else: params)
|
||||
return wrapReply(id, res)
|
||||
except InvalidRequest as err:
|
||||
return wrapError(err.code, err.msg, id)
|
||||
except CatchableError as err:
|
||||
debug "Error occurred within RPC", methodName = methodName, err = err.msg
|
||||
return wrapError(
|
||||
SERVER_ERROR, methodName & " raised an exception", id, newJString(err.msg))
|
||||
|
||||
proc route*(router: RpcRouter, data: string): Future[string] {.async, gcsafe.} =
|
||||
## Route to RPC from string data. Data is expected to be able to be converted to Json.
|
||||
proc route*(router: RpcRouter, data: string):
|
||||
Future[string] {.gcsafe, async: (raises: []).} =
|
||||
## Route to RPC from string data. Data is expected to be able to be
|
||||
## converted to Json.
|
||||
## Returns string of Json from RPC result/error node
|
||||
when defined(nimHasWarnBareExcept):
|
||||
{.warning[BareExcept]:off.}
|
||||
|
||||
let node =
|
||||
try: parseJson(data)
|
||||
let request =
|
||||
try:
|
||||
JrpcSys.decode(data, RequestRx)
|
||||
except CatchableError as err:
|
||||
return string(wrapError(JSON_PARSE_ERROR, err.msg))
|
||||
return wrapError(JSON_PARSE_ERROR, err.msg)
|
||||
except Exception as err:
|
||||
# TODO https://github.com/status-im/nimbus-eth2/issues/2430
|
||||
return string(wrapError(JSON_PARSE_ERROR, err.msg))
|
||||
return wrapError(JSON_PARSE_ERROR, err.msg)
|
||||
|
||||
let reply =
|
||||
try:
|
||||
let response = await router.route(request)
|
||||
JrpcSys.encode(response)
|
||||
except CatchableError as err:
|
||||
return wrapError(JSON_ENCODE_ERROR, err.msg)
|
||||
except Exception as err:
|
||||
return wrapError(JSON_ENCODE_ERROR, err.msg)
|
||||
|
||||
when defined(nimHasWarnBareExcept):
|
||||
{.warning[BareExcept]:on.}
|
||||
|
||||
return string(await router.route(node))
|
||||
return reply
|
||||
|
||||
proc tryRoute*(router: RpcRouter, data: JsonNode, fut: var Future[StringOfJson]): bool =
|
||||
proc tryRoute*(router: RpcRouter, data: StringOfJson,
|
||||
fut: var Future[StringOfJson]): Result[void, string] =
|
||||
## Route to RPC, returns false if the method or params cannot be found.
|
||||
## Expects json input and returns json output.
|
||||
let
|
||||
jPath = data.getOrDefault(methodField)
|
||||
jParams = data.getOrDefault(paramsField)
|
||||
if jPath.isEmpty or jParams.isEmpty:
|
||||
return false
|
||||
when defined(nimHasWarnBareExcept):
|
||||
{.warning[BareExcept]:off.}
|
||||
{.warning[UnreachableCode]:off.}
|
||||
|
||||
let
|
||||
path = jPath.getStr
|
||||
rpc = router.procs.getOrDefault(path)
|
||||
if rpc != nil:
|
||||
fut = rpc(jParams)
|
||||
return true
|
||||
try:
|
||||
let req = JrpcSys.decode(data.string, RequestRx)
|
||||
|
||||
proc hasReturnType(params: NimNode): bool =
|
||||
if params != nil and params.len > 0 and params[0] != nil and
|
||||
params[0].kind != nnkEmpty:
|
||||
result = true
|
||||
if req.jsonrpc.isNone:
|
||||
return err("`jsonrpc` missing or invalid")
|
||||
|
||||
macro rpc*(server: RpcRouter, path: string, body: untyped): untyped =
|
||||
if req.meth.isNone:
|
||||
return err("`method` missing or invalid")
|
||||
|
||||
let rpc = router.procs.getOrDefault(req.meth.get)
|
||||
if rpc.isNil:
|
||||
return err("rpc method not found: " & req.meth.get)
|
||||
|
||||
fut = rpc(req.params)
|
||||
return ok()
|
||||
|
||||
except CatchableError as ex:
|
||||
return err(ex.msg)
|
||||
except Exception as ex:
|
||||
return err(ex.msg)
|
||||
|
||||
when defined(nimHasWarnBareExcept):
|
||||
{.warning[BareExcept]:on.}
|
||||
{.warning[UnreachableCode]:on.}
|
||||
|
||||
macro rpc*(server: RpcRouter, path: static[string], body: untyped): untyped =
|
||||
## Define a remote procedure call.
|
||||
## Input and return parameters are defined using the ``do`` notation.
|
||||
## For example:
|
||||
@ -146,41 +215,17 @@ macro rpc*(server: RpcRouter, path: string, body: untyped): untyped =
|
||||
## ```
|
||||
## Input parameters are automatically marshalled from json to Nim types,
|
||||
## and output parameters are automatically marshalled to json for transport.
|
||||
result = newStmtList()
|
||||
let
|
||||
parameters = body.findChild(it.kind == nnkFormalParams)
|
||||
# all remote calls have a single parameter: `params: JsonNode`
|
||||
paramsIdent = newIdentNode"params"
|
||||
rpcProcImpl = genSym(nskProc)
|
||||
rpcProcWrapper = genSym(nskProc)
|
||||
var
|
||||
setup = jsonToNim(parameters, paramsIdent)
|
||||
params = body.findChild(it.kind == nnkFormalParams)
|
||||
procBody = if body.kind == nnkStmtList: body else: body.body
|
||||
procWrapper = genSym(nskProc, $path & "_rpcWrapper")
|
||||
|
||||
let ReturnType = if parameters.hasReturnType: parameters[0]
|
||||
else: ident "JsonNode"
|
||||
|
||||
# delegate async proc allows return and setting of result as native type
|
||||
result.add quote do:
|
||||
proc `rpcProcImpl`(`paramsIdent`: JsonNode): Future[`ReturnType`] {.async.} =
|
||||
`setup`
|
||||
`procBody`
|
||||
|
||||
let
|
||||
awaitedResult = ident "awaitedResult"
|
||||
doEncode = quote do: encode(JsonRpc, `awaitedResult`)
|
||||
maybeWrap =
|
||||
if ReturnType == ident"StringOfJson": doEncode
|
||||
else: ident"StringOfJson".newCall doEncode
|
||||
result = wrapServerHandler($path, params, procBody, procWrapper)
|
||||
|
||||
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`)
|
||||
`server`.register(`path`, `procWrapper`)
|
||||
|
||||
when defined(nimDumpRpcs):
|
||||
echo "\n", path, ": ", result.repr
|
||||
|
||||
{.pop.}
|
||||
|
||||
@ -7,12 +7,11 @@
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
{.push raises: [Defect].}
|
||||
|
||||
import
|
||||
pkg/websock/websock,
|
||||
./servers/[httpserver],
|
||||
./clients/[httpclient, websocketclient]
|
||||
./clients/[httpclient, websocketclient],
|
||||
./private/jrpc_sys
|
||||
|
||||
type
|
||||
ClientKind* = enum
|
||||
@ -40,6 +39,8 @@ type
|
||||
compression*: bool
|
||||
flags*: set[TLSFlags]
|
||||
|
||||
{.push gcsafe, raises: [].}
|
||||
|
||||
# TODO Add validations that provided uri-s are correct https/wss uri and retrun
|
||||
# Result[string, ClientConfig]
|
||||
proc getHttpClientConfig*(uri: string): ClientConfig =
|
||||
@ -53,9 +54,9 @@ proc getWebSocketClientConfig*(
|
||||
ClientConfig(kind: WebSocket, wsUri: uri, compression: compression, flags: flags)
|
||||
|
||||
proc proxyCall(client: RpcClient, name: string): RpcProc =
|
||||
return proc (params: JsonNode): Future[StringOfJson] {.async.} =
|
||||
let res = await client.call(name, params)
|
||||
return StringOfJson($res)
|
||||
return proc (params: RequestParamsRx): Future[StringOfJson] {.gcsafe, async.} =
|
||||
let res = await client.call(name, params.toTx)
|
||||
return res
|
||||
|
||||
proc getClient*(proxy: RpcProxy): RpcClient =
|
||||
case proxy.kind
|
||||
@ -85,14 +86,14 @@ proc new*(
|
||||
listenAddresses: openArray[TransportAddress],
|
||||
cfg: ClientConfig,
|
||||
authHooks: seq[HttpAuthHook] = @[]
|
||||
): T {.raises: [Defect, CatchableError].} =
|
||||
): T {.raises: [CatchableError].} =
|
||||
RpcProxy.new(newRpcHttpServer(listenAddresses, RpcRouter.init(), authHooks), cfg)
|
||||
|
||||
proc new*(
|
||||
T: type RpcProxy,
|
||||
listenAddresses: openArray[string],
|
||||
cfg: ClientConfig,
|
||||
authHooks: seq[HttpAuthHook] = @[]): T {.raises: [Defect, CatchableError].} =
|
||||
authHooks: seq[HttpAuthHook] = @[]): T {.raises: [CatchableError].} =
|
||||
RpcProxy.new(newRpcHttpServer(listenAddresses, RpcRouter.init(), authHooks), cfg)
|
||||
|
||||
proc connectToProxy(proxy: RpcProxy): Future[void] =
|
||||
@ -125,3 +126,5 @@ proc stop*(proxy: RpcProxy) {.async.} =
|
||||
|
||||
proc closeWait*(proxy: RpcProxy) {.async.} =
|
||||
await proxy.rpcHttpServer.closeWait()
|
||||
|
||||
{.pop.}
|
||||
|
||||
@ -8,21 +8,35 @@
|
||||
# those terms.
|
||||
|
||||
import
|
||||
std/tables,
|
||||
std/json,
|
||||
chronos,
|
||||
./router,
|
||||
./jsonmarshal
|
||||
./private/jrpc_conv,
|
||||
./private/jrpc_sys,
|
||||
./private/shared_wrapper,
|
||||
./private/errors
|
||||
|
||||
export chronos, jsonmarshal, router
|
||||
export
|
||||
chronos,
|
||||
jrpc_conv,
|
||||
router
|
||||
|
||||
type
|
||||
RpcServer* = ref object of RootRef
|
||||
router*: RpcRouter
|
||||
|
||||
proc new(T: type RpcServer): T =
|
||||
{.push gcsafe, raises: [].}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Constructors
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
proc new*(T: type RpcServer): T =
|
||||
T(router: RpcRouter.init())
|
||||
|
||||
proc newRpcServer*(): RpcServer {.deprecated.} = RpcServer.new()
|
||||
# ------------------------------------------------------------------------------
|
||||
# Public functions
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
template rpc*(server: RpcServer, path: string, body: untyped): untyped =
|
||||
server.router.rpc(path, body)
|
||||
@ -32,8 +46,23 @@ template hasMethod*(server: RpcServer, methodName: string): bool =
|
||||
|
||||
proc executeMethod*(server: RpcServer,
|
||||
methodName: string,
|
||||
args: JsonNode): Future[StringOfJson] =
|
||||
server.router.procs[methodName](args)
|
||||
params: RequestParamsTx): Future[StringOfJson]
|
||||
{.gcsafe, raises: [JsonRpcError].} =
|
||||
|
||||
let
|
||||
req = requestTx(methodName, params, RequestId(kind: riNumber, num: 0))
|
||||
reqData = JrpcSys.encode(req).JsonString
|
||||
|
||||
server.router.tryRoute(reqData, result).isOkOr:
|
||||
raise newException(JsonRpcError, error)
|
||||
|
||||
proc executeMethod*(server: RpcServer,
|
||||
methodName: string,
|
||||
args: JsonNode): Future[StringOfJson]
|
||||
{.gcsafe, raises: [JsonRpcError].} =
|
||||
|
||||
let params = paramsTx(args)
|
||||
server.executeMethod(methodName, params)
|
||||
|
||||
# Wrapper for message processing
|
||||
|
||||
@ -42,10 +71,12 @@ proc route*(server: RpcServer, line: string): Future[string] {.gcsafe.} =
|
||||
|
||||
# Server registration
|
||||
|
||||
proc register*(server: RpcServer, name: string, rpc: RpcProc) =
|
||||
proc register*(server: RpcServer, name: string, rpc: RpcProc) {.gcsafe, raises: [CatchableError].} =
|
||||
## Add a name/code pair to the RPC server.
|
||||
server.router.register(name, rpc)
|
||||
|
||||
proc unRegisterAll*(server: RpcServer) =
|
||||
# Remove all remote procedure calls from this server.
|
||||
server.router.clear
|
||||
|
||||
{.pop.}
|
||||
|
||||
@ -11,9 +11,11 @@ import
|
||||
stew/byteutils,
|
||||
chronicles, httputils, chronos,
|
||||
chronos/apps/http/[httpserver, shttpserver],
|
||||
".."/[errors, server]
|
||||
../private/errors,
|
||||
../server
|
||||
|
||||
export server, shttpserver
|
||||
export
|
||||
server, shttpserver
|
||||
|
||||
logScope:
|
||||
topics = "JSONRPC-HTTP-SERVER"
|
||||
@ -36,41 +38,52 @@ type
|
||||
httpServers: seq[HttpServerRef]
|
||||
authHooks: seq[HttpAuthHook]
|
||||
|
||||
proc processClientRpc(rpcServer: RpcHttpServer): HttpProcessCallback =
|
||||
return proc (req: RequestFence): Future[HttpResponseRef] {.async.} =
|
||||
if req.isOk():
|
||||
let request = req.get()
|
||||
proc processClientRpc(rpcServer: RpcHttpServer): HttpProcessCallback2 =
|
||||
return proc (req: RequestFence): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} =
|
||||
if not req.isOk():
|
||||
return defaultResponse()
|
||||
|
||||
# if hook result is not nil,
|
||||
# it means we should return immediately
|
||||
let request = req.get()
|
||||
# if hook result is not nil,
|
||||
# it means we should return immediately
|
||||
try:
|
||||
for hook in rpcServer.authHooks:
|
||||
let res = await hook(request)
|
||||
if not res.isNil:
|
||||
return res
|
||||
except CatchableError as exc:
|
||||
error "Internal error while processing JSON-RPC hook", msg=exc.msg
|
||||
try:
|
||||
return await request.respond(
|
||||
Http503,
|
||||
"Internal error while processing JSON-RPC hook: " & exc.msg)
|
||||
except HttpWriteError as exc:
|
||||
error "Something error", msg=exc.msg
|
||||
return defaultResponse()
|
||||
|
||||
let
|
||||
headers = HttpTable.init([("Content-Type",
|
||||
"application/json; charset=utf-8")])
|
||||
try:
|
||||
let
|
||||
body = await request.getBody()
|
||||
headers = HttpTable.init([("Content-Type",
|
||||
"application/json; charset=utf-8")])
|
||||
|
||||
data =
|
||||
try:
|
||||
await rpcServer.route(string.fromBytes(body))
|
||||
except CancelledError as exc:
|
||||
raise exc
|
||||
except CatchableError as exc:
|
||||
debug "Internal error while processing JSON-RPC call"
|
||||
return await request.respond(
|
||||
Http503,
|
||||
"Internal error while processing JSON-RPC call: " & exc.msg,
|
||||
headers)
|
||||
|
||||
data = await rpcServer.route(string.fromBytes(body))
|
||||
res = await request.respond(Http200, data, headers)
|
||||
|
||||
trace "JSON-RPC result has been sent"
|
||||
return res
|
||||
else:
|
||||
return dumbResponse()
|
||||
except CancelledError as exc:
|
||||
raise exc
|
||||
except CatchableError as exc:
|
||||
debug "Internal error while processing JSON-RPC call"
|
||||
try:
|
||||
return await request.respond(
|
||||
Http503,
|
||||
"Internal error while processing JSON-RPC call: " & exc.msg)
|
||||
except HttpWriteError as exc:
|
||||
error "Something error", msg=exc.msg
|
||||
return defaultResponse()
|
||||
|
||||
proc addHttpServer*(
|
||||
rpcServer: RpcHttpServer,
|
||||
|
||||
@ -10,7 +10,8 @@
|
||||
import
|
||||
chronicles,
|
||||
json_serialization/std/net,
|
||||
".."/[errors, server]
|
||||
../private/errors,
|
||||
../server
|
||||
|
||||
export errors, server
|
||||
|
||||
@ -18,26 +19,25 @@ type
|
||||
RpcSocketServer* = ref object of RpcServer
|
||||
servers: seq[StreamServer]
|
||||
|
||||
proc sendError*[T](transport: T, code: int, msg: string, id: JsonNode,
|
||||
data: JsonNode = newJNull()) {.async.} =
|
||||
## Send error message to client
|
||||
let error = wrapError(code, msg, id, data)
|
||||
result = transport.write(string wrapReply(id, StringOfJson("null"), error))
|
||||
|
||||
proc processClient(server: StreamServer, transport: StreamTransport) {.async, gcsafe.} =
|
||||
proc processClient(server: StreamServer, transport: StreamTransport) {.async: (raises: []), gcsafe.} =
|
||||
## Process transport data to the RPC server
|
||||
var rpc = getUserData[RpcSocketServer](server)
|
||||
while true:
|
||||
var
|
||||
value = await transport.readLine(defaultMaxRequestLength)
|
||||
if value == "":
|
||||
await transport.closeWait()
|
||||
break
|
||||
try:
|
||||
var rpc = getUserData[RpcSocketServer](server)
|
||||
while true:
|
||||
var
|
||||
value = await transport.readLine(defaultMaxRequestLength)
|
||||
if value == "":
|
||||
await transport.closeWait()
|
||||
break
|
||||
|
||||
debug "Processing message", address = transport.remoteAddress(), line = value
|
||||
debug "Processing message", address = transport.remoteAddress(), line = value
|
||||
|
||||
let res = await rpc.route(value)
|
||||
discard await transport.write(res)
|
||||
let res = await rpc.route(value)
|
||||
discard await transport.write(res & "\r\n")
|
||||
except TransportError as ex:
|
||||
error "Transport closed during processing client", msg=ex.msg
|
||||
except CatchableError as ex:
|
||||
error "Error occured during processing client", msg=ex.msg
|
||||
|
||||
# Utility functions for setting up servers using stream transport addresses
|
||||
|
||||
|
||||
@ -1,3 +1,12 @@
|
||||
# json-rpc
|
||||
# Copyright (c) 2019-2023 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
# at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
{. warning[UnusedImport]:off .}
|
||||
|
||||
import
|
||||
@ -6,4 +15,7 @@ import
|
||||
testhttp,
|
||||
testserverclient,
|
||||
testproxy,
|
||||
testhook
|
||||
testhook,
|
||||
test_jrpc_sys,
|
||||
test_router_rpc,
|
||||
test_callsigs
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
import
|
||||
../json_rpc/router
|
||||
|
||||
converter toStr*(value: distinct (string|StringOfJson)): string = string(value)
|
||||
|
||||
template `==`*(a: StringOfJson, b: JsonNode): bool =
|
||||
parseJson(string a) == b
|
||||
|
||||
template `==`*(a: JsonNode, b: StringOfJson): bool =
|
||||
a == parseJson(string b)
|
||||
|
||||
@ -41,23 +41,14 @@ proc eth_getCompilers(): seq[string]
|
||||
proc eth_compileLLL(): seq[byte]
|
||||
proc eth_compileSolidity(): seq[byte]
|
||||
proc eth_compileSerpent(): seq[byte]
|
||||
proc eth_newFilter(filterOptions: FilterOptions): int
|
||||
proc eth_newBlockFilter(): int
|
||||
proc eth_newPendingTransactionFilter(): int
|
||||
proc eth_uninstallFilter(filterId: int): bool
|
||||
proc eth_getFilterChanges(filterId: int): seq[LogObject]
|
||||
proc eth_getFilterLogs(filterId: int): seq[LogObject]
|
||||
proc eth_getLogs(filterOptions: FilterOptions): seq[LogObject]
|
||||
proc eth_getWork(): seq[UInt256]
|
||||
proc eth_submitWork(nonce: int64, powHash: Uint256, mixDigest: Uint256): bool
|
||||
proc eth_submitHashrate(hashRate: UInt256, id: Uint256): bool
|
||||
proc shh_post(): string
|
||||
proc shh_version(message: WhisperPost): bool
|
||||
proc shh_newIdentity(): array[60, byte]
|
||||
proc shh_hasIdentity(identity: array[60, byte]): bool
|
||||
proc shh_newGroup(): array[60, byte]
|
||||
proc shh_addToGroup(identity: array[60, byte]): bool
|
||||
proc shh_newFilter(filterOptions: FilterOptions, to: array[60, byte], topics: seq[UInt256]): int
|
||||
proc shh_uninstallFilter(id: int): bool
|
||||
proc shh_getFilterChanges(id: int): seq[WhisperMessage]
|
||||
proc shh_getMessages(id: int): seq[WhisperMessage]
|
||||
@ -1,9 +1,14 @@
|
||||
import
|
||||
../../json_rpc/private/errors
|
||||
|
||||
type
|
||||
HexQuantityStr* = distinct string
|
||||
HexDataStr* = distinct string
|
||||
|
||||
# Hex validation
|
||||
|
||||
{.push gcsafe, raises: [].}
|
||||
|
||||
template stripLeadingZeros(value: string): string =
|
||||
var cidx = 0
|
||||
# ignore the last character so we retain '0' on zero value
|
||||
@ -61,53 +66,55 @@ template hexQuantityStr*(value: string): HexQuantityStr = value.HexQuantityStr
|
||||
# Converters
|
||||
|
||||
import json
|
||||
import ../json_rpc/jsonmarshal
|
||||
import ../../json_rpc/private/jrpc_conv
|
||||
|
||||
proc `%`*(value: HexDataStr): JsonNode =
|
||||
proc `%`*(value: HexDataStr): JsonNode {.gcsafe, raises: [JsonRpcError].} =
|
||||
if not value.validate:
|
||||
raise newException(ValueError, "HexDataStr: Invalid hex for Ethereum: " & value.string)
|
||||
raise newException(JsonRpcError, "HexDataStr: Invalid hex for Ethereum: " & value.string)
|
||||
else:
|
||||
result = %(value.string)
|
||||
|
||||
proc `%`*(value: HexQuantityStr): JsonNode =
|
||||
proc `%`*(value: HexQuantityStr): JsonNode {.gcsafe, raises: [JsonRpcError].} =
|
||||
if not value.validate:
|
||||
raise newException(ValueError, "HexQuantityStr: Invalid hex for Ethereum: " & value.string)
|
||||
raise newException(JsonRpcError, "HexQuantityStr: Invalid hex for Ethereum: " & value.string)
|
||||
else:
|
||||
result = %(value.string)
|
||||
|
||||
proc writeValue*(w: var JsonWriter[JsonRpc], val: HexDataStr) {.raises: [IOError].} =
|
||||
proc writeValue*(w: var JsonWriter[JrpcConv], val: HexDataStr) {.raises: [IOError].} =
|
||||
writeValue(w, val.string)
|
||||
|
||||
proc writeValue*(w: var JsonWriter[JsonRpc], val: HexQuantityStr) {.raises: [IOError].} =
|
||||
proc writeValue*(w: var JsonWriter[JrpcConv], val: HexQuantityStr) {.raises: [IOError].} =
|
||||
writeValue(w, $val.string)
|
||||
|
||||
proc readValue*(r: var JsonReader[JsonRpc], v: var HexDataStr) =
|
||||
proc readValue*(r: var JsonReader[JrpcConv], v: var HexDataStr) {.gcsafe, raises: [JsonReaderError].} =
|
||||
# Note that '0x' is stripped after validation
|
||||
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 & "\"")
|
||||
raise newException(JsonRpcError, "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) =
|
||||
proc readValue*(r: var JsonReader[JrpcConv], v: var HexQuantityStr) {.gcsafe, raises: [JsonReaderError].} =
|
||||
# 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 & "\"")
|
||||
raise newException(JsonRpcError, "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)
|
||||
|
||||
{.pop.}
|
||||
|
||||
# testing
|
||||
|
||||
when isMainModule:
|
||||
import unittest
|
||||
suite "Hex quantity":
|
||||
test "Empty string":
|
||||
expect ValueError:
|
||||
expect JsonRpcError:
|
||||
let
|
||||
source = ""
|
||||
x = hexQuantityStr source
|
||||
@ -123,17 +130,17 @@ when isMainModule:
|
||||
x = hexQuantityStr"0x123"
|
||||
check %x == %source
|
||||
test "Missing header":
|
||||
expect ValueError:
|
||||
expect JsonRpcError:
|
||||
let
|
||||
source = "1234"
|
||||
x = hexQuantityStr source
|
||||
check %x != %source
|
||||
expect ValueError:
|
||||
expect JsonRpcError:
|
||||
let
|
||||
source = "01234"
|
||||
x = hexQuantityStr source
|
||||
check %x != %source
|
||||
expect ValueError:
|
||||
expect JsonRpcError:
|
||||
let
|
||||
source = "x1234"
|
||||
x = hexQuantityStr source
|
||||
@ -146,23 +153,23 @@ when isMainModule:
|
||||
x = hexDataStr source
|
||||
check %x == %source
|
||||
test "Odd length":
|
||||
expect ValueError:
|
||||
expect JsonRpcError:
|
||||
let
|
||||
source = "0x123"
|
||||
x = hexDataStr source
|
||||
check %x != %source
|
||||
test "Missing header":
|
||||
expect ValueError:
|
||||
expect JsonRpcError:
|
||||
let
|
||||
source = "1234"
|
||||
x = hexDataStr source
|
||||
check %x != %source
|
||||
expect ValueError:
|
||||
expect JsonRpcError:
|
||||
let
|
||||
source = "01234"
|
||||
x = hexDataStr source
|
||||
check %x != %source
|
||||
expect ValueError:
|
||||
expect JsonRpcError:
|
||||
let
|
||||
source = "x1234"
|
||||
x = hexDataStr source
|
||||
@ -1,6 +1,9 @@
|
||||
import
|
||||
nimcrypto, stint,
|
||||
ethtypes, ethhexstrings, stintjson, ../json_rpc/rpcserver
|
||||
./ethtypes,
|
||||
./ethhexstrings,
|
||||
./stintjson,
|
||||
../../json_rpc/rpcserver
|
||||
|
||||
#[
|
||||
For details on available RPC calls, see: https://github.com/ethereum/wiki/wiki/JSON-RPC
|
||||
@ -28,6 +31,22 @@ import
|
||||
specified once without invoking `reset`.
|
||||
]#
|
||||
|
||||
EthSend.useDefaultSerializationIn JrpcConv
|
||||
EthCall.useDefaultSerializationIn JrpcConv
|
||||
TransactionObject.useDefaultSerializationIn JrpcConv
|
||||
ReceiptObject.useDefaultSerializationIn JrpcConv
|
||||
FilterOptions.useDefaultSerializationIn JrpcConv
|
||||
FilterData.useDefaultSerializationIn JrpcConv
|
||||
LogObject.useDefaultSerializationIn JrpcConv
|
||||
WhisperPost.useDefaultSerializationIn JrpcConv
|
||||
WhisperMessage.useDefaultSerializationIn JrpcConv
|
||||
|
||||
template derefType(): untyped =
|
||||
var x: BlockObject
|
||||
typeof(x[])
|
||||
|
||||
useDefaultSerializationIn(derefType(), JrpcConv)
|
||||
|
||||
proc addEthRpcs*(server: RpcServer) =
|
||||
server.rpc("web3_clientVersion") do() -> string:
|
||||
## Returns the current client version.
|
||||
@ -51,10 +70,10 @@ proc addEthRpcs*(server: RpcServer) =
|
||||
## "3": Ropsten Testnet
|
||||
## "4": Rinkeby Testnet
|
||||
## "42": Kovan Testnet
|
||||
#[ Note, See:
|
||||
https://github.com/ethereum/interfaces/issues/6
|
||||
https://github.com/ethereum/EIPs/issues/611
|
||||
]#
|
||||
## Note, See:
|
||||
## https://github.com/ethereum/interfaces/issues/6
|
||||
## https://github.com/ethereum/EIPs/issues/611
|
||||
|
||||
result = ""
|
||||
|
||||
server.rpc("net_listening") do() -> bool:
|
||||
@ -449,4 +468,3 @@ proc addEthRpcs*(server: RpcServer) =
|
||||
## id: the filter id.
|
||||
## Returns a list of messages received since last poll.
|
||||
discard
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# json-rpc
|
||||
# Copyright (c) 2019-2023 Status Research & Development GmbH
|
||||
# Copyright (c) 2024 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
@ -7,6 +7,4 @@
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
import server
|
||||
import servers/[socketserver, shttpserver]
|
||||
export server, socketserver, shttpserver
|
||||
proc shh_uninstallFilter(id: int): bool
|
||||
19
tests/private/helpers.nim
Normal file
19
tests/private/helpers.nim
Normal file
@ -0,0 +1,19 @@
|
||||
# json-rpc
|
||||
# Copyright (c) 2019-2023 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
# at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
import
|
||||
../../json_rpc/router
|
||||
|
||||
converter toStr*(value: distinct (string|StringOfJson)): string = string(value)
|
||||
|
||||
template `==`*(a: StringOfJson, b: JsonNode): bool =
|
||||
parseJson(string a) == b
|
||||
|
||||
template `==`*(a: JsonNode, b: StringOfJson): bool =
|
||||
a == parseJson(string b)
|
||||
@ -1,4 +1,9 @@
|
||||
import stint, ../json_rpc/jsonmarshal
|
||||
import
|
||||
std/json,
|
||||
stint,
|
||||
../../json_rpc/private/jrpc_conv
|
||||
|
||||
{.push gcsafe, raises: [].}
|
||||
|
||||
template stintStr(n: UInt256|Int256): JsonNode =
|
||||
var s = n.toHex
|
||||
@ -10,13 +15,16 @@ proc `%`*(n: UInt256): JsonNode = n.stintStr
|
||||
|
||||
proc `%`*(n: Int256): JsonNode = n.stintStr
|
||||
|
||||
proc writeValue*(w: var JsonWriter[JsonRpc], val: UInt256) =
|
||||
proc writeValue*(w: var JsonWriter[JrpcConv], val: UInt256)
|
||||
{.gcsafe, raises: [IOError].} =
|
||||
writeValue(w, val.stintStr)
|
||||
|
||||
proc writeValue*(w: var JsonWriter[JsonRpc], val: ref UInt256) =
|
||||
proc writeValue*(w: var JsonWriter[JrpcConv], val: ref UInt256)
|
||||
{.gcsafe, raises: [IOError].} =
|
||||
writeValue(w, val[].stintStr)
|
||||
|
||||
proc readValue*(r: var JsonReader[JsonRpc], v: var UInt256) =
|
||||
proc readValue*(r: var JsonReader[JrpcConv], v: var UInt256)
|
||||
{.gcsafe, raises: [JsonReaderError].} =
|
||||
## Allows UInt256 to be passed as a json string.
|
||||
## Expects base 16 string, starting with "0x".
|
||||
try:
|
||||
@ -27,8 +35,10 @@ proc readValue*(r: var JsonReader[JsonRpc], v: var UInt256) =
|
||||
except Exception as err:
|
||||
r.raiseUnexpectedValue("Error deserializing for '" & $v.type & "' stream: " & err.msg)
|
||||
|
||||
proc readValue*(r: var JsonReader[JsonRpc], v: var ref UInt256) =
|
||||
proc readValue*(r: var JsonReader[JrpcConv], v: var ref UInt256)
|
||||
{.gcsafe, raises: [JsonReaderError].} =
|
||||
## Allows ref UInt256 to be passed as a json string.
|
||||
## Expects base 16 string, starting with "0x".
|
||||
readValue(r, v[])
|
||||
|
||||
{.pop.}
|
||||
26
tests/test_callsigs.nim
Normal file
26
tests/test_callsigs.nim
Normal file
@ -0,0 +1,26 @@
|
||||
# json-rpc
|
||||
# Copyright (c) 2023 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
# at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
import
|
||||
../json_rpc/client
|
||||
|
||||
from os import getCurrentDir, DirSep
|
||||
from strutils import rsplit
|
||||
template sourceDir: string = currentSourcePath.rsplit(DirSep, 1)[0]
|
||||
|
||||
createRpcSigs(RpcClient, sourceDir & "/private/file_callsigs.nim")
|
||||
|
||||
createSingleRpcSig(RpcClient, "bottle"):
|
||||
proc get_Bottle(id: int): bool
|
||||
|
||||
createRpcSigsFromNim(RpcClient):
|
||||
proc get_Banana(id: int): bool
|
||||
proc get_Combo(id, index: int, name: string): bool
|
||||
proc get_Name(id: int): string
|
||||
proc getJsonString(name: string): JsonString
|
||||
246
tests/test_jrpc_sys.nim
Normal file
246
tests/test_jrpc_sys.nim
Normal file
@ -0,0 +1,246 @@
|
||||
# json-rpc
|
||||
# Copyright (c) 2023 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
# at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
import
|
||||
unittest2,
|
||||
../json_rpc/private/jrpc_sys
|
||||
|
||||
func id(): RequestId =
|
||||
RequestId(kind: riNull)
|
||||
|
||||
func id(x: string): RequestId =
|
||||
RequestId(kind: riString, str: x)
|
||||
|
||||
func id(x: int): RequestId =
|
||||
RequestId(kind: riNumber, num: x)
|
||||
|
||||
func req(id: int or string, meth: string, params: RequestParamsTx): RequestTx =
|
||||
RequestTx(
|
||||
id: Opt.some(id(id)),
|
||||
`method`: meth,
|
||||
params: params
|
||||
)
|
||||
|
||||
func reqNull(meth: string, params: RequestParamsTx): RequestTx =
|
||||
RequestTx(
|
||||
id: Opt.some(id()),
|
||||
`method`: meth,
|
||||
params: params
|
||||
)
|
||||
|
||||
func reqNoId(meth: string, params: RequestParamsTx): RequestTx =
|
||||
RequestTx(
|
||||
`method`: meth,
|
||||
params: params
|
||||
)
|
||||
|
||||
func toParams(params: varargs[(string, JsonString)]): seq[ParamDescNamed] =
|
||||
for x in params:
|
||||
result.add ParamDescNamed(name:x[0], value:x[1])
|
||||
|
||||
func namedPar(params: varargs[(string, JsonString)]): RequestParamsTx =
|
||||
RequestParamsTx(
|
||||
kind: rpNamed,
|
||||
named: toParams(params)
|
||||
)
|
||||
|
||||
func posPar(params: varargs[JsonString]): RequestParamsTx =
|
||||
RequestParamsTx(
|
||||
kind: rpPositional,
|
||||
positional: @params
|
||||
)
|
||||
|
||||
func res(id: int or string, r: JsonString): ResponseTx =
|
||||
ResponseTx(
|
||||
id: id(id),
|
||||
kind: rkResult,
|
||||
result: r,
|
||||
)
|
||||
|
||||
func res(id: int or string, err: ResponseError): ResponseTx =
|
||||
ResponseTx(
|
||||
id: id(id),
|
||||
kind: rkError,
|
||||
error: err,
|
||||
)
|
||||
|
||||
func resErr(code: int, msg: string): ResponseError =
|
||||
ResponseError(
|
||||
code: code,
|
||||
message: msg,
|
||||
)
|
||||
|
||||
func resErr(code: int, msg: string, data: JsonString): ResponseError =
|
||||
ResponseError(
|
||||
code: code,
|
||||
message: msg,
|
||||
data: Opt.some(data)
|
||||
)
|
||||
|
||||
func reqBatch(args: varargs[RequestTx]): RequestBatchTx =
|
||||
if args.len == 1:
|
||||
RequestBatchTx(
|
||||
kind: rbkSingle, single: args[0]
|
||||
)
|
||||
else:
|
||||
RequestBatchTx(
|
||||
kind: rbkMany, many: @args
|
||||
)
|
||||
|
||||
func resBatch(args: varargs[ResponseTx]): ResponseBatchTx =
|
||||
if args.len == 1:
|
||||
ResponseBatchTx(
|
||||
kind: rbkSingle, single: args[0]
|
||||
)
|
||||
else:
|
||||
ResponseBatchTx(
|
||||
kind: rbkMany, many: @args
|
||||
)
|
||||
|
||||
suite "jrpc_sys conversion":
|
||||
let np1 = namedPar(("banana", JsonString("true")), ("apple", JsonString("123")))
|
||||
let pp1 = posPar(JsonString("123"), JsonString("true"), JsonString("\"hello\""))
|
||||
|
||||
test "RequestTx -> RequestRx: id(int), positional":
|
||||
let tx = req(123, "int_positional", pp1)
|
||||
let txBytes = JrpcSys.encode(tx)
|
||||
let rx = JrpcSys.decode(txBytes, RequestRx)
|
||||
|
||||
check:
|
||||
rx.jsonrpc.isSome
|
||||
rx.id.kind == riNumber
|
||||
rx.id.num == 123
|
||||
rx.meth.get == "int_positional"
|
||||
rx.params.kind == rpPositional
|
||||
rx.params.positional.len == 3
|
||||
rx.params.positional[0].kind == JsonValueKind.Number
|
||||
rx.params.positional[1].kind == JsonValueKind.Bool
|
||||
rx.params.positional[2].kind == JsonValueKind.String
|
||||
|
||||
test "RequestTx -> RequestRx: id(string), named":
|
||||
let tx = req("word", "string_named", np1)
|
||||
let txBytes = JrpcSys.encode(tx)
|
||||
let rx = JrpcSys.decode(txBytes, RequestRx)
|
||||
|
||||
check:
|
||||
rx.jsonrpc.isSome
|
||||
rx.id.kind == riString
|
||||
rx.id.str == "word"
|
||||
rx.meth.get == "string_named"
|
||||
rx.params.kind == rpNamed
|
||||
rx.params.named[0].name == "banana"
|
||||
rx.params.named[0].value.string == "true"
|
||||
rx.params.named[1].name == "apple"
|
||||
rx.params.named[1].value.string == "123"
|
||||
|
||||
test "RequestTx -> RequestRx: id(null), named":
|
||||
let tx = reqNull("null_named", np1)
|
||||
let txBytes = JrpcSys.encode(tx)
|
||||
let rx = JrpcSys.decode(txBytes, RequestRx)
|
||||
|
||||
check:
|
||||
rx.jsonrpc.isSome
|
||||
rx.id.kind == riNull
|
||||
rx.meth.get == "null_named"
|
||||
rx.params.kind == rpNamed
|
||||
rx.params.named[0].name == "banana"
|
||||
rx.params.named[0].value.string == "true"
|
||||
rx.params.named[1].name == "apple"
|
||||
rx.params.named[1].value.string == "123"
|
||||
|
||||
test "RequestTx -> RequestRx: none, none":
|
||||
let tx = reqNoId("none_positional", posPar())
|
||||
let txBytes = JrpcSys.encode(tx)
|
||||
let rx = JrpcSys.decode(txBytes, RequestRx)
|
||||
|
||||
check:
|
||||
rx.jsonrpc.isSome
|
||||
rx.id.kind == riNull
|
||||
rx.meth.get == "none_positional"
|
||||
rx.params.kind == rpPositional
|
||||
rx.params.positional.len == 0
|
||||
|
||||
test "ResponseTx -> ResponseRx: id(int), res":
|
||||
let tx = res(777, JsonString("true"))
|
||||
let txBytes = JrpcSys.encode(tx)
|
||||
let rx = JrpcSys.decode(txBytes, ResponseRx)
|
||||
check:
|
||||
rx.jsonrpc.isSome
|
||||
rx.id.isSome
|
||||
rx.id.get.num == 777
|
||||
rx.result.isSome
|
||||
rx.result.get == JsonString("true")
|
||||
rx.error.isNone
|
||||
|
||||
test "ResponseTx -> ResponseRx: id(string), err: nodata":
|
||||
let tx = res("gum", resErr(999, "fatal"))
|
||||
let txBytes = JrpcSys.encode(tx)
|
||||
let rx = JrpcSys.decode(txBytes, ResponseRx)
|
||||
check:
|
||||
rx.jsonrpc.isSome
|
||||
rx.id.isSome
|
||||
rx.id.get.str == "gum"
|
||||
rx.result.isNone
|
||||
rx.error.isSome
|
||||
rx.error.get.code == 999
|
||||
rx.error.get.message == "fatal"
|
||||
rx.error.get.data.isNone
|
||||
|
||||
test "ResponseTx -> ResponseRx: id(string), err: some data":
|
||||
let tx = res("gum", resErr(999, "fatal", JsonString("888.999")))
|
||||
let txBytes = JrpcSys.encode(tx)
|
||||
let rx = JrpcSys.decode(txBytes, ResponseRx)
|
||||
check:
|
||||
rx.jsonrpc.isSome
|
||||
rx.id.isSome
|
||||
rx.id.get.str == "gum"
|
||||
rx.result.isNone
|
||||
rx.error.isSome
|
||||
rx.error.get.code == 999
|
||||
rx.error.get.message == "fatal"
|
||||
rx.error.get.data.get == JsonString("888.999")
|
||||
|
||||
test "RequestBatchTx -> RequestBatchRx: single":
|
||||
let tx1 = req(123, "int_positional", pp1)
|
||||
let tx = reqBatch(tx1)
|
||||
let txBytes = JrpcSys.encode(tx)
|
||||
let rx = JrpcSys.decode(txBytes, RequestBatchRx)
|
||||
check:
|
||||
rx.kind == rbkSingle
|
||||
|
||||
test "RequestBatchTx -> RequestBatchRx: many":
|
||||
let tx1 = req(123, "int_positional", pp1)
|
||||
let tx2 = req("word", "string_named", np1)
|
||||
let tx3 = reqNull("null_named", np1)
|
||||
let tx4 = reqNoId("none_positional", posPar())
|
||||
let tx = reqBatch(tx1, tx2, tx3, tx4)
|
||||
let txBytes = JrpcSys.encode(tx)
|
||||
let rx = JrpcSys.decode(txBytes, RequestBatchRx)
|
||||
check:
|
||||
rx.kind == rbkMany
|
||||
rx.many.len == 4
|
||||
|
||||
test "ResponseBatchTx -> ResponseBatchRx: single":
|
||||
let tx1 = res(777, JsonString("true"))
|
||||
let tx = resBatch(tx1)
|
||||
let txBytes = JrpcSys.encode(tx)
|
||||
let rx = JrpcSys.decode(txBytes, ResponseBatchRx)
|
||||
check:
|
||||
rx.kind == rbkSingle
|
||||
|
||||
test "ResponseBatchTx -> ResponseBatchRx: many":
|
||||
let tx1 = res(777, JsonString("true"))
|
||||
let tx2 = res("gum", resErr(999, "fatal"))
|
||||
let tx3 = res("gum", resErr(999, "fatal", JsonString("888.999")))
|
||||
let tx = resBatch(tx1, tx2, tx3)
|
||||
let txBytes = JrpcSys.encode(tx)
|
||||
let rx = JrpcSys.decode(txBytes, ResponseBatchRx)
|
||||
check:
|
||||
rx.kind == rbkMany
|
||||
rx.many.len == 3
|
||||
97
tests/test_router_rpc.nim
Normal file
97
tests/test_router_rpc.nim
Normal file
@ -0,0 +1,97 @@
|
||||
import
|
||||
unittest2,
|
||||
../json_rpc/router,
|
||||
json_serialization/stew/results,
|
||||
json_serialization/std/options
|
||||
|
||||
var server = RpcRouter()
|
||||
|
||||
server.rpc("optional") do(A: int, B: Option[int], C: string, D: Option[int], E: Option[string]) -> string:
|
||||
var res = "A: " & $A
|
||||
res.add ", B: " & $B.get(99)
|
||||
res.add ", C: " & C
|
||||
res.add ", D: " & $D.get(77)
|
||||
res.add ", E: " & E.get("none")
|
||||
return res
|
||||
|
||||
server.rpc("noParams") do() -> int:
|
||||
return 123
|
||||
|
||||
server.rpc("emptyParams"):
|
||||
return %777
|
||||
|
||||
server.rpc("comboParams") do(a, b, c: int) -> int:
|
||||
return a+b+c
|
||||
|
||||
server.rpc("returnJsonString") do(a, b, c: int) -> JsonString:
|
||||
return JsonString($(a+b+c))
|
||||
|
||||
func req(meth: string, params: string): string =
|
||||
"""{"jsonrpc":"2.0", "id":0, "method": """ &
|
||||
"\"" & meth & "\", \"params\": " & params & "}"
|
||||
|
||||
suite "rpc router":
|
||||
test "no params":
|
||||
let n = req("noParams", "[]")
|
||||
let res = waitFor server.route(n)
|
||||
check res == """{"jsonrpc":"2.0","id":0,"result":123}"""
|
||||
|
||||
test "no params with params":
|
||||
let n = req("noParams", "[123]")
|
||||
let res = waitFor server.route(n)
|
||||
check res == """{"jsonrpc":"2.0","id":0,"error":{"code":-32000,"message":"noParams raised an exception","data":"Expected 0 Json parameter(s) but got 1"}}"""
|
||||
|
||||
test "optional B E, positional":
|
||||
let n = req("optional", "[44, null, \"apple\", 33]")
|
||||
let res = waitFor server.route(n)
|
||||
check res == """{"jsonrpc":"2.0","id":0,"result":"A: 44, B: 99, C: apple, D: 33, E: none"}"""
|
||||
|
||||
test "optional B D E, positional":
|
||||
let n = req("optional", "[44, null, \"apple\"]")
|
||||
let res = waitFor server.route(n)
|
||||
check res == """{"jsonrpc":"2.0","id":0,"result":"A: 44, B: 99, C: apple, D: 77, E: none"}"""
|
||||
|
||||
test "optional D E, positional":
|
||||
let n = req("optional", "[44, 567, \"apple\"]")
|
||||
let res = waitFor server.route(n)
|
||||
check res == """{"jsonrpc":"2.0","id":0,"result":"A: 44, B: 567, C: apple, D: 77, E: none"}"""
|
||||
|
||||
test "optional D wrong E, positional":
|
||||
let n = req("optional", "[44, 567, \"apple\", \"banana\"]")
|
||||
let res = waitFor server.route(n)
|
||||
check res == """{"jsonrpc":"2.0","id":0,"error":{"code":-32000,"message":"optional raised an exception","data":"Parameter [D] of type 'Option[system.int]' could not be decoded: number expected"}}"""
|
||||
|
||||
test "optional D extra, positional":
|
||||
let n = req("optional", "[44, 567, \"apple\", 999, \"banana\", true]")
|
||||
let res = waitFor server.route(n)
|
||||
check res == """{"jsonrpc":"2.0","id":0,"result":"A: 44, B: 567, C: apple, D: 999, E: banana"}"""
|
||||
|
||||
test "optional B D E, named":
|
||||
let n = req("optional", """{"A": 33, "C":"banana" }""")
|
||||
let res = waitFor server.route(n)
|
||||
check res == """{"jsonrpc":"2.0","id":0,"result":"A: 33, B: 99, C: banana, D: 77, E: none"}"""
|
||||
|
||||
test "optional B E, D front, named":
|
||||
let n = req("optional", """{"D": 8887, "A": 33, "C":"banana" }""")
|
||||
let res = waitFor server.route(n)
|
||||
check res == """{"jsonrpc":"2.0","id":0,"result":"A: 33, B: 99, C: banana, D: 8887, E: none"}"""
|
||||
|
||||
test "optional B E, D front, extra X, named":
|
||||
let n = req("optional", """{"D": 8887, "X": false , "A": 33, "C":"banana"}""")
|
||||
let res = waitFor server.route(n)
|
||||
check res == """{"jsonrpc":"2.0","id":0,"result":"A: 33, B: 99, C: banana, D: 8887, E: none"}"""
|
||||
|
||||
test "empty params":
|
||||
let n = req("emptyParams", "[]")
|
||||
let res = waitFor server.route(n)
|
||||
check res == """{"jsonrpc":"2.0","id":0,"result":777}"""
|
||||
|
||||
test "combo params":
|
||||
let n = req("comboParams", "[6,7,8]")
|
||||
let res = waitFor server.route(n)
|
||||
check res == """{"jsonrpc":"2.0","id":0,"result":21}"""
|
||||
|
||||
test "return json string":
|
||||
let n = req("returnJsonString", "[6,7,8]")
|
||||
let res = waitFor server.route(n)
|
||||
check res == """{"jsonrpc":"2.0","id":0,"result":21}"""
|
||||
@ -1,7 +1,20 @@
|
||||
# json-rpc
|
||||
# Copyright (c) 2019-2023 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
# at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
import
|
||||
unittest2, tables,
|
||||
stint, ethtypes, ethprocs, stintjson, chronicles,
|
||||
../json_rpc/[rpcclient, rpcserver], ./helpers
|
||||
stint, chronicles,
|
||||
../json_rpc/[rpcclient, rpcserver],
|
||||
./private/helpers,
|
||||
./private/ethtypes,
|
||||
./private/ethprocs,
|
||||
./private/stintjson
|
||||
|
||||
from os import getCurrentDir, DirSep
|
||||
from strutils import rsplit
|
||||
@ -15,7 +28,7 @@ var
|
||||
server.addEthRpcs()
|
||||
|
||||
## Generate client convenience marshalling wrappers from forward declarations
|
||||
createRpcSigs(RpcSocketClient, sourceDir & DirSep & "ethcallsigs.nim")
|
||||
createRpcSigs(RpcSocketClient, sourceDir & "/private/ethcallsigs.nim")
|
||||
|
||||
func rpcDynamicName(name: string): string =
|
||||
"rpc." & name
|
||||
@ -38,7 +51,7 @@ proc testLocalCalls: Future[seq[StringOfJson]] =
|
||||
returnUint256 = server.executeMethod("rpc.testReturnUint256", %[])
|
||||
return all(uint256Param, returnUint256)
|
||||
|
||||
proc testRemoteUInt256: Future[seq[Response]] =
|
||||
proc testRemoteUInt256: Future[seq[StringOfJson]] =
|
||||
## Call function remotely on server, testing `stint` types
|
||||
let
|
||||
uint256Param = client.call("rpc.uint256Param", %[%"0x1234567890"])
|
||||
|
||||
@ -1,10 +1,19 @@
|
||||
# json-rpc
|
||||
# Copyright (c) 2019-2023 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
# at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
import
|
||||
unittest2,
|
||||
websock/websock,
|
||||
../json_rpc/[rpcclient, rpcserver]
|
||||
|
||||
const
|
||||
serverHost = "localhost"
|
||||
serverHost = "127.0.0.1"
|
||||
serverPort = 8547
|
||||
serverAddress = serverHost & ":" & $serverPort
|
||||
|
||||
@ -31,18 +40,19 @@ suite "HTTP server hook test":
|
||||
waitFor client.connect(serverHost, Port(serverPort), false)
|
||||
expect ErrorResponse:
|
||||
let r = waitFor client.call("testHook", %[%"abc"])
|
||||
discard r
|
||||
|
||||
test "good auth token":
|
||||
let client = newRpcHttpClient(getHeaders = authHeaders)
|
||||
waitFor client.connect(serverHost, Port(serverPort), false)
|
||||
let r = waitFor client.call("testHook", %[%"abc"])
|
||||
check r.getStr == "Hello abc"
|
||||
check r.string == "\"Hello abc\""
|
||||
|
||||
waitFor srv.closeWait()
|
||||
|
||||
proc wsAuthHeaders(ctx: Hook,
|
||||
headers: var HttpTable): Result[void, string]
|
||||
{.gcsafe, raises: [Defect].} =
|
||||
{.gcsafe, raises: [].} =
|
||||
headers.add("Auth-Token", "Good Token")
|
||||
return ok()
|
||||
|
||||
@ -80,7 +90,7 @@ suite "Websocket server hook test":
|
||||
test "good auth token":
|
||||
waitFor client.connect("ws://127.0.0.1:8545/", hooks = @[hook])
|
||||
let r = waitFor client.call("testHook", %[%"abc"])
|
||||
check r.getStr == "Hello abc"
|
||||
check r.string == "\"Hello abc\""
|
||||
|
||||
srv.stop()
|
||||
waitFor srv.closeWait()
|
||||
|
||||
@ -1,3 +1,12 @@
|
||||
# json-rpc
|
||||
# Copyright (c) 2019-2023 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
# at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
import unittest2
|
||||
import ../json_rpc/[rpcserver, rpcclient]
|
||||
|
||||
@ -7,7 +16,7 @@ proc simpleTest(address: string, port: Port): Future[bool] {.async.} =
|
||||
var client = newRpcHttpClient()
|
||||
await client.connect(address, port, secure = false)
|
||||
var r = await client.call("noParamsProc", %[])
|
||||
if r.getStr == "Hello world":
|
||||
if r.string == "\"Hello world\"":
|
||||
result = true
|
||||
|
||||
proc continuousTest(address: string, port: Port): Future[int] {.async.} =
|
||||
@ -16,7 +25,7 @@ proc continuousTest(address: string, port: Port): Future[int] {.async.} =
|
||||
for i in 0..<TestsCount:
|
||||
await client.connect(address, port, secure = false)
|
||||
var r = await client.call("myProc", %[%"abc", %[1, 2, 3, i]])
|
||||
if r.getStr == "Hello abc data: [1, 2, 3, " & $i & "]":
|
||||
if r.string == "\"Hello abc data: [1, 2, 3, " & $i & "]\"":
|
||||
result += 1
|
||||
await client.close()
|
||||
|
||||
@ -27,17 +36,17 @@ proc invalidTest(address: string, port: Port): Future[bool] {.async.} =
|
||||
try:
|
||||
var r = await client.call("invalidProcA", %[])
|
||||
discard r
|
||||
except ValueError:
|
||||
except JsonRpcError:
|
||||
invalidA = true
|
||||
try:
|
||||
var r = await client.call("invalidProcB", %[1, 2, 3])
|
||||
discard r
|
||||
except ValueError:
|
||||
except JsonRpcError:
|
||||
invalidB = true
|
||||
if invalidA and invalidB:
|
||||
result = true
|
||||
|
||||
var httpsrv = newRpcHttpServer(["localhost:8545"])
|
||||
var httpsrv = newRpcHttpServer(["127.0.0.1:8545"])
|
||||
|
||||
# Create RPC on server
|
||||
httpsrv.rpc("myProc") do(input: string, data: array[0..3, int]):
|
||||
@ -49,11 +58,11 @@ httpsrv.start()
|
||||
|
||||
suite "JSON-RPC test suite":
|
||||
test "Simple RPC call":
|
||||
check waitFor(simpleTest("localhost", Port(8545))) == true
|
||||
check waitFor(simpleTest("127.0.0.1", Port(8545))) == true
|
||||
test "Continuous RPC calls (" & $TestsCount & " messages)":
|
||||
check waitFor(continuousTest("localhost", Port(8545))) == TestsCount
|
||||
check waitFor(continuousTest("127.0.0.1", Port(8545))) == TestsCount
|
||||
test "Invalid RPC calls":
|
||||
check waitFor(invalidTest("localhost", Port(8545))) == true
|
||||
check waitFor(invalidTest("127.0.0.1", Port(8545))) == true
|
||||
|
||||
waitFor httpsrv.stop()
|
||||
waitFor httpsrv.closeWait()
|
||||
|
||||
@ -1,6 +1,14 @@
|
||||
import unittest2, strutils
|
||||
import httputils
|
||||
import ../json_rpc/[rpcsecureserver, rpcclient]
|
||||
# json-rpc
|
||||
# Copyright (c) 2019-2023 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
# at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
import unittest2
|
||||
import ../json_rpc/[rpcserver, rpcclient]
|
||||
import chronos/[streams/tlsstream, apps/http/httpcommon]
|
||||
|
||||
const TestsCount = 100
|
||||
@ -69,7 +77,7 @@ proc simpleTest(address: string, port: Port): Future[bool] {.async.} =
|
||||
var client = newRpcHttpClient(secure=true)
|
||||
await client.connect(address, port, secure=true)
|
||||
var r = await client.call("noParamsProc", %[])
|
||||
if r.getStr == "Hello world":
|
||||
if r.string == "\"Hello world\"":
|
||||
result = true
|
||||
|
||||
proc continuousTest(address: string, port: Port): Future[int] {.async.} =
|
||||
@ -78,7 +86,7 @@ proc continuousTest(address: string, port: Port): Future[int] {.async.} =
|
||||
for i in 0..<TestsCount:
|
||||
await client.connect(address, port, secure=true)
|
||||
var r = await client.call("myProc", %[%"abc", %[1, 2, 3, i]])
|
||||
if r.getStr == "Hello abc data: [1, 2, 3, " & $i & "]":
|
||||
if r.string == "\"Hello abc data: [1, 2, 3, " & $i & "]\"":
|
||||
result += 1
|
||||
await client.close()
|
||||
|
||||
@ -89,19 +97,21 @@ proc invalidTest(address: string, port: Port): Future[bool] {.async.} =
|
||||
try:
|
||||
var r = await client.call("invalidProcA", %[])
|
||||
discard r
|
||||
except ValueError:
|
||||
except JsonRpcError:
|
||||
invalidA = true
|
||||
try:
|
||||
var r = await client.call("invalidProcB", %[1, 2, 3])
|
||||
discard r
|
||||
except ValueError:
|
||||
except JsonRpcError:
|
||||
invalidB = true
|
||||
if invalidA and invalidB:
|
||||
result = true
|
||||
|
||||
let secureKey = TLSPrivateKey.init(HttpsSelfSignedRsaKey)
|
||||
let secureCert = TLSCertificate.init(HttpsSelfSignedRsaCert)
|
||||
var secureHttpSrv = newRpcSecureHttpServer(["localhost:8545"], secureKey, secureCert)
|
||||
var secureHttpSrv = RpcHttpServer.new()
|
||||
|
||||
secureHttpSrv.addSecureHttpServer("127.0.0.1:8545", secureKey, secureCert)
|
||||
|
||||
# Create RPC on server
|
||||
secureHttpSrv.rpc("myProc") do(input: string, data: array[0..3, int]):
|
||||
@ -113,11 +123,11 @@ secureHttpSrv.start()
|
||||
|
||||
suite "JSON-RPC test suite":
|
||||
test "Simple RPC call":
|
||||
check waitFor(simpleTest("localhost", Port(8545))) == true
|
||||
check waitFor(simpleTest("127.0.0.1", Port(8545))) == true
|
||||
test "Continuous RPC calls (" & $TestsCount & " messages)":
|
||||
check waitFor(continuousTest("localhost", Port(8545))) == TestsCount
|
||||
check waitFor(continuousTest("127.0.0.1", Port(8545))) == TestsCount
|
||||
test "Invalid RPC calls":
|
||||
check waitFor(invalidTest("localhost", Port(8545))) == true
|
||||
check waitFor(invalidTest("127.0.0.1", Port(8545))) == true
|
||||
|
||||
waitFor secureHttpSrv.stop()
|
||||
waitFor secureHttpSrv.closeWait()
|
||||
|
||||
@ -1,9 +1,18 @@
|
||||
# json-rpc
|
||||
# Copyright (c) 2019-2023 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
# at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
import
|
||||
unittest2, chronicles,
|
||||
../json_rpc/[rpcclient, rpcserver, rpcproxy]
|
||||
|
||||
let srvAddress = initTAddress("127.0.0.1", Port(8545))
|
||||
let proxySrvAddress = "localhost:8546"
|
||||
let proxySrvAddress = "127.0.0.1:8546"
|
||||
let proxySrvAddressForClient = "http://"&proxySrvAddress
|
||||
|
||||
template registerMethods(srv: RpcServer, proxy: RpcProxy) =
|
||||
@ -29,10 +38,10 @@ suite "Proxy RPC through http":
|
||||
|
||||
test "Successful RPC call thorugh proxy":
|
||||
let r = waitFor client.call("myProc", %[%"abc", %[1, 2, 3, 4]])
|
||||
check r.getStr == "Hello abc data: [1, 2, 3, 4]"
|
||||
check r.string == "\"Hello abc data: [1, 2, 3, 4]\""
|
||||
test "Successful RPC call no proxy":
|
||||
let r = waitFor client.call("myProc1", %[%"abc", %[1, 2, 3, 4]])
|
||||
check r.getStr == "Hello abc data: [1, 2, 3, 4]"
|
||||
check r.string == "\"Hello abc data: [1, 2, 3, 4]\""
|
||||
test "Missing params":
|
||||
expect(CatchableError):
|
||||
discard waitFor client.call("myProc", %[%"abc"])
|
||||
@ -58,10 +67,10 @@ suite "Proxy RPC through websockets":
|
||||
|
||||
test "Successful RPC call thorugh proxy":
|
||||
let r = waitFor client.call("myProc", %[%"abc", %[1, 2, 3, 4]])
|
||||
check r.getStr == "Hello abc data: [1, 2, 3, 4]"
|
||||
check r.string == "\"Hello abc data: [1, 2, 3, 4]\""
|
||||
test "Successful RPC call no proxy":
|
||||
let r = waitFor client.call("myProc1", %[%"abc", %[1, 2, 3, 4]])
|
||||
check r.getStr == "Hello abc data: [1, 2, 3, 4]"
|
||||
check r.string == "\"Hello abc data: [1, 2, 3, 4]\""
|
||||
test "Missing params":
|
||||
expect(CatchableError):
|
||||
discard waitFor client.call("myProc", %[%"abc"])
|
||||
|
||||
@ -1,5 +1,18 @@
|
||||
import unittest2, chronicles, options
|
||||
import ../json_rpc/rpcserver, ./helpers
|
||||
# json-rpc
|
||||
# Copyright (c) 2019-2023 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
# at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
import
|
||||
unittest2,
|
||||
chronicles,
|
||||
../json_rpc/rpcserver,
|
||||
./private/helpers,
|
||||
json_serialization/std/options
|
||||
|
||||
type
|
||||
# some nested types to check object parsing
|
||||
@ -26,6 +39,19 @@ type
|
||||
Enum0
|
||||
Enum1
|
||||
|
||||
MyObject.useDefaultSerializationIn JrpcConv
|
||||
Test.useDefaultSerializationIn JrpcConv
|
||||
Test2.useDefaultSerializationIn JrpcConv
|
||||
MyOptional.useDefaultSerializationIn JrpcConv
|
||||
MyOptionalNotBuiltin.useDefaultSerializationIn JrpcConv
|
||||
|
||||
proc readValue*(r: var JsonReader[JrpcConv], val: var MyEnum)
|
||||
{.gcsafe, raises: [IOError, SerializationError].} =
|
||||
let intVal = r.parseInt(int)
|
||||
if intVal < low(MyEnum).int or intVal > high(MyEnum).int:
|
||||
r.raiseUnexpectedValue("invalid enum range " & $intVal)
|
||||
val = MyEnum(intVal)
|
||||
|
||||
let
|
||||
testObj = %*{
|
||||
"a": %1,
|
||||
@ -38,7 +64,7 @@ let
|
||||
},
|
||||
"c": %1.0}
|
||||
|
||||
var s = newRpcSocketServer(["localhost:8545"])
|
||||
var s = newRpcSocketServer(["127.0.0.1:8545"])
|
||||
|
||||
# RPC definitions
|
||||
s.rpc("rpc.simplePath"):
|
||||
@ -100,6 +126,8 @@ type
|
||||
d: Option[int]
|
||||
e: Option[string]
|
||||
|
||||
OptionalFields.useDefaultSerializationIn JrpcConv
|
||||
|
||||
s.rpc("rpc.mixedOptionalArg") do(a: int, b: Option[int], c: string,
|
||||
d: Option[int], e: Option[string]) -> OptionalFields:
|
||||
|
||||
@ -125,6 +153,8 @@ type
|
||||
o2: Option[bool]
|
||||
o3: Option[bool]
|
||||
|
||||
MaybeOptions.useDefaultSerializationIn JrpcConv
|
||||
|
||||
s.rpc("rpc.optInObj") do(data: string, options: Option[MaybeOptions]) -> int:
|
||||
if options.isSome:
|
||||
let o = options.get
|
||||
@ -155,10 +185,10 @@ suite "Server types":
|
||||
|
||||
test "Enum param paths":
|
||||
block:
|
||||
let r = waitFor s.executeMethod("rpc.enumParam", %[(int64(Enum1))])
|
||||
let r = waitFor s.executeMethod("rpc.enumParam", %[%int64(Enum1)])
|
||||
check r == "[\"Enum1\"]"
|
||||
|
||||
expect(ValueError):
|
||||
expect(JsonRpcError):
|
||||
discard waitFor s.executeMethod("rpc.enumParam", %[(int64(42))])
|
||||
|
||||
test "Different param types":
|
||||
@ -201,30 +231,30 @@ suite "Server types":
|
||||
inp2 = MyOptional()
|
||||
r1 = waitFor s.executeMethod("rpc.optional", %[%inp1])
|
||||
r2 = waitFor s.executeMethod("rpc.optional", %[%inp2])
|
||||
check r1 == JsonRpc.encode inp1
|
||||
check r2 == JsonRpc.encode inp2
|
||||
check r1.string == JrpcConv.encode inp1
|
||||
check r2.string == JrpcConv.encode inp2
|
||||
|
||||
test "Return statement":
|
||||
let r = waitFor s.executeMethod("rpc.testReturns", %[])
|
||||
check r == JsonRpc.encode 1234
|
||||
check r == JrpcConv.encode 1234
|
||||
|
||||
test "Runtime errors":
|
||||
expect ValueError:
|
||||
expect JsonRpcError:
|
||||
# root param not array
|
||||
discard waitFor s.executeMethod("rpc.arrayParam", %"test")
|
||||
expect ValueError:
|
||||
expect JsonRpcError:
|
||||
# too big for array
|
||||
discard waitFor s.executeMethod("rpc.arrayParam", %[%[0, 1, 2, 3, 4, 5, 6], %"hello"])
|
||||
expect ValueError:
|
||||
expect JsonRpcError:
|
||||
# wrong sub parameter type
|
||||
discard waitFor s.executeMethod("rpc.arrayParam", %[%"test", %"hello"])
|
||||
expect ValueError:
|
||||
expect JsonRpcError:
|
||||
# wrong param type
|
||||
discard waitFor s.executeMethod("rpc.differentParams", %[%"abc", %1])
|
||||
|
||||
test "Multiple variables of one type":
|
||||
let r = waitFor s.executeMethod("rpc.multiVarsOfOneType", %[%"hello", %"world"])
|
||||
check r == JsonRpc.encode "hello world"
|
||||
check r == JrpcConv.encode "hello world"
|
||||
|
||||
test "Optional arg":
|
||||
let
|
||||
@ -233,37 +263,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 == JsonRpc.encode int1
|
||||
check r2 == JsonRpc.encode int2
|
||||
check r3 == JsonRpc.encode int2
|
||||
check r1 == JrpcConv.encode int1
|
||||
check r2 == JrpcConv.encode int2
|
||||
check r3 == JrpcConv.encode int2
|
||||
|
||||
test "Optional arg2":
|
||||
let r1 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B"])
|
||||
check r1 == JsonRpc.encode "AB"
|
||||
check r1 == JrpcConv.encode "AB"
|
||||
|
||||
let r2 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B", newJNull()])
|
||||
check r2 == JsonRpc.encode "AB"
|
||||
check r2 == JrpcConv.encode "AB"
|
||||
|
||||
let r3 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B", newJNull(), newJNull()])
|
||||
check r3 == JsonRpc.encode "AB"
|
||||
check r3 == JrpcConv.encode "AB"
|
||||
|
||||
let r4 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B", newJNull(), %"D"])
|
||||
check r4 == JsonRpc.encode "ABD"
|
||||
check r4 == JrpcConv.encode "ABD"
|
||||
|
||||
let r5 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B", %"C", %"D"])
|
||||
check r5 == JsonRpc.encode "ABCD"
|
||||
check r5 == JrpcConv.encode "ABCD"
|
||||
|
||||
let r6 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B", %"C", newJNull()])
|
||||
check r6 == JsonRpc.encode "ABC"
|
||||
check r6 == JrpcConv.encode "ABC"
|
||||
|
||||
let r7 = waitFor s.executeMethod("rpc.optionalArg2", %[%"A", %"B", %"C"])
|
||||
check r7 == JsonRpc.encode "ABC"
|
||||
check r7 == JrpcConv.encode "ABC"
|
||||
|
||||
test "Mixed optional arg":
|
||||
var ax = waitFor s.executeMethod("rpc.mixedOptionalArg", %[%10, %11, %"hello", %12, %"world"])
|
||||
check ax == JsonRpc.encode OptionalFields(a: 10, b: some(11), c: "hello", d: some(12), e: some("world"))
|
||||
check ax == JrpcConv.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 == JsonRpc.encode OptionalFields(a: 10, c: "hello")
|
||||
check bx == JrpcConv.encode OptionalFields(a: 10, c: "hello")
|
||||
|
||||
test "Non-built-in optional types":
|
||||
let
|
||||
@ -271,33 +301,33 @@ suite "Server types":
|
||||
testOpts1 = MyOptionalNotBuiltin(val: some(t2))
|
||||
testOpts2 = MyOptionalNotBuiltin()
|
||||
var r = waitFor s.executeMethod("rpc.optionalArgNotBuiltin", %[%testOpts1])
|
||||
check r == JsonRpc.encode t2.y
|
||||
check r == JrpcConv.encode t2.y
|
||||
var r2 = waitFor s.executeMethod("rpc.optionalArgNotBuiltin", %[])
|
||||
check r2 == JsonRpc.encode "Empty1"
|
||||
check r2 == JrpcConv.encode "Empty1"
|
||||
var r3 = waitFor s.executeMethod("rpc.optionalArgNotBuiltin", %[%testOpts2])
|
||||
check r3 == JsonRpc.encode "Empty2"
|
||||
check r3 == JrpcConv.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 == JsonRpc.encode 1
|
||||
check r1 == JrpcConv.encode 1
|
||||
let opts2 = parseJson("""{"o2": true}""")
|
||||
var r2 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts2])
|
||||
check r2 == JsonRpc.encode 2
|
||||
check r2 == JrpcConv.encode 2
|
||||
let opts3 = parseJson("""{"o3": true}""")
|
||||
var r3 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts3])
|
||||
check r3 == JsonRpc.encode 4
|
||||
check r3 == JrpcConv.encode 4
|
||||
# Combinations
|
||||
let opts4 = parseJson("""{"o1": true, "o3": true}""")
|
||||
var r4 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts4])
|
||||
check r4 == JsonRpc.encode 5
|
||||
check r4 == JrpcConv.encode 5
|
||||
let opts5 = parseJson("""{"o2": true, "o3": true}""")
|
||||
var r5 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts5])
|
||||
check r5 == JsonRpc.encode 6
|
||||
check r5 == JrpcConv.encode 6
|
||||
let opts6 = parseJson("""{"o1": true, "o2": true}""")
|
||||
var r6 = waitFor s.executeMethod("rpc.optInObj", %[%"0x31ded", opts6])
|
||||
check r6 == JsonRpc.encode 3
|
||||
check r6 == JrpcConv.encode 3
|
||||
|
||||
s.stop()
|
||||
waitFor s.closeWait()
|
||||
|
||||
@ -1,5 +1,14 @@
|
||||
# json-rpc
|
||||
# Copyright (c) 2019-2023 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
# at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
import
|
||||
unittest2,
|
||||
unittest2,
|
||||
../json_rpc/[rpcclient, rpcserver]
|
||||
|
||||
# Create RPC on server
|
||||
@ -14,16 +23,16 @@ proc setupServer*(srv: RpcServer) =
|
||||
raise (ref InvalidRequest)(code: -32001, msg: "Unknown payload")
|
||||
|
||||
suite "Socket Server/Client RPC":
|
||||
var srv = newRpcSocketServer(["localhost:8545"])
|
||||
var srv = newRpcSocketServer(["127.0.0.1:8545"])
|
||||
var client = newRpcSocketClient()
|
||||
|
||||
srv.setupServer()
|
||||
srv.start()
|
||||
waitFor client.connect("localhost", Port(8545))
|
||||
waitFor client.connect("127.0.0.1", Port(8545))
|
||||
|
||||
test "Successful RPC call":
|
||||
let r = waitFor client.call("myProc", %[%"abc", %[1, 2, 3, 4]])
|
||||
check r.getStr == "Hello abc data: [1, 2, 3, 4]"
|
||||
check r.string == "\"Hello abc data: [1, 2, 3, 4]\""
|
||||
|
||||
test "Missing params":
|
||||
expect(CatchableError):
|
||||
@ -38,7 +47,7 @@ suite "Socket Server/Client RPC":
|
||||
discard waitFor client.call("invalidRequest", %[])
|
||||
check false
|
||||
except CatchableError as e:
|
||||
check e.msg == """{"code":-32001,"message":"Unknown payload","data":null}"""
|
||||
check e.msg == """{"code":-32001,"message":"Unknown payload"}"""
|
||||
|
||||
srv.stop()
|
||||
waitFor srv.closeWait()
|
||||
@ -53,7 +62,7 @@ suite "Websocket Server/Client RPC":
|
||||
|
||||
test "Successful RPC call":
|
||||
let r = waitFor client.call("myProc", %[%"abc", %[1, 2, 3, 4]])
|
||||
check r.getStr == "Hello abc data: [1, 2, 3, 4]"
|
||||
check r.string == "\"Hello abc data: [1, 2, 3, 4]\""
|
||||
|
||||
test "Missing params":
|
||||
expect(CatchableError):
|
||||
@ -68,7 +77,7 @@ suite "Websocket Server/Client RPC":
|
||||
discard waitFor client.call("invalidRequest", %[])
|
||||
check false
|
||||
except CatchableError as e:
|
||||
check e.msg == """{"code":-32001,"message":"Unknown payload","data":null}"""
|
||||
check e.msg == """{"code":-32001,"message":"Unknown payload"}"""
|
||||
|
||||
srv.stop()
|
||||
waitFor srv.closeWait()
|
||||
@ -85,7 +94,7 @@ suite "Websocket Server/Client RPC with Compression":
|
||||
|
||||
test "Successful RPC call":
|
||||
let r = waitFor client.call("myProc", %[%"abc", %[1, 2, 3, 4]])
|
||||
check r.getStr == "Hello abc data: [1, 2, 3, 4]"
|
||||
check r.string == "\"Hello abc data: [1, 2, 3, 4]\""
|
||||
|
||||
test "Missing params":
|
||||
expect(CatchableError):
|
||||
@ -100,7 +109,8 @@ suite "Websocket Server/Client RPC with Compression":
|
||||
discard waitFor client.call("invalidRequest", %[])
|
||||
check false
|
||||
except CatchableError as e:
|
||||
check e.msg == """{"code":-32001,"message":"Unknown payload","data":null}"""
|
||||
check e.msg == """{"code":-32001,"message":"Unknown payload"}"""
|
||||
|
||||
srv.stop()
|
||||
waitFor srv.closeWait()
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user