Merge pull request #42 from jangko/rpc_macro
add optional arg support to rpc macro
This commit is contained in:
commit
5e7f2d6a61
44
README.md
44
README.md
|
@ -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.
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
Loading…
Reference in New Issue