Skip to content

Commit

Permalink
Refactor the router to allow bypassing the use of std/json
Browse files Browse the repository at this point in the history
The users will be now able to write RPC handlers returning the
distinct type `StringOfJson`. This will bypass any use of the
standard library's JsonNode type and its serialization routines.

The change was motivated by the integration of JSON-RPC in
status-im/nim-beacon-chain where most of the data types cannot
be handled by Nim's std lib or json-rpc's jsonmarshal module.
  • Loading branch information
zah committed Mar 17, 2020
1 parent 4d1d257 commit d19de19
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 43 deletions.
77 changes: 37 additions & 40 deletions json_rpc/router.nim
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ type
rjeInvalidJson, rjeVersionError, rjeNoMethod, rjeNoId, rjeNoParams, rjeNoJObject
RpcJsonErrorContainer* = tuple[err: RpcJsonError, msg: string]

StringOfJson* = distinct string

# Procedure signature accepted as an RPC call by server
RpcProc* = proc(input: JsonNode): Future[JsonNode] {.gcsafe.}
RpcProc* = proc(input: JsonNode): Future[StringOfJson] {.gcsafe.}

RpcProcError* = ref object of Exception
code*: int
Expand Down Expand Up @@ -99,16 +101,22 @@ proc checkJsonState*(line: string,

# Json reply wrappers

proc wrapReply*(id: JsonNode, value: JsonNode, error: JsonNode): JsonNode =
return %{jsonRpcField: %"2.0", idField: id, resultField: value, errorField: error}
proc wrapReply*(id: JsonNode, value, error: StringOfJson): StringOfJson =
return StringOfJson(
"""{"jsonRpcField":"2.0","idField":$1,"resultField":$2,"errorField":$3}""" % [
$id, string(value), string(error)
])

proc wrapError*(code: int, msg: string, id: JsonNode,
data: JsonNode = newJNull()): JsonNode {.gcsafe.} =
data: JsonNode = newJNull()): StringOfJson {.gcsafe.} =
# Create standardised error json
result = %{codeField: %(code), idField: id, messageField: %msg, dataField: data}
result = StringOfJson(
"""{"codeField":$1,"idField":$2,"messageField":$3,"dataField":$4}""" % [
$code, $id, escapeJson(msg), $data
])
debug "Error generated", error = result, id = id

proc route*(router: RpcRouter, node: JsonNode): Future[JsonNode] {.async, gcsafe.} =
proc route*(router: RpcRouter, node: JsonNode): Future[StringOfJson] {.async, gcsafe.} =
## Assumes correct setup of node
let
methodName = node[methodField].str
Expand All @@ -119,19 +127,17 @@ proc route*(router: RpcRouter, node: JsonNode): Future[JsonNode] {.async, gcsafe
let
methodNotFound = %(methodName & " is not a registered RPC method.")
error = wrapError(METHOD_NOT_FOUND, "Method not found", id, methodNotFound)
result = wrapReply(id, newJNull(), error)
result = wrapReply(id, StringOfJson("null"), error)
else:
let
jParams = node[paramsField]
res = await rpcProc(jParams)
errCode = res.getOrDefault(codeField)
errMsg = res.getOrDefault(messageField)
if errCode != nil and errCode.kind == JInt and
errMsg != nil and errMsg.kind == JString:
let error = wrapError(errCode.getInt, methodName & " raised an exception", id, errMsg)
result = wrapReply(id, newJNull(), error)
else:
result = wrapReply(id, res, newJNull())
try:
let jParams = node[paramsField]
let res = await rpcProc(jParams)
result = wrapReply(id, res, StringOfJson("null"))
except CatchableError as err:
debug "Error occurred within RPC", methodName, errorMessage = err.msg
let error = wrapError(SERVER_ERROR, methodName & " raised an exception",
id, newJString(err.msg))
result = wrapReply(id, StringOfJson("null"), error)

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.
Expand All @@ -157,12 +163,12 @@ proc route*(router: RpcRouter, data: string): Future[string] {.async, gcsafe.} =
fullMsg = errKind[1] & " " & errState[1]
res = wrapError(code = errKind[0], msg = fullMsg, id = id)
# return error state as json
result = $res & messageTerminator
result = string(res) & messageTerminator
else:
let res = await router.route(node)
result = $res & messageTerminator
result = string(res) & messageTerminator

proc tryRoute*(router: RpcRouter, data: JsonNode, fut: var Future[JsonNode]): bool =
proc tryRoute*(router: RpcRouter, data: JsonNode, fut: var Future[StringOfJson]): bool =
## Route to RPC, returns false if the method or params cannot be found.
## Expects json input and returns json output.
let
Expand All @@ -188,14 +194,6 @@ proc hasReturnType(params: NimNode): bool =
params[0].kind != nnkEmpty:
result = true

template trap(path: string, body: untyped): untyped =
try:
body
except CatchableError as exc:
let msg = exc.msg
debug "Error occurred within RPC ", path = path, errorMessage = msg
result = %*{codeField: %SERVER_ERROR, messageField: %msg}

macro rpc*(server: RpcRouter, path: string, body: untyped): untyped =
## Define a remote procedure call.
## Input and return parameters are defined using the ``do`` notation.
Expand Down Expand Up @@ -238,23 +236,22 @@ macro rpc*(server: RpcRouter, path: string, body: untyped): untyped =
if ReturnType == ident"JsonNode":
# `JsonNode` results don't need conversion
result.add quote do:
proc `procName`(`paramsIdent`: JsonNode): Future[JsonNode] {.async, gcsafe.} =
trap(`pathStr`):
`res` = await `doMain`(`paramsIdent`)
elif ReturnType == ident"JsonString":
discard
proc `procName`(`paramsIdent`: JsonNode): Future[StringOfJson] {.async, gcsafe.} =
return StringOfJson($(await `doMain`(`paramsIdent`)))
elif ReturnType == ident"StringOfJson":
result.add quote do:
proc `procName`(`paramsIdent`: JsonNode): Future[StringOfJson] {.async, gcsafe.} =
return await `doMain`(`paramsIdent`)
else:
result.add quote do:
proc `procName`(`paramsIdent`: JsonNode): Future[JsonNode] {.async, gcsafe.} =
trap(`pathStr`):
`res` = %(await `doMain`(`paramsIdent`))
proc `procName`(`paramsIdent`: JsonNode): Future[StringOfJson] {.async, gcsafe.} =
return StringOfJson($(%(await `doMain`(`paramsIdent`))))
else:
# no return types, inline contents
result.add quote do:
proc `procName`(`paramsIdent`: JsonNode): Future[JsonNode] {.async, gcsafe.} =
proc `procName`(`paramsIdent`: JsonNode): Future[StringOfJson] {.async, gcsafe.} =
`setup`
trap(`pathStr`):
`procBody`
`procBody`

result.add quote do:
`server`.register(`path`, `procName`)
Expand Down
3 changes: 2 additions & 1 deletion json_rpc/server.nim
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ proc newRpcServer*(): RpcServer =
template rpc*(server: RpcServer, path: string, body: untyped): untyped =
server.router.rpc(path, body)

template hasMethod*(server: RpcServer, methodName: string): bool = server.router.hasMethod(methodName)
template hasMethod*(server: RpcServer, methodName: string): bool =
server.router.hasMethod(methodName)

# Wrapper for message processing

Expand Down
3 changes: 1 addition & 2 deletions json_rpc/servers/socketserver.nim
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ 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)
var value = $wrapReply(id, newJNull(), error)
result = transport.write(value)
result = transport.write(string wrapReply(id, StringOfJson("null"), error))

proc processClient(server: StreamServer, transport: StreamTransport) {.async, gcsafe.} =
## Process transport data to the RPC server
Expand Down

0 comments on commit d19de19

Please sign in to comment.