mirror of
https://github.com/status-im/nim-json-rpc.git
synced 2025-02-24 18:28:10 +00:00
163 lines
4.6 KiB
Nim
163 lines
4.6 KiB
Nim
import json, tables, options, macros, chronicles
|
|
import asyncdispatch2, router
|
|
import jsonmarshal
|
|
|
|
export asyncdispatch2, json, jsonmarshal, router
|
|
|
|
logScope:
|
|
topics = "RpcServer"
|
|
|
|
type
|
|
RpcJsonError* = enum rjeInvalidJson, rjeVersionError, rjeNoMethod, rjeNoId, rjeNoParams
|
|
|
|
RpcJsonErrorContainer* = tuple[err: RpcJsonError, msg: string]
|
|
|
|
RpcServer*[S] = ref object
|
|
servers*: seq[S]
|
|
router*: RpcRouter
|
|
|
|
RpcProcError* = ref object of Exception
|
|
code*: int
|
|
data*: JsonNode
|
|
|
|
RpcBindError* = object of Exception
|
|
RpcAddressUnresolvableError* = object of Exception
|
|
|
|
const
|
|
JSON_PARSE_ERROR* = -32700
|
|
INVALID_REQUEST* = -32600
|
|
METHOD_NOT_FOUND* = -32601
|
|
INVALID_PARAMS* = -32602
|
|
INTERNAL_ERROR* = -32603
|
|
SERVER_ERROR* = -32000
|
|
|
|
defaultMaxRequestLength* = 1024 * 128
|
|
jsonErrorMessages*: array[RpcJsonError, (int, string)] =
|
|
[
|
|
(JSON_PARSE_ERROR, "Invalid JSON"),
|
|
(INVALID_REQUEST, "JSON 2.0 required"),
|
|
(INVALID_REQUEST, "No method requested"),
|
|
(INVALID_REQUEST, "No id specified"),
|
|
(INVALID_PARAMS, "No parameters specified")
|
|
]
|
|
|
|
proc newRpcServer*[S](): RpcServer[S] =
|
|
new result
|
|
result.router = newRpcRouter()
|
|
result.servers = @[]
|
|
|
|
# Utility functions
|
|
# TODO: Move outside server?
|
|
#func `%`*(p: Port): JsonNode = %(p.int)
|
|
|
|
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)
|
|
|
|
# Json state checking
|
|
|
|
template jsonValid*(jsonString: string, node: var JsonNode): (bool, string) =
|
|
var
|
|
valid = true
|
|
msg = ""
|
|
try: node = parseJson(line)
|
|
except:
|
|
valid = false
|
|
msg = getCurrentExceptionMsg()
|
|
debug "Cannot process json", json = jsonString, msg = msg
|
|
(valid, msg)
|
|
|
|
proc checkJsonState*(line: string,
|
|
node: var JsonNode): Option[RpcJsonErrorContainer] =
|
|
## Tries parsing line into node, if successful checks required fields
|
|
## Returns: error state or none
|
|
let res = jsonValid(line, node)
|
|
if not res[0]:
|
|
return some((rjeInvalidJson, res[1]))
|
|
if not node.hasKey("id"):
|
|
return some((rjeNoId, ""))
|
|
let jVer = node{"jsonrpc"}
|
|
if jVer != nil and jVer.kind != JNull and jVer != %"2.0":
|
|
return some((rjeVersionError, ""))
|
|
if not node.hasKey("method"):
|
|
return some((rjeNoMethod, ""))
|
|
if not node.hasKey("params"):
|
|
return some((rjeNoParams, ""))
|
|
return none(RpcJsonErrorContainer)
|
|
|
|
# Json reply wrappers
|
|
|
|
proc wrapReply*(id: JsonNode, value: JsonNode, error: JsonNode): string =
|
|
let node = %{"jsonrpc": %"2.0", "result": value, "error": error, "id": id}
|
|
return $node & "\c\l"
|
|
|
|
proc wrapError*(code: int, msg: string, id: JsonNode,
|
|
data: JsonNode = newJNull()): JsonNode =
|
|
# Create standardised error json
|
|
result = %{"code": %(code), "id": id, "message": %msg, "data": data}
|
|
debug "Error generated", error = result, id = id
|
|
|
|
# Server message processing
|
|
|
|
proc processMessages*[T](server: RpcServer[T], line: string): Future[string] {.async, gcsafe.} =
|
|
var
|
|
node: JsonNode
|
|
# parse json node and/or flag missing fields and errors
|
|
jsonErrorState = checkJsonState(line, node)
|
|
|
|
if jsonErrorState.isSome:
|
|
let errState = jsonErrorState.get
|
|
var id =
|
|
if errState.err == rjeInvalidJson or errState.err == rjeNoId:
|
|
newJNull()
|
|
else:
|
|
node["id"]
|
|
let errMsg = jsonErrorMessages[errState.err]
|
|
# return error state as json
|
|
result = $wrapError(
|
|
code = errMsg[0],
|
|
msg = errMsg[1],
|
|
id = id)
|
|
else:
|
|
let
|
|
methodName = node["method"].str
|
|
id = node["id"]
|
|
var callRes: Future[JsonNode]
|
|
|
|
if server.router.ifRoute(node, callRes):
|
|
let res = await callRes
|
|
result = $wrapReply(id, res, newJNull())
|
|
else:
|
|
let
|
|
methodNotFound = %(methodName & " is not a registered method.")
|
|
error = wrapError(METHOD_NOT_FOUND, "Method not found", id, methodNotFound)
|
|
result = $wrapReply(id, newJNull(), error)
|
|
|
|
proc start*(server: RpcServer) =
|
|
## Start the RPC server.
|
|
for item in server.servers:
|
|
item.start()
|
|
|
|
proc stop*(server: RpcServer) =
|
|
## Stop the RPC server.
|
|
for item in server.servers:
|
|
item.stop()
|
|
|
|
proc close*(server: RpcServer) =
|
|
## Cleanup resources of RPC server.
|
|
for item in server.servers:
|
|
item.close()
|
|
|
|
# Server registration
|
|
|
|
proc register*(server: RpcServer, name: string, rpc: RpcProc) =
|
|
## Add a name/code pair to the RPC server.
|
|
server.router.addRoute(name, rpc)
|
|
|
|
proc unRegisterAll*(server: RpcServer) =
|
|
# Remove all remote procedure calls from this server.
|
|
server.router.clear
|
|
|
|
|