diff --git a/eth-rpc/server/servertypes.nim b/eth-rpc/server/servertypes.nim index a75c81e..1fcf9a2 100644 --- a/eth-rpc/server/servertypes.nim +++ b/eth-rpc/server/servertypes.nim @@ -81,42 +81,19 @@ macro multiRemove(s: string, values: varargs[string]): untyped = body.add multiReplaceCall result = newBlockStmt(body) -proc jsonGetFunc(paramType: string): (NimNode, JsonNodeKind) = - # Unknown types get attempted as int - 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) - else: - if paramType == "byte" or paramType[0..3] == "uint" or paramType[0..2] == "int": - result = (ident"getInt", JInt) +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: - result = (nil, JInt) + var t = typeNode[i] + if preParseTypes(t, typeName, errorCheck): + typeNode[i] = t -proc jsonTranslate(translation: var NimNode, paramType: string): NimNode = - # TODO: Remove or rework this into `translate` - 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) - of "int8": - 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 - -proc expectKind(node, jsonIdent, fieldName: NimNode, tn: JsonNodeKind) = +proc expect(node, jsonIdent, fieldName: NimNode, tn: JsonNodeKind) = let expectedStr = "Expected parameter `" & fieldName.repr & "` to be " & $tn & " but got " tnIdent = ident($tn) @@ -124,148 +101,46 @@ proc expectKind(node, jsonIdent, fieldName: NimNode, tn: JsonNodeKind) = if `jsonIdent`.kind != `tnIdent`: raise newException(ValueError, `expectedStr` & $`jsonIdent`.kind) ) - -proc getDigit(s: string): (bool, int) = - if s.len == 0: return (false, 0) - for c in s: - if not c.isDigit: return (false, 0) - return (true, s.parseInt) - - -from math import pow - -proc translate(paramTypeStr: string, getField: NimNode): NimNode = - # Add checking and type conversion for more constrained types - # Note: - # * specific types add extra run time bounds checking code - # * types that map one-one get passed as is - # * any other types get a simple cast, ie; MyType(value) and - # get are assumed to be integer. - # NOTE: However this will never occur because currently jsonFunc - # is required to return nil to process other types. - # TODO: Allow distinct types - var paramType = paramTypeStr - if paramType == "byte": paramType = "uint8" - - case paramType - of "string", "int", "bool": - result = quote do: `getField` - else: - if paramType[0 .. 3].toLowerAscii == "uint": - let (numeric, bitSize) = paramType[4 .. high(paramType)].getDigit - if numeric: - assert bitSize mod 8 == 0 - let - maxSize = 1 shl bitSize - 1 - sizeRangeStr = "0 to " & $maxSize - uintType = ident("uint" & $bitSize) - result = quote do: - let x = `getField` - if x > `maxSize` or x < 0: - raise newException(ValueError, "Value out of range of byte, expected " & `sizeRangeStr` & ", got " & $x) - `uintType`(x) - elif paramType[0 .. 2].toLowerAscii == "int": - let (numeric, bitSize) = paramType[3 .. paramType.high].getDigit - if numeric: - assert bitSize mod 8 == 0 - let - maxSize = 1 shl (bitSize - 1) - minVal = -maxSize - maxVal = maxSize - 1 - sizeRangeStr = $minVal & " to " & $maxVal - intType = ident("int" & $bitSize) - result = quote do: - let x = `getField` - if x < `minVal` or x > `maxVal`: - raise newException(ValueError, "Value out of range of byte, expected " & `sizeRangeStr` & ", got " & $x) - `intType`(x) - else: - let nativeParamType = ident(paramTypeStr) - result = quote do: `nativeParamType`(`getField`) - -macro processFields(jsonIdent, fieldName, fieldType: typed): untyped = - result = newStmtList() - let - fieldTypeStr = fieldType.repr.toLowerAscii() - (jFetch, jKind) = jsonGetFunc(fieldTypeStr) - if not jFetch.isNil: - # TODO: getType(fieldType) to translate byte -> uint8 and avoid special cases - result.expectKind(jsonIdent, fieldName, jKind) - let - getField = quote do: `jsonIdent`.`jFetch` - res = translate(`fieldTypeStr`, `getField`) - result.add(quote do: - `fieldName` = `res` - ) - else: - var fetchedType = getType(fieldType) - var derivedType: NimNode - if fetchedType[0].repr == "typeDesc": - derivedType = getType(fetchedType[1]) - else: - derivedType = fetchedType - if derivedType.kind == nnkObjectTy: - result.expectKind(jsonIdent, fieldName, JObject) - let recs = derivedType.findChild it.kind == nnkRecList - for i in 0.. `expectedLen`: - raise newException(ValueError, `expectedLenStr` & $`expectedLen` & " but got " & $`jsonIdent`.len) - ) - rootType = derivedType[2] - of "seq": - result.add(quote do: - `fieldName` = @[] - `fieldName`.setLen(`jsonIdent`.len) - ) - rootType = derivedType[1] - else: - raise newException(ValueError, "Cannot determine bracket expression type of \"" & derivedType.treerepr & "\"") - # add fetch code for array/seq - var translation: NimNode - let - (jFunc, jKind) = jsonGetFunc(($rootType).toLowerAscii) - transIdent = translation.jsonTranslate($rootType) - # TODO: Add checks PER ITEM (performance hit!) in the array, if required by the type - # TODO: Refactor `jsonTranslate` into `translate` - result.add(quote do: - `translation` - for i in 0 ..< `jsonIdent`.len: - `fieldName`[i] = `transIdent`(`jsonIdent`.elems[i].`jFunc`) - ) - else: - raise newException(ValueError, "Unknown type \"" & derivedType.treerepr & "\"") - when defined(nimDumpRpcs): - echo result.repr +### + +proc fromJson(n: JsonNode, result: var int) = + # TODO: validate... + result = n.getInt() + +proc fromJson(n: JsonNode, result: var byte) = + let v = n.getInt() + if v > 255: raise newException(ValueError, "Parameter value to large for byte: " & $v) + result = byte(v) + +proc fromJson(n: JsonNode, result: var float) = + # TODO: validate... + result = n.getFloat() + +proc fromJson(n: JsonNode, result: var string) = + # TODO: validate... + result = n.getStr() + +proc fromJson[T](n: JsonNode, result: var seq[T]) = + # TODO: validate... + result = newSeq[T](n.len) + for i in 0 ..< n.len: + fromJson(n[i], result[i]) + +proc fromJson[N, T](n: JsonNode, result: var array[N, T]) = + # TODO: validate... + if n.len > result.len: raise newException(ValueError, "Parameter data too big for array") + for i in 0..< n.len: + fromJson(n[i], result[i]) + +proc fromJson[T: object](n: JsonNode, result: var T) = # This reads a custom object + # TODO: validate... + for k, v in fieldpairs(result): + fromJson(n[k], v) + +proc unpackArg[T](argIdx: int, argName: string, argtype: typedesc[T], args: JsonNode): T = + echo argName, " ", args.pretty + fromJson(args[argIdx], result) proc setupParams(node, parameters, paramsIdent: NimNode) = # recurse parameter's fields until we only have symbols @@ -279,19 +154,18 @@ proc setupParams(node, parameters, paramsIdent: NimNode) = raise newException(ValueError, `expectedStr` & $`paramsIdent`.len) ) - for i in 1..< parameters.len: + for i in 1 ..< parameters.len: let paramName = parameters[i][0] pos = i - 1 + paramNameStr = $paramName var paramType = parameters[i][1] - #discard paramType.preParseTypes(paramName, errorCheck) node.add(quote do: - var `paramName`: `paramType` - processFields(`paramsIdent`[`pos`], `paramName`, `paramType`) + var `paramName` = `unpackArg`(`pos`, `paramNameStr`, `paramType`, `paramsIdent`) + `errorCheck` ) - # TODO: Check for byte ranges macro on*(server: var RpcServer, path: string, body: untyped): untyped = result = newStmtList() @@ -320,6 +194,13 @@ macro on*(server: var RpcServer, path: string, body: untyped): untyped = when isMainModule: import unittest var s = newRpcServer("localhost") + s.on("rpc.simplepath"): + result = %1 + s.on("rpc.returnint") do() -> int: + result = %2 + s.on("rpc.differentparams") do(a: int, b: string): + var node = %"test" + result = node s.on("rpc.arrayparam") do(arr: array[0..5, byte], b: string): var res = newJArray() for item in arr: @@ -335,24 +216,25 @@ when isMainModule: type Test2 = object - x: array[2, int] - Test = object - d: array[0..1, int] - e: Test2 + x: array[0..2, int] + y: string - type MyObject* = object - a: int - b: Test - c: float + Test = object + a: array[0..1, int] + b: Test2 + + MyObject* = object + a: int + b: Test + c: float s.on("rpc.objparam") do(a: string, obj: MyObject): result = %obj - s.on("rpc.uinttypes") do(a: byte, b: uint16, c: uint32): - result = %[int(a), int(b), int(c)] - s.on("rpc.inttypes") do(a: int8, b: int16, c: int32, d: int8, e: int16, f: int32): - result = %[int(a), int(b), int(c), int(d), int(e), int(f)] - suite "Server types": + test "On macro registration": + check s.procs.hasKey("rpc.simplepath") + check s.procs.hasKey("rpc.returnint") + check s.procs.hasKey("rpc.returnint") test "Array/seq parameters": let r1 = waitfor rpcArrayParam(%[%[1, 2, 3], %"hello"]) var ckR1 = %[1, 2, 3, 0, 0, 0] @@ -365,32 +247,20 @@ when isMainModule: check r2 == ckR2 test "Object parameters": let - obj = %*{"a": %1, "b": %*{"d": %[5, 0], "e": %*{"x": %[1, 1]}}, "c": %1.23} - r = waitfor rpcObjParam(%[%"Test", obj]) + obj = %*{ + "a": %1, + "b": %*{ + "a": %[5, 0], + "b": %*{ + "x": %[1, 2, 3], + "y": %"test" + } + }, + "c": %1.23} + r = waitfor rpcObjParam(%[%"abc", obj]) check r == obj - expect ValueError: - # here we fail to provide one of the nested fields in json to the rpc - # TODO: Should this be allowed? We either allow partial non-ambiguous parsing or not - # 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? - test "Uint types": - let - testCase = %[%255, %65534, %4294967295] - r = waitfor rpcUIntTypes(testCase) - check r == testCase - test "Int types": - let - testCase = %[ - %(127), %(32767), %(2147483647), - %(-128), %(-32768), %(-2147483648) - ] - r = waitfor rpcIntTypes(testCase) - check r == testCase test "Runtime errors": expect ValueError: echo waitfor rpcArrayParam(%[%[0, 1, 2, 3, 4, 5, 6], %"hello"]) -# TODO: Split runtime strictness checking into defines - is there ever a reason to trust input? -# TODO: Add path as constant for each rpc +