mirror of
https://github.com/logos-storage/nim-json-rpc.git
synced 2026-01-02 21:53:12 +00:00
319 lines
9.8 KiB
Nim
319 lines
9.8 KiB
Nim
# json-rpc
|
|
# Copyright (c) 2019-2024 Status Research & Development GmbH
|
|
# Licensed under either of
|
|
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
|
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
|
# at your option.
|
|
# This file may not be copied, modified, or distributed except according to
|
|
# those terms.
|
|
|
|
import
|
|
std/[macros, typetraits],
|
|
stew/[byteutils, objects],
|
|
json_serialization,
|
|
json_serialization/std/[options],
|
|
../errors,
|
|
./jrpc_sys,
|
|
./shared_wrapper,
|
|
../jsonmarshal
|
|
|
|
export
|
|
jsonmarshal
|
|
|
|
{.push gcsafe, raises: [].}
|
|
|
|
proc unpackArg(args: JsonString, argName: string, argType: type): argType
|
|
{.gcsafe, raises: [JsonRpcError].} =
|
|
## This where input parameters are decoded from JSON into
|
|
## Nim data types
|
|
try:
|
|
result = JrpcConv.decode(args.string, argType)
|
|
except CatchableError as err:
|
|
raise newException(RequestDecodeError,
|
|
"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
|
|
|
|
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)
|
|
|
|
minLength
|
|
|
|
proc containsOptionalArg(params: NimNode): bool =
|
|
## Is one of handler parameters an optional?
|
|
for n, t in paramsIter(params):
|
|
if t.isOptionalArg:
|
|
return true
|
|
|
|
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`)
|
|
|
|
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
|
|
|
|
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 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)
|
|
|
|
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)
|
|
|
|
# e.g. (A: int, B: Option[int], C: string, D: Option[int], E: Option[string])
|
|
if paramType.isOptionalArg:
|
|
if pos >= 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`
|
|
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`
|
|
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`
|
|
|
|
proc makeParams(retType: NimNode, params: NimNode): seq[NimNode] =
|
|
## Convert rpc params into handler params
|
|
result.add retType
|
|
if params.len > 1:
|
|
for i in 1..<params.len:
|
|
result.add params[i]
|
|
|
|
proc makeHandler(procName, params, procBody, returnInner: NimNode): NimNode =
|
|
## Generate rpc handler proc
|
|
let
|
|
returnType = quote do: Future[`returnInner`]
|
|
paramList = makeParams(returnType, params)
|
|
pragmas = quote do: {.async.}
|
|
|
|
result = newProc(
|
|
name = procName,
|
|
params = paramList,
|
|
body = procBody,
|
|
pragmas = pragmas
|
|
)
|
|
|
|
proc ofStmt(x, paramsObj, paramName, paramType: NimNode): NimNode =
|
|
let caseStr = $paramName
|
|
result = nnkOfBranch.newTree(
|
|
quote do: `caseStr`,
|
|
quote do:
|
|
`paramsObj`.`paramName` = unpackArg(`x`.value, `caseStr`, `paramType`)
|
|
)
|
|
|
|
proc setupNamed(paramsObj, paramsIdent, params: NimNode): NimNode =
|
|
let x = ident"x"
|
|
|
|
var caseStmt = nnkCaseStmt.newTree(
|
|
quote do: `x`.name
|
|
)
|
|
|
|
for paramName, paramType in paramsIter(params):
|
|
caseStmt.add ofStmt(x, paramsObj, paramName, paramType)
|
|
|
|
caseStmt.add nnkElse.newTree(
|
|
quote do: discard
|
|
)
|
|
|
|
result = quote do:
|
|
for `x` in `paramsIdent`.named:
|
|
`caseStmt`
|
|
|
|
proc wrapServerHandler*(methName: string, params, procBody, procWrapper: NimNode): NimNode =
|
|
## This proc generate something like this:
|
|
##
|
|
## proc rpcHandler(paramA: ParamAType, paramB: ParamBType): Future[ReturnType] =
|
|
## procBody
|
|
## return retVal
|
|
##
|
|
## proc rpcWrapper(params: RequestParamsRx): Future[JsonString] =
|
|
## type
|
|
## RpcType = object
|
|
## paramA: ParamAType
|
|
## paramB: ParamBType
|
|
##
|
|
## var rpcVar: RpcType
|
|
##
|
|
## if params.isPositional:
|
|
## if params.positional.len < expectedLen:
|
|
## raise exception
|
|
## rpcVar.paramA = params.unpack(paramA of ParamAType)
|
|
## rpcVar.paramB = params.unpack(paramB of ParamBType)
|
|
## else:
|
|
## # missing parameters is ok in named mode
|
|
## # the default value will be used
|
|
## for x in params.named:
|
|
## case x.name
|
|
## of "paramA": rpcVar.paramA = params.unpack(paramA of ParamAType)
|
|
## of "paramB": rpcVar.paramB = params.unpack(paramB of ParamBType)
|
|
## else: discard
|
|
##
|
|
## let res = await rpcHandler(rpcVar.paramA, rpcVar.paramB)
|
|
## return JrpcConv.encode(res).JsonString
|
|
|
|
let
|
|
params = params.ensureReturnType()
|
|
setup = newStmtList()
|
|
typeName = genSym(nskType, "RpcType")
|
|
paramsObj = ident"rpcVar"
|
|
handlerName = genSym(nskProc, methName & "_rpcHandler")
|
|
paramsIdent = genSym(nskParam, "rpcParams")
|
|
returnType = params[0]
|
|
hasParams = params.len > 1 # not including return type
|
|
(posSetup, minLength) = setupPositional(params, paramsIdent)
|
|
handler = makeHandler(handlerName, params, procBody, returnType)
|
|
named = setupNamed(paramsObj, paramsIdent, params)
|
|
|
|
if hasParams:
|
|
setup.add makeType(typeName, params)
|
|
setup.add quote do:
|
|
var `paramsObj`: `typeName`
|
|
|
|
# unpack each parameter and provide assignments
|
|
var
|
|
pos = 0
|
|
positional = newStmtList()
|
|
executeParams: seq[NimNode]
|
|
|
|
for paramIdent, paramType in paramsIter(params):
|
|
positional.setupPositional(paramsObj, paramsIdent,
|
|
paramIdent, paramType, pos, minLength)
|
|
executeParams.add quote do:
|
|
`paramsObj`.`paramIdent`
|
|
inc pos
|
|
|
|
if hasParams:
|
|
setup.add quote do:
|
|
if `paramsIdent`.kind == rpPositional:
|
|
`posSetup`
|
|
`positional`
|
|
else:
|
|
`named`
|
|
else:
|
|
# even though there is no parameters expected
|
|
# but the numbers of received params should
|
|
# still be checked (RPC spec)
|
|
setup.add quote do:
|
|
if `paramsIdent`.kind == rpPositional:
|
|
`posSetup`
|
|
|
|
let
|
|
awaitedResult = ident "awaitedResult"
|
|
doEncode = quote do: encode(JrpcConv, `awaitedResult`)
|
|
maybeWrap =
|
|
if returnType.noWrap: awaitedResult
|
|
else: ident"JsonString".newCall doEncode
|
|
executeCall = newCall(handlerName, executeParams)
|
|
|
|
result = newStmtList()
|
|
result.add handler
|
|
result.add quote do:
|
|
proc `procWrapper`(`paramsIdent`: RequestParamsRx): Future[JsonString] {.async, gcsafe.} =
|
|
# Avoid 'yield in expr not lowered' with an intermediate variable.
|
|
# See: https://github.com/nim-lang/Nim/issues/17849
|
|
`setup`
|
|
let `awaitedResult` = await `executeCall`
|
|
return `maybeWrap`
|