From ee3ba6d5ada889ee4d9461251d7b82766f5a88b0 Mon Sep 17 00:00:00 2001 From: andri lim Date: Mon, 12 Nov 2018 17:47:03 +0700 Subject: [PATCH 1/2] add optional arg support to rpc macro --- json_rpc/jsonmarshal.nim | 112 ++++++++++++++++++++++++++++++--------- tests/testrpcmacro.nim | 18 ++++++- 2 files changed, 102 insertions(+), 28 deletions(-) diff --git a/json_rpc/jsonmarshal.nim b/json_rpc/jsonmarshal.nim index b38987a..c2b24f5 100644 --- a/json_rpc/jsonmarshal.nim +++ b/json_rpc/jsonmarshal.nim @@ -18,7 +18,7 @@ template expectType*(actual: JsonNodeKind, expected: typedesc, argName: string, 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,108 @@ 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) + +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) = + var + meetOptional = false + minLength = 0 + idx = 0 + + for arg, typ in paramsIter(parameters): + if typ.isOptionalArg: + if not meetOptional: minLength = idx + meetOptional = true + else: + if meetOptional: + macros.error("cannot have regular parameters: `" & $arg & "` after optional one", arg) + inc idx + + 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) + ) + +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: + let paramsWithOptionalArg = params.containsOptionalArg() + if paramsWithOptionalArg: + # more elaborate parameters array check + 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 + innerType = paramType[1] + innerNode = jsonToNim(paramIdent, innerType, jsonElement, paramName, true) result.add(quote do: - var `paramIdent`: `paramType` + if `jsonIdent`.len >= `pos`: `innerNode` ) + else: # unpack Nim type and assign from json result.add jsonToNim(paramIdent, paramType, jsonElement, paramName) diff --git a/tests/testrpcmacro.nim b/tests/testrpcmacro.nim index b052bd0..87f6ae8 100644 --- a/tests/testrpcmacro.nim +++ b/tests/testrpcmacro.nim @@ -34,7 +34,6 @@ let var s = newRpcSocketServer(["localhost:8545"]) # RPC definitions - s.rpc("rpc.simplepath"): result = %1 @@ -72,9 +71,14 @@ 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)) + # Tests suite "Server types": - test "On macro registration": check s.hasMethod("rpc.simplepath") check s.hasMethod("rpc.differentparams") @@ -85,6 +89,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 +161,14 @@ 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 + s.stop() waitFor s.closeWait() From a1fe7d57b4e56dcf390a3962f8930547153776cb Mon Sep 17 00:00:00 2001 From: andri lim Date: Fri, 16 Nov 2018 20:07:39 +0700 Subject: [PATCH 2/2] allow optional parameters in the middle of parameters list --- README.md | 44 ++++++++++++++++++++++++++++++++------ json_rpc/jsonmarshal.nim | 46 +++++++++++++++++++++++----------------- tests/testrpcmacro.nim | 24 +++++++++++++++++++++ 3 files changed, 89 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index defdde8..c11b152 100644 --- a/README.md +++ b/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. + diff --git a/json_rpc/jsonmarshal.nim b/json_rpc/jsonmarshal.nim index c2b24f5..c130954 100644 --- a/json_rpc/jsonmarshal.nim +++ b/json_rpc/jsonmarshal.nim @@ -151,6 +151,13 @@ iterator paramsIter(params: NimNode): tuple[name, ntype: NimNode] = 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 @@ -159,20 +166,12 @@ proc isOptionalArg(typeNode: NimNode): bool = result = typeNode[0].kind == nnkIdent and typeNode[0].strVal == "Option" -proc expectOptionalArrayLen(node, parameters: NimNode, jsonIdent: untyped, maxLength: int) = - var - meetOptional = false - minLength = 0 - idx = 0 +proc expectOptionalArrayLen(node, parameters: NimNode, jsonIdent: untyped, maxLength: int): int = + var minLength = maxLength - for arg, typ in paramsIter(parameters): - if typ.isOptionalArg: - if not meetOptional: minLength = idx - meetOptional = true - else: - if meetOptional: - macros.error("cannot have regular parameters: `" & $arg & "` after optional one", arg) - inc idx + for arg, typ in paramsRevIter(parameters): + if not typ.isOptionalArg: break + dec minLength let identStr = jsonIdent.repr @@ -184,6 +183,8 @@ proc expectOptionalArrayLen(node, parameters: NimNode, jsonIdent: untyped, maxLe raise newException(ValueError, `expectedStr` & $`jsonIdent`.len) ) + result = minLength + proc containsOptionalArg(params: NimNode): bool = for n, t in paramsIter(params): if t.isOptionalArg: @@ -214,10 +215,10 @@ proc jsonToNim*(params, jsonIdent: NimNode): NimNode = # Add code to verify input and load params into Nim types result = newStmtList() if not params.isNil: - let paramsWithOptionalArg = params.containsOptionalArg() - if paramsWithOptionalArg: + var minLength = 0 + if params.containsOptionalArg(): # more elaborate parameters array check - result.expectOptionalArrayLen(params, jsonIdent, + minLength = result.expectOptionalArrayLen(params, jsonIdent, calcActualParamCount(params)) else: # simple parameters array length check @@ -241,11 +242,18 @@ proc jsonToNim*(params, jsonIdent: NimNode): NimNode = if paramType.isOptionalArg: let + nullAble = pos < minLength innerType = paramType[1] innerNode = jsonToNim(paramIdent, innerType, jsonElement, paramName, true) - result.add(quote do: - if `jsonIdent`.len >= `pos`: `innerNode` - ) + + 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) diff --git a/tests/testrpcmacro.nim b/tests/testrpcmacro.nim index 87f6ae8..c5f6dbb 100644 --- a/tests/testrpcmacro.nim +++ b/tests/testrpcmacro.nim @@ -77,6 +77,23 @@ s.rpc("rpc.optionalArg") do(val: int, obj: Option[MyOptional]) -> MyOptional: 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": @@ -170,5 +187,12 @@ suite "Server types": 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() +