Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add framework to support more optional types #206

Merged
merged 2 commits into from
Feb 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 111 additions & 121 deletions json_rpc/private/server_handler_wrapper.nim
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import
stew/[byteutils, objects],
json_serialization,
json_serialization/std/[options],
json_serialization/stew/results,
../errors,
./jrpc_sys,
./shared_wrapper,
Expand All @@ -20,8 +21,26 @@ import
export
jsonmarshal

type
RpcSetup = object
numFields: int
numOptionals: int
minLength: int

{.push gcsafe, raises: [].}

# ------------------------------------------------------------------------------
# Optional resolvers
# ------------------------------------------------------------------------------

template rpc_isOptional(_: auto): bool = false
template rpc_isOptional[T](_: results.Opt[T]): bool = true
template rpc_isOptional[T](_: options.Option[T]): bool = true

# ------------------------------------------------------------------------------
# Run time helpers
# ------------------------------------------------------------------------------

proc unpackArg(args: JsonString, argName: string, argType: type): argType
{.gcsafe, raises: [JsonRpcError].} =
## This where input parameters are decoded from JSON into
Expand All @@ -33,146 +52,109 @@ proc unpackArg(args: JsonString, argName: string, argType: type): argType
"Parameter [" & argName & "] of type '" &
$argType & "' could not be decoded: " & err.msg)

proc expectArrayLen(node, paramsIdent: NimNode, length: int) =
## Make sure positional params meets the handler expectation
let
expected = "Expected " & $length & " Json parameter(s) but got "
node.add quote do:
if `paramsIdent`.positional.len != `length`:
raise newException(RequestDecodeError, `expected` &
$`paramsIdent`.positional.len)

iterator paramsRevIter(params: NimNode): tuple[name, ntype: NimNode] =
## Bacward iterator of handler parameters
for i in countdown(params.len-1,1):
let arg = params[i]
let argType = arg[^2]
for j in 0 ..< arg.len-2:
yield (arg[j], argType)

proc isOptionalArg(typeNode: NimNode): bool =
# typed version
(typeNode.kind == nnkCall and
typeNode.len > 1 and
typeNode[1].kind in {nnkIdent, nnkSym} and
typeNode[1].strVal == "Option") or

# untyped version
(typeNode.kind == nnkBracketExpr and
typeNode[0].kind == nnkIdent and
typeNode[0].strVal == "Option")

proc expectOptionalArrayLen(node: NimNode,
parameters: NimNode,
paramsIdent: NimNode,
maxLength: int): int =
## Validate if parameters sent by client meets
## minimum expectation of server
var minLength = maxLength

for arg, typ in paramsRevIter(parameters):
if not typ.isOptionalArg: break
dec minLength
# ------------------------------------------------------------------------------
# Compile time helpers
# ------------------------------------------------------------------------------
func hasOptionals(setup: RpcSetup): bool {.compileTime.} =
setup.numOptionals > 0

func rpcSetupImpl[T](val: T): RpcSetup {.compileTime.} =
## Counting number of fields, optional fields, and
## minimum fields needed by a rpc method
mixin rpc_isOptional
var index = 1
for field in fields(val):
inc result.numFields
if rpc_isOptional(field):
inc result.numOptionals
else:
result.minLength = index
inc index

func rpcSetupFromType(T: type): RpcSetup {.compileTime.} =
var dummy: T
rpcSetupImpl(dummy)

template expectOptionalParamsLen(params: RequestParamsRx,
minLength, maxLength: static[int]) =
## Make sure positional params with optional fields
## meets the handler expectation
let
expected = "Expected at least " & $minLength & " and maximum " &
$maxLength & " Json parameter(s) but got "

node.add quote do:
if `paramsIdent`.positional.len < `minLength`:
raise newException(RequestDecodeError, `expected` &
$`paramsIdent`.positional.len)
if params.positional.len < minLength:
raise newException(RequestDecodeError,
expected & $params.positional.len)

minLength
template expectParamsLen(params: RequestParamsRx, length: static[int]) =
## Make sure positional params meets the handler expectation
let
expected = "Expected " & $length & " Json parameter(s) but got "

proc containsOptionalArg(params: NimNode): bool =
## Is one of handler parameters an optional?
for n, t in paramsIter(params):
if t.isOptionalArg:
return true
if params.positional.len != length:
raise newException(RequestDecodeError,
expected & $params.positional.len)

proc jsonToNim(paramVar: NimNode,
paramType: NimNode,
paramVal: NimNode,
paramName: string): NimNode =
## Convert a positional parameter from Json into Nim
result = quote do:
`paramVar` = `unpackArg`(`paramVal`, `paramName`, `paramType`)
template setupPositional(setup: static[RpcSetup], params: RequestParamsRx) =
## Generate code to check positional params length
when setup.hasOptionals:
expectOptionalParamsLen(params, setup.minLength, setup.numFields)
else:
expectParamsLen(params, setup.numFields)

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 n, t in paramsIter(params):
inc result
template len(params: RequestParamsRx): int =
params.positional.len

proc makeType(typeName, params: NimNode): NimNode =
## Generate type section contains an object definition
## with fields of handler params
let typeSec = quote do:
type `typeName` = object
template notNull(params: RequestParamsRx, pos: int): bool =
params.positional[pos].kind != JsonValueKind.Null

let obj = typeSec[0][2]
let recList = newNimNode(nnkRecList)
if params.len > 1:
for i in 1..<params.len:
recList.add params[i]
obj[2] = recList
typeSec
template val(params: RequestParamsRx, pos: int): auto =
params.positional[pos].param

proc setupPositional(params, paramsIdent: NimNode): (NimNode, int) =
## Generate code to check positional params length
var
minLength = 0
code = newStmtList()

if params.containsOptionalArg():
# more elaborate parameters array check
minLength = code.expectOptionalArrayLen(params, paramsIdent,
calcActualParamCount(params))
else:
# simple parameters array length check
code.expectArrayLen(paramsIdent, calcActualParamCount(params))

(code, minLength)
template unpackPositional(params: RequestParamsRx,
paramVar: auto,
paramName: static[string],
pos: static[int],
setup: static[RpcSetup],
paramType: type) =
## Convert a positional parameter from Json into Nim

proc setupPositional(code: NimNode;
paramsObj, paramsIdent, paramIdent, paramType: NimNode;
pos, minLength: int) =
## processing multiple params of one type
## e.g. (a, b: T), including common (a: U, b: V) form
let
paramName = $paramIdent
paramVal = quote do:
`paramsIdent`.positional[`pos`].param
paramKind = quote do:
`paramsIdent`.positional[`pos`].kind
paramVar = quote do:
`paramsObj`.`paramIdent`
innerNode = jsonToNim(paramVar, paramType, paramVal, paramName)
template innerNode() =
paramVar = unpackArg(params.val(pos), paramName, paramType)

# e.g. (A: int, B: Option[int], C: string, D: Option[int], E: Option[string])
if paramType.isOptionalArg:
if pos >= minLength:
when rpc_isOptional(paramVar):
when pos >= setup.minLength:
# allow both empty and null after mandatory args
# D & E fall into this category
code.add quote do:
if `paramsIdent`.positional.len > `pos` and
`paramKind` != JsonValueKind.Null:
`innerNode`
if params.len > pos and params.notNull(pos):
innerNode()
else:
# allow null param for optional args between/before mandatory args
# B fall into this category
code.add quote do:
if `paramKind` != JsonValueKind.Null:
`innerNode`
if params.notNull(pos):
innerNode()
else:
# mandatory args
# A and C fall into this category
# unpack Nim type and assign from json
code.add quote do:
if `paramKind` != JsonValueKind.Null:
`innerNode`
if params.notNull(pos):
innerNode()

proc makeType(typeName, params: NimNode): NimNode =
## Generate type section contains an object definition
## with fields of handler params
let typeSec = quote do:
type `typeName` = object

let obj = typeSec[0][2]
let recList = newNimNode(nnkRecList)
if params.len > 1:
for i in 1..<params.len:
recList.add params[i]
obj[2] = recList
typeSec

proc makeParams(retType: NimNode, params: NimNode): seq[NimNode] =
## Convert rpc params into handler params
Expand Down Expand Up @@ -262,13 +244,14 @@ proc wrapServerHandler*(methName: string, params, procBody, procWrapper: NimNode
paramsIdent = genSym(nskParam, "rpcParams")
returnType = params[0]
hasParams = params.len > 1 # not including return type
(posSetup, minLength) = setupPositional(params, paramsIdent)
rpcSetup = ident"rpcSetup"
handler = makeHandler(handlerName, params, procBody, returnType)
named = setupNamed(paramsObj, paramsIdent, params)

if hasParams:
setup.add makeType(typeName, params)
setup.add quote do:
const `rpcSetup` = rpcSetupFromType(`typeName`)
var `paramsObj`: `typeName`

# unpack each parameter and provide assignments
Expand All @@ -278,16 +261,23 @@ proc wrapServerHandler*(methName: string, params, procBody, procWrapper: NimNode
executeParams: seq[NimNode]

for paramIdent, paramType in paramsIter(params):
positional.setupPositional(paramsObj, paramsIdent,
paramIdent, paramType, pos, minLength)
let paramName = $paramIdent
positional.add quote do:
unpackPositional(`paramsIdent`,
`paramsObj`.`paramIdent`,
`paramName`,
`pos`,
`rpcSetup`,
`paramType`)

executeParams.add quote do:
`paramsObj`.`paramIdent`
inc pos

if hasParams:
setup.add quote do:
if `paramsIdent`.kind == rpPositional:
`posSetup`
setupPositional(`rpcSetup`, `paramsIdent`)
`positional`
else:
`named`
Expand All @@ -297,7 +287,7 @@ proc wrapServerHandler*(methName: string, params, procBody, procWrapper: NimNode
# still be checked (RPC spec)
setup.add quote do:
if `paramsIdent`.kind == rpPositional:
`posSetup`
expectParamsLen(`paramsIdent`, 0)

let
awaitedResult = ident "awaitedResult"
Expand Down
2 changes: 1 addition & 1 deletion json_rpc/router.nim
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ proc route*(router: RpcRouter, req: RequestRx):
let methodName = req.meth.get # this Opt already validated
debug "Error occurred within RPC",
methodName = methodName, err = err.msg
return serverError(methodName & " raised an exception",
return serverError("`" & methodName & "` raised an exception",
escapeJson(err.msg).JsonString).
wrapError(req.id)

Expand Down
6 changes: 3 additions & 3 deletions tests/test_batch_call.nim
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ suite "Socket batch call":
check r[1].result.string == "\"apple: green\""

check r[2].error.isSome
check r[2].error.get == """{"code":-32000,"message":"get_except raised an exception","data":"get_except error"}"""
check r[2].error.get == """{"code":-32000,"message":"`get_except` raised an exception","data":"get_except error"}"""
check r[2].result.string.len == 0

test "rpc call after batch call":
Expand Down Expand Up @@ -95,7 +95,7 @@ suite "HTTP batch call":
check r[1].result.string == "\"apple: green\""

check r[2].error.isSome
check r[2].error.get == """{"code":-32000,"message":"get_except raised an exception","data":"get_except error"}"""
check r[2].error.get == """{"code":-32000,"message":"`get_except` raised an exception","data":"get_except error"}"""
check r[2].result.string.len == 0

test "rpc call after batch call":
Expand Down Expand Up @@ -134,7 +134,7 @@ suite "Websocket batch call":
check r[1].result.string == "\"apple: green\""

check r[2].error.isSome
check r[2].error.get == """{"code":-32000,"message":"get_except raised an exception","data":"get_except error"}"""
check r[2].error.get == """{"code":-32000,"message":"`get_except` raised an exception","data":"get_except error"}"""
check r[2].result.string.len == 0

test "rpc call after batch call":
Expand Down
Loading