nim-json-rpc/eth-rpc/server/servertypes.nim

362 lines
12 KiB
Nim
Raw Normal View History

import asyncdispatch, asyncnet, json, tables, macros, strutils
export asyncdispatch, asyncnet, json
2018-03-02 11:46:59 +00:00
type
2018-04-11 20:08:12 +01:00
RpcProc* = proc (params: JsonNode): Future[JsonNode]
2018-03-02 11:46:59 +00:00
RpcServer* = ref object
socket*: AsyncSocket
port*: Port
address*: string
procs*: TableRef[string, RpcProc]
RpcProcError* = ref object of Exception
code*: int
data*: JsonNode
proc register*(server: RpcServer, name: string, rpc: RpcProc) =
server.procs[name] = rpc
proc unRegisterAll*(server: RpcServer) = server.procs.clear
macro rpc*(prc: untyped): untyped =
## Converts a procedure into the following format:
## <proc name>*(params: JsonNode): Future[JsonNode] {.async.}
## This procedure is then added into a compile-time list
## so that it is automatically registered for every server that
## calls registerRpcs(server)
prc.expectKind nnkProcDef
result = prc
let
params = prc.params
procName = prc.name
procName.expectKind(nnkIdent)
# check there isn't already a result type
assert params[0].kind == nnkEmpty
# add parameter
params.add nnkIdentDefs.newTree(
newIdentNode("params"),
newIdentNode("JsonNode"),
newEmptyNode()
)
# set result type
params[0] = nnkBracketExpr.newTree(
newIdentNode("Future"),
newIdentNode("JsonNode")
)
# add async pragma; we can assume there isn't an existing .async.
# as this would mean there's a return type and fail the result check above.
prc.addPragma(newIdentNode("async"))
proc newRpcServer*(address = "localhost", port: Port = Port(8545)): RpcServer =
result = RpcServer(
2018-03-02 11:46:59 +00:00
socket: newAsyncSocket(),
port: port,
address: address,
procs: newTable[string, RpcProc]()
)
var sharedServer: RpcServer
proc sharedRpcServer*(): RpcServer =
if sharedServer.isNil: sharedServer = newRpcServer("")
result = sharedServer
macro multiRemove(s: string, values: varargs[string]): untyped =
2018-04-24 16:28:01 +01:00
## Wrapper for multiReplace
var
body = newStmtList()
multiReplaceCall = newCall(ident"multiReplace", s)
body.add(newVarStmt(ident"eStr", newStrLitNode("")))
let emptyStr = ident"eStr"
for item in values:
2018-04-24 16:28:01 +01:00
# generate tuples of values with the empty string `eStr`
let sItem = $item
2018-04-24 16:28:01 +01:00
multiReplaceCall.add(newPar(newStrLitNode(sItem), emptyStr))
body.add multiReplaceCall
result = newBlockStmt(body)
2018-05-03 20:20:10 +01:00
proc jsonGetFunc(paramType: string): (NimNode, JsonNodeKind) =
case paramType
of "string": result = (ident"getStr", JString)
of "int": result = (ident"getInt", JInt)
of "float": result = (ident"getFloat", JFloat)
of "bool": result = (ident"getBool", JBool)
of "uint8", "byte": result = (ident"getInt", JInt)
else: result = (nil, JInt)
#[proc jsonGetFunc(paramType: string): NimNode =
case paramType
of "string": result = ident"getStr"
of "int": result = ident"getInt"
of "float": result = ident"getFloat"
of "bool": result = ident"getBool"
of "uint8": result = ident"getInt"
else: result = nil
2018-05-03 20:20:10 +01:00
]#
proc jsonTranslate(translation: var NimNode, paramType: string): NimNode =
case paramType
of "uint8":
result = genSym(nskTemplate)
translation = quote do:
template `result`(value: int): uint8 =
if value > 255 or value < 0:
raise newException(ValueError, "Value out of range of byte, expected 0-255, got " & $value)
uint8(value and 0xff)
else:
result = genSym(nskTemplate)
translation = quote do:
template `result`(value: untyped): untyped = value
2018-05-03 20:20:10 +01:00
#[
2018-05-02 16:21:05 +01:00
proc jsonCheckType(paramType: string): JsonNodeKind =
case paramType
2018-05-02 16:21:05 +01:00
of "string": result = JString
of "int": result = JInt
of "float": result = JFloat
of "bool": result = JBool
2018-05-03 20:20:10 +01:00
of "byte": result = JInt
]#
2018-05-01 20:32:28 +01:00
proc preParseTypes(typeNode: var NimNode, typeName: NimNode, errorCheck: var NimNode): bool {.compileTime.} =
# handle byte
for i, item in typeNode:
if item.kind == nnkIdent and item.basename == ident"byte":
typeNode[i] = ident"int"
# add some extra checks
result = true
else:
var t = typeNode[i]
if preParseTypes(t, typeName, errorCheck):
typeNode[i] = t
2018-05-03 20:20:10 +01:00
proc expectKind(node, jsonIdent, fieldName: NimNode, tn: JsonNodeKind) =
2018-05-01 20:32:28 +01:00
let
expectedStr = "Expected parameter `" & fieldName.repr & "` to be " & $tn & " but got "
tnIdent = ident($tn)
node.add(quote do:
if `jsonIdent`.kind != `tnIdent`:
raise newException(ValueError, `expectedStr` & $`jsonIdent`.kind)
)
2018-05-02 16:21:05 +01:00
2018-05-03 20:20:10 +01:00
proc translate(paramType: string, getField: NimNode): NimNode =
# Note that specific types add extra run time bounds checking code
case paramType
of "byte":
result = quote do:
let x = `getField`
if x > 255 or x < 0:
raise newException(ValueError, "Value out of range of byte, expected 0-255, got " & $x)
uint8(x and 0xff)
else:
result = quote do: `getField`
2018-05-01 20:32:28 +01:00
macro processFields(jsonIdent, fieldName, fieldType: typed): untyped =
result = newStmtList()
2018-05-02 16:21:05 +01:00
let
fieldTypeStr = fieldType.repr
2018-05-03 20:20:10 +01:00
(jFetch, jKind) = jsonGetFunc(fieldTypeStr)
var translation: NimNode
2018-05-02 16:21:05 +01:00
if not jFetch.isNil:
2018-05-03 20:20:10 +01:00
result.expectKind(jsonIdent, fieldName, jKind)
#let transIdent = translation.jsonTranslate(fieldTypeStr)
let
getField = quote do: `jsonIdent`.`jFetch`
res = translate(`fieldTypeStr`, `getField`)
2018-05-02 16:21:05 +01:00
result.add(quote do:
2018-05-03 20:20:10 +01:00
`fieldName` = `res`
2018-05-02 16:21:05 +01:00
)
2018-05-03 20:20:10 +01:00
echo ">>", result.repr, "<<"
#result.add(quote do:
# `translation`
# `fieldName` = `transIdent`(`jsonIdent`.`jFetch`)
#)
2018-05-02 16:21:05 +01:00
else:
var fetchedType = getType(fieldType)
var derivedType: NimNode
if fetchedType[0].repr == "typeDesc":
derivedType = getType(fetchedType[1])
2018-05-01 20:32:28 +01:00
else:
2018-05-02 16:21:05 +01:00
derivedType = fetchedType
if derivedType.kind == nnkObjectTy:
2018-05-03 20:20:10 +01:00
result.expectKind(jsonIdent, fieldName, JObject)
2018-05-02 16:21:05 +01:00
let recs = derivedType.findChild it.kind == nnkRecList
for i in 0..<recs.len:
let
objFieldName = recs[i]
objFieldNameStr = objFieldName.toStrLit
objFieldType = getType(recs[i])
realType = getType(objFieldType)
jsonIdentStr = jsonIdent.repr
result.add(quote do:
if not `jsonIdent`.hasKey(`objFieldNameStr`):
raise newException(ValueError, "Cannot find field " & `objFieldNameStr` & " in " & `jsonIdentStr`)
processFields(`jsonIdent`[`objFieldNameStr`], `fieldName`.`objfieldName`, `realType`)
)
elif derivedType.kind == nnkBracketExpr:
# this should be a seq or array
2018-05-03 20:20:10 +01:00
result.expectKind(jsonIdent, fieldName, JArray)
2018-05-01 20:32:28 +01:00
let
2018-05-02 16:21:05 +01:00
formatType = derivedType[0].repr
expectedLen = genSym(nskConst)
2018-05-03 20:20:10 +01:00
var rootType: NimNode
2018-05-02 16:21:05 +01:00
case formatType
of "array":
let
startLen = derivedType[1][1]
endLen = derivedType[1][2]
expectedParamLen = quote do:
const `expectedLen` = `endLen` - `startLen` + 1
expectedLenStr = "Expected parameter `" & fieldName.repr & "` to have a length of "
# TODO: Note, currently only raising if greater than length, not different size
2018-05-02 16:21:05 +01:00
result.add(quote do:
`expectedParamLen`
if `jsonIdent`.len > `expectedLen`:
raise newException(ValueError, `expectedLenStr` & $`expectedLen` & " but got " & $`jsonIdent`.len)
)
rootType = derivedType[2]
2018-05-02 16:21:05 +01:00
of "seq":
result.add(quote do:
`fieldName` = @[]
`fieldName`.setLen(`jsonIdent`.len)
)
rootType = derivedType[1]
2018-05-02 16:21:05 +01:00
else:
raise newException(ValueError, "Cannot determine bracket expression type of \"" & derivedType.treerepr & "\"")
# add fetch code for array/seq
2018-05-03 20:20:10 +01:00
let (jFunc, jKind) = jsonGetFunc($rootType)
let transIdent = translation.jsonTranslate($rootType)
2018-05-01 20:32:28 +01:00
result.add(quote do:
`translation`
2018-05-02 16:21:05 +01:00
for i in 0 ..< `jsonIdent`.len:
`fieldName`[i] = `transIdent`(`jsonIdent`.elems[i].`jFunc`)
2018-05-01 20:32:28 +01:00
)
2018-05-02 16:21:05 +01:00
else:
2018-05-03 20:20:10 +01:00
echo "DT ", fieldType.treerepr
echo "DT ", fetchedType.treerepr
echo "DT ", derivedType.treerepr
echo "fts ", fieldTypeStr
echo "JN ", jFetch.treerepr
2018-05-02 16:21:05 +01:00
raise newException(ValueError, "Unknown type \"" & derivedType.treerepr & "\"")
2018-05-01 20:32:28 +01:00
proc setupParams(node, parameters, paramsIdent: NimNode) =
# recurse parameter's fields until we only have symbols
if not parameters.isNil:
2018-05-01 20:32:28 +01:00
var
errorCheck = newStmtList()
expectedParams = parameters.len - 1
let expectedStr = "Expected " & $`expectedParams` & " Json parameter(s) but got "
2018-05-01 20:32:28 +01:00
node.add(quote do:
if `paramsIdent`.len != `expectedParams`:
raise newException(ValueError, `expectedStr` & $`paramsIdent`.len)
)
2018-05-01 20:32:28 +01:00
for i in 1..< parameters.len:
let
paramName = parameters[i][0]
pos = i - 1
var
paramType = parameters[i][1]
#discard paramType.preParseTypes(paramName, errorCheck)
2018-05-01 20:32:28 +01:00
node.add(quote do:
var `paramName`: `paramType`
processFields(`paramsIdent`[`pos`], `paramName`, `paramType`)
`errorCheck`
)
# TODO: Check for byte ranges
2018-05-01 20:32:28 +01:00
macro on*(server: var RpcServer, path: string, body: untyped): untyped =
result = newStmtList()
var setup = newStmtList()
let
parameters = body.findChild(it.kind == nnkFormalParams)
paramsIdent = ident"params"
setup.setupParams(parameters, paramsIdent)
2018-05-01 20:32:28 +01:00
# wrapping proc
let
pathStr = $path
procName = ident(pathStr.multiRemove(".", "/")) # TODO: Make this unique to avoid potential clashes, or allow people to know the name for calling?
var procBody: NimNode
if body.kind == nnkStmtList: procBody = body
else: procBody = body.body
result = quote do:
proc `procName`*(`paramsIdent`: JsonNode): Future[JsonNode] {.async.} =
2018-05-01 20:32:28 +01:00
#`checkTypeError`
`setup`
`procBody`
`server`.register(`path`, `procName`)
when defined(nimDumpRpcs):
2018-05-01 20:32:28 +01:00
echo pathStr, ": ", result.repr
when isMainModule:
import unittest
var s = newRpcServer("localhost")
s.on("rpc.arrayparam") do(arr: array[0..5, byte], b: string):
var res = newJArray()
2018-04-24 19:21:51 +01:00
for item in arr:
res.add %int(item)
res.add %b
result = %res
2018-05-01 20:32:28 +01:00
s.on("rpc.seqparam") do(a: string, s: seq[int]):
2018-04-24 19:21:51 +01:00
var res = newJArray()
2018-05-01 20:32:28 +01:00
res.add %a
for item in s:
res.add %int(item)
result = res
2018-05-02 16:21:05 +01:00
type
Test2 = object
x: array[2, int]
Test = object
d: array[0..1, int]
e: Test2
2018-05-01 20:32:28 +01:00
type MyObject* = object
a: int
2018-05-01 20:32:28 +01:00
b: Test
c: float
2018-05-03 20:20:10 +01:00
2018-05-01 20:32:28 +01:00
s.on("rpc.objparam") do(a: string, obj: MyObject):
result = %obj
2018-05-03 20:20:10 +01:00
s.on("rpc.specialtypes") do(a: byte):
result = %int(a)
# TODO: Add path as constant for each rpc
suite "Server types":
2018-04-24 19:21:51 +01:00
test "Array/seq parameters":
let r1 = waitfor rpcArrayParam(%[%[1, 2, 3], %"hello"])
2018-04-24 19:21:51 +01:00
var ckR1 = %[1, 2, 3, 0, 0, 0]
ckR1.elems.add %"hello"
check r1 == ckR1
let r2 = waitfor rpcSeqParam(%[%"abc", %[1, 2, 3, 4, 5]])
2018-04-24 19:21:51 +01:00
var ckR2 = %["abc"]
for i in 0..4: ckR2.add %(i + 1)
check r2 == ckR2
test "Object parameters":
let
2018-05-02 16:21:05 +01:00
obj = %*{"a": %1, "b": %*{"d": %[5, 0], "e": %*{"x": %[1, 1]}}, "c": %1.23}
2018-05-03 20:20:10 +01:00
r = waitfor rpcObjParam(%[%"Test", obj])
check r == obj
2018-05-02 16:21:05 +01:00
expect ValueError:
# here we fail to provide one of the nested fields in json to the rpc
2018-05-03 20:20:10 +01:00
# TODO: Should this be allowed? We either allow partial non-ambiguous parsing or not
2018-05-02 16:21:05 +01:00
# Currently, as long as the Nim fields are satisfied, other fields are ignored
let
obj = %*{"a": %1, "b": %*{"a": %[5, 0]}, "c": %1.23}
discard waitFor rpcObjParam(%[%"abc", obj]) # Why doesn't asyncCheck raise?
2018-05-03 20:20:10 +01:00
test "Special types":
let r = waitfor rpcSpecialTypes(%[%5])
check r == %5
test "Runtime errors":
expect ValueError:
2018-05-01 20:32:28 +01:00
echo waitfor rpcArrayParam(%[%[0, 1, 2, 3, 4, 5, 6], %"hello"])