Parse objects by overload

This commit is contained in:
coffeepots 2018-05-08 11:51:24 +01:00 committed by zah
parent e9baad7e87
commit b4d42709b4

View File

@ -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..<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
result.expectKind(jsonIdent, fieldName, JArray)
let
formatType = derivedType[0].repr
expectedLen = genSym(nskConst)
var rootType: NimNode
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
result.add(quote do:
`expectedParamLen`
if `jsonIdent`.len > `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