Merge pull request #42 from jangko/rpc_macro

add optional arg support to rpc macro
This commit is contained in:
coffeepots 2018-11-19 10:58:38 +00:00 committed by GitHub
commit 5e7f2d6a61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 174 additions and 36 deletions

View File

@ -74,7 +74,7 @@ Here is a more complex parameter example:
```nim
type
HeaderKind = enum hkOne, hkTwo, hkThree
Header = ref object
kind: HeaderKind
size: int64
@ -88,7 +88,7 @@ type
name: string
router.rpc("updateData") do(myObj: MyObject, newData: DataBlob) -> DataBlob:
result = myObj.data
result = myObj.data
myObj.data = newData
```
@ -97,6 +97,37 @@ At runtime, the json is checked to ensure that it contains the correct number an
Compiling with `-d:nimDumpRpcs` will show the output code for the RPC call. To see the output of the `async` generation, add `-d:nimDumpAsync`.
### Special type : `Option[T] `
Option[T] is a special type indicating that parameter may have value or not.
* If optional parameters located in the middle of parameters list, you set it to `null` to tell the server that it has no value.
* If optional parameters located at the end of parameter list and there are no more mandatory parameters after that, those optional parameters can be omitted altogether.
```Nim
# d can be omitted, b should use null to indicate it has no value
router.rpc("updateData") do(a: int, b: Option[int], c: string, d: Option[T]):
if b.isSome:
# do something
else:
# do something else
```
* If Option[T] used as return type, it also denotes the returned value might not available.
```Nim
router.rpc("getData") do(name: string) -> Option[int]:
if name == "monkey":
result = some(4)
```
* If Option[T] used as field type of an object, it also tell us that field might be present or not, and the rpc mechanism will automatically set it to some value if it available.
```Nim
type
MyOptional = object
maybeInt: Option[int]
```
## Marshalling
Note that `array` parameters are explicitly checked for length, and will return an error node if the length differs from their declaration size.
@ -202,7 +233,7 @@ This `route` variant will handle all the conversion of `string` to `JsonNode` an
#### Returns
`Future[string]`: This will be the stringified JSON response, which can be the JSON RPC result or a JSON wrapped error.
### `route` by `JsonNode`
This variant allows simplified processing if you already have a `JsonNode`. However if the required fields are not present within `node`, exceptions will be raised.
@ -216,7 +247,7 @@ This variant allows simplified processing if you already have a `JsonNode`. Howe
#### Returns
`Future[JsonNode]`: The JSON RPC result or a JSON wrapped error.
### `tryRoute`
This `route` variant allows you to invoke a call if possible, without raising an exception.
@ -232,7 +263,7 @@ This `route` variant allows you to invoke a call if possible, without raising an
#### Returns
`bool`: `true` if the `method` field provided in `node` matches an available route. Returns `false` when the `method` cannot be found, or if `method` or `params` field cannot be found within `node`.
To see the result of a call, we need to provide Json in the expected format.
Here's an example of how that looks by manually creating the JSON. Later we will see the helper utilities that make this easier.
@ -280,7 +311,7 @@ import json_rpc/rpcserver
# Create a socket server for transport
var srv = newRpcSocketServer("localhost", Port(8585))
# srv.rpc is a shortcut for srv.router.rpc
# srv.rpc is a shortcut for srv.router.rpc
srv.rpc("hello") do(input: string) -> string:
result = "Hello " & input
@ -413,3 +444,4 @@ Licensed and distributed under either of
at your option. This file may not be copied, modified, or distributed except according to those terms.

View File

@ -18,7 +18,7 @@ proc expectType*(actual: JsonNodeKind, expected: typedesc, argName: string, allo
elif expected is string:
expType = JString
else:
const eStr = "Unable to convert " & expected.name & " to JSON for expectType"
const eStr = "Unable to convert " & expected.name & " to JSON for expectType"
{.fatal: eStr}
if actual != expType:
if allowNull == false or (allowNull and actual != JNull):
@ -144,48 +144,116 @@ proc expectArrayLen(node: NimNode, jsonIdent: untyped, length: int) =
raise newException(ValueError, `expectedStr` & $`jsonIdent`.len)
)
proc jsonToNim*(assignIdent, paramType, jsonIdent: NimNode, paramNameStr: string): NimNode =
iterator paramsIter(params: NimNode): tuple[name, ntype: NimNode] =
for i in 1 ..< params.len:
let arg = params[i]
let argType = arg[^2]
for j in 0 ..< arg.len-2:
yield (arg[j], argType)
iterator paramsRevIter(params: NimNode): tuple[name, ntype: NimNode] =
for i in countDown(params.len-1,0):
let arg = params[i]
let argType = arg[^2]
for j in 0 ..< arg.len-2:
yield (arg[j], argType)
proc isOptionalArg(typeNode: NimNode): bool =
if typeNode.kind != nnkBracketExpr:
result = false
return
result = typeNode[0].kind == nnkIdent and
typeNode[0].strVal == "Option"
proc expectOptionalArrayLen(node, parameters: NimNode, jsonIdent: untyped, maxLength: int): int =
var minLength = maxLength
for arg, typ in paramsRevIter(parameters):
if not typ.isOptionalArg: break
dec minLength
let
identStr = jsonIdent.repr
expectedStr = "Expected at least " & $minLength & " and maximum " & $maxLength & " Json parameter(s) but got "
node.add(quote do:
`jsonIdent`.kind.expect(JArray, `identStr`)
if `jsonIdent`.len < `minLength`:
raise newException(ValueError, `expectedStr` & $`jsonIdent`.len)
)
result = minLength
proc containsOptionalArg(params: NimNode): bool =
for n, t in paramsIter(params):
if t.isOptionalArg:
result = true
break
proc jsonToNim*(assignIdent, paramType, jsonIdent: NimNode, paramNameStr: string, optional = false): NimNode =
# verify input and load a Nim type from json data
# note: does not create `assignIdent`, so can be used for `result` variables
result = newStmtList()
# unpack each parameter and provide assignments
result.add(quote do:
`assignIdent` = `unpackArg`(`jsonIdent`, `paramNameStr`, type(`paramType`))
)
let unpackNode = quote do:
`unpackArg`(`jsonIdent`, `paramNameStr`, type(`paramType`))
proc calcActualParamCount(parameters: NimNode): int =
if optional:
result.add(quote do: `assignIdent` = `some`(`unpackNode`))
else:
result.add(quote do: `assignIdent` = `unpackNode`)
proc calcActualParamCount(params: NimNode): int =
# this proc is needed to calculate the actual parameter count
# not matter what is the declaration form
# e.g. (a: U, b: V) vs. (a, b: T)
for i in 1 ..< parameters.len:
inc(result, parameters[i].len-2)
for n, t in paramsIter(params):
inc result
proc jsonToNim*(parameters, jsonIdent: NimNode): NimNode =
# Add code to verify input and load parameters into Nim types
proc jsonToNim*(params, jsonIdent: NimNode): NimNode =
# Add code to verify input and load params into Nim types
result = newStmtList()
if not parameters.isNil:
# initial parameter array length check
result.expectArrayLen(jsonIdent, calcActualParamCount(parameters))
if not params.isNil:
var minLength = 0
if params.containsOptionalArg():
# more elaborate parameters array check
minLength = result.expectOptionalArrayLen(params, jsonIdent,
calcActualParamCount(params))
else:
# simple parameters array length check
result.expectArrayLen(jsonIdent, calcActualParamCount(params))
# unpack each parameter and provide assignments
var pos = 0
for i in 1 ..< parameters.len:
let
param = parameters[i]
paramType = param[^2]
for paramIdent, paramType in paramsIter(params):
# processing multiple variables of one type
# e.g. (a, b: T), including common (a: U, b: V) form
for j in 0 ..< param.len-2:
let
paramName = $paramIdent
jsonElement = quote do:
`jsonIdent`.elems[`pos`]
inc pos
# declare variable before assignment
result.add(quote do:
var `paramIdent`: `paramType`
)
if paramType.isOptionalArg:
let
paramIdent = param[j]
paramName = $paramIdent
jsonElement = quote do:
`jsonIdent`.elems[`pos`]
inc pos
# declare variable before assignment
result.add(quote do:
var `paramIdent`: `paramType`
)
nullAble = pos < minLength
innerType = paramType[1]
innerNode = jsonToNim(paramIdent, innerType, jsonElement, paramName, true)
if nullAble:
result.add(quote do:
if `jsonElement`.kind != JNull: `innerNode`
)
else:
result.add(quote do:
if `jsonIdent`.len >= `pos`: `innerNode`
)
else:
# unpack Nim type and assign from json
result.add jsonToNim(paramIdent, paramType, jsonElement, paramName)

View File

@ -34,7 +34,6 @@ let
var s = newRpcSocketServer(["localhost:8545"])
# RPC definitions
s.rpc("rpc.simplepath"):
result = %1
@ -72,9 +71,31 @@ s.rpc("rpc.multivarsofonetype") do(a, b: string) -> string:
s.rpc("rpc.optional") do(obj: MyOptional) -> MyOptional:
result = obj
s.rpc("rpc.optionalArg") do(val: int, obj: Option[MyOptional]) -> MyOptional:
if obj.isSome():
result = obj.get()
else:
result = MyOptional(maybeInt: some(val))
type
OptionalFields = object
a: int
b: Option[int]
c: string
d: Option[int]
e: Option[string]
s.rpc("rpc.mixedOptionalArg") do(a: int, b: Option[int], c: string,
d: Option[int], e: Option[string]) -> OptionalFields:
result.a = a
result.b = b
result.c = c
result.d = d
result.e = e
# Tests
suite "Server types":
test "On macro registration":
check s.hasMethod("rpc.simplepath")
check s.hasMethod("rpc.differentparams")
@ -85,6 +106,7 @@ suite "Server types":
check s.hasMethod("rpc.returntypecomplex")
check s.hasMethod("rpc.testreturns")
check s.hasMethod("rpc.multivarsofonetype")
check s.hasMethod("rpc.optionalArg")
test "Simple paths":
let r = waitFor rpcSimplePath(%[])
@ -156,5 +178,21 @@ suite "Server types":
let r = waitfor rpcMultiVarsOfOneType(%[%"hello", %"world"])
check r == %"hello world"
test "Optional arg":
let
int1 = MyOptional(maybeInt: some(75))
int2 = MyOptional(maybeInt: some(117))
r1 = waitFor rpcOptionalArg(%[%117, %int1])
r2 = waitFor rpcOptionalArg(%[%117])
check r1 == %int1
check r2 == %int2
test "mixed optional arg":
var ax = waitFor rpcMixedOptionalArg(%[%10, %11, %"hello", %12, %"world"])
check ax == %OptionalFields(a: 10, b: some(11), c: "hello", d: some(12), e: some("world"))
var bx = waitFor rpcMixedOptionalArg(%[%10, newJNull(), %"hello"])
check bx == %OptionalFields(a: 10, c: "hello")
s.stop()
waitFor s.closeWait()