2018-04-12 18:48:46 +01:00
import asyncdispatch , asyncnet , json , tables , macros , strutils
2018-04-24 13:41:59 +01:00
export asyncdispatch , asyncnet , json
2018-03-02 11:46:59 +00:00
type
2018-04-11 20:08:12 +01:00
RpcProc * = proc ( params : JsonNode ) : Future [ JsonNode ]
2018-03-02 11:46:59 +00:00
RpcServer * = ref object
socket * : AsyncSocket
port * : Port
address * : string
procs * : TableRef [ string , RpcProc ]
RpcProcError * = ref object of Exception
code * : int
data * : JsonNode
2018-04-12 18:48:46 +01:00
proc register * ( server : RpcServer , name : string , rpc : RpcProc ) =
server . procs [ name ] = rpc
proc unRegisterAll * ( server : RpcServer ) = server . procs . clear
macro rpc * ( prc : untyped ) : untyped =
## Converts a procedure into the following format:
## <proc name>*(params: JsonNode): Future[JsonNode] {.async.}
## This procedure is then added into a compile-time list
## so that it is automatically registered for every server that
## calls registerRpcs(server)
prc . expectKind nnkProcDef
result = prc
let
params = prc . params
procName = prc . name
procName . expectKind ( nnkIdent )
# check there isn't already a result type
assert params [ 0 ] . kind = = nnkEmpty
# add parameter
params . add nnkIdentDefs . newTree (
newIdentNode ( " params " ) ,
newIdentNode ( " JsonNode " ) ,
newEmptyNode ( )
)
# set result type
params [ 0 ] = nnkBracketExpr . newTree (
newIdentNode ( " Future " ) ,
newIdentNode ( " JsonNode " )
)
# add async pragma; we can assume there isn't an existing .async.
# as this would mean there's a return type and fail the result check above.
prc . addPragma ( newIdentNode ( " async " ) )
2018-04-20 21:19:08 +01:00
proc newRpcServer * ( address = " localhost " , port : Port = Port ( 8545 ) ) : RpcServer =
2018-04-12 18:48:46 +01:00
result = RpcServer (
2018-03-02 11:46:59 +00:00
socket : newAsyncSocket ( ) ,
port : port ,
address : address ,
procs : newTable [ string , RpcProc ] ( )
)
2018-04-20 21:19:08 +01:00
var sharedServer : RpcServer
proc sharedRpcServer * ( ) : RpcServer =
if sharedServer . isNil : sharedServer = newRpcServer ( " " )
result = sharedServer
macro multiRemove ( s : string , values : varargs [ string ] ) : untyped =
2018-04-24 16:28:01 +01:00
## Wrapper for multiReplace
var
body = newStmtList ( )
multiReplaceCall = newCall ( ident" multiReplace " , s )
body . add ( newVarStmt ( ident" eStr " , newStrLitNode ( " " ) ) )
let emptyStr = ident" eStr "
2018-04-20 21:19:08 +01:00
for item in values :
2018-04-24 16:28:01 +01:00
# generate tuples of values with the empty string `eStr`
2018-04-20 21:19:08 +01:00
let sItem = $ item
2018-04-24 16:28:01 +01:00
multiReplaceCall . add ( newPar ( newStrLitNode ( sItem ) , emptyStr ) )
body . add multiReplaceCall
result = newBlockStmt ( body )
2018-04-20 21:19:08 +01:00
2018-04-24 17:37:01 +01:00
proc jsonGetFunc ( paramType : string ) : NimNode =
case paramType
of " string " : result = ident" getStr "
of " int " : result = ident" getInt "
of " float " : result = ident" getFloat "
of " bool " : result = ident" getBool "
of " byte " : result = ident" getInt "
else : result = nil
2018-04-26 19:35:57 +01:00
proc jsonCheckType ( paramType : string ) : NimNode =
case paramType
of " string " : result = ident" JString "
of " int " : result = ident" JInt "
of " float " : result = ident" JFloat "
of " bool " : result = ident" JBool "
of " byte " : result = ident" JInt "
else : result = nil
# TODO: Nested complex fields in objects
# Probably going to need to make it recursive
macro bindObj * ( objInst : untyped , objType : typedesc , paramsArg : typed , elemIdx : int ) : untyped =
result = newNimNode ( nnkStmtList )
let typeDesc = getType ( getType ( objType ) [ 1 ] )
for field in typeDesc [ 2 ] . children :
let
fieldStr = $ field
fieldTypeStr = $ field . getType ( )
getFunc = jsonGetFunc ( fieldTypeStr )
expectedKind = fieldTypeStr . jsonCheckType
expectedStr = " Expected " & $ expectedKind & " but got "
result . add ( quote do :
let jParam = ` paramsArg ` . elems [ ` elemIdx ` ] [ ` fieldStr ` ]
if jParam . kind ! = ` expectedKind ` :
raise newException ( ValueError , ` expectedStr ` & $ jParam . kind )
` objInst ` . ` field ` = jParam . ` getFunc `
)
when defined ( nimDumpRpcs ) :
echo " BindObj expansion: " , result . repr
2018-04-20 21:19:08 +01:00
macro on * ( server : var RpcServer , path : string , body : untyped ) : untyped =
2018-04-24 19:21:51 +01:00
var
2018-04-25 19:18:42 +01:00
paramFetch = newStmtList ( )
2018-04-24 19:21:51 +01:00
expectedParams = 0
2018-04-20 21:19:08 +01:00
let parameters = body . findChild ( it . kind = = nnkFormalParams )
if not parameters . isNil :
2018-04-24 13:41:59 +01:00
# process parameters of body into json fetch templates
2018-04-20 21:19:08 +01:00
var resType = parameters [ 0 ]
2018-04-25 19:18:42 +01:00
2018-04-20 21:19:08 +01:00
if resType . kind ! = nnkEmpty :
# TODO: transform result type and/or return to json
discard
2018-04-24 17:37:01 +01:00
var paramsIdent = ident" params "
2018-04-24 19:21:51 +01:00
expectedParams = parameters . len - 1
2018-04-25 19:18:42 +01:00
let expectedStr = " Expected " & $ ` expectedParams ` & " Json parameter(s) but got "
paramFetch . add ( quote do :
if ` paramsIdent ` . len ! = ` expectedParams ` :
raise newException ( ValueError , ` expectedStr ` & $ ` paramsIdent ` . len )
)
2018-04-24 17:37:01 +01:00
2018-04-20 21:19:08 +01:00
for i in 1 .. < parameters . len :
2018-04-24 17:37:01 +01:00
let pos = i - 1 # first index is return type
2018-04-20 21:19:08 +01:00
parameters [ i ] . expectKind nnkIdentDefs
2018-04-24 17:37:01 +01:00
# take user's parameter name for template
let name = parameters [ i ] [ 0 ]
var paramType = parameters [ i ] [ 1 ]
2018-04-24 19:21:51 +01:00
# TODO: Replace exception with async error return values
# Requires passing the server in local parameters to access the socket
2018-04-24 17:37:01 +01:00
if paramType . kind = = nnkBracketExpr :
2018-04-24 19:21:51 +01:00
# process array and seq parameters
2018-04-25 19:18:42 +01:00
# and marshal json arrays to native types
2018-04-24 19:21:51 +01:00
let paramTypeStr = $ paramType [ 0 ]
assert paramTypeStr = = " array " or paramTypeStr = = " seq "
2018-04-25 19:18:42 +01:00
type ListFormat = enum ltArray , ltSeq
let listFormat = if paramTypeStr = = " array " : ltArray else : ltSeq
2018-04-24 19:21:51 +01:00
2018-04-25 19:18:42 +01:00
if listFormat = = ltArray : paramType . expectLen 3 else : paramType . expectLen 2
var
listType : NimNode
checks = newStmtList ( )
varDecl : NimNode
2018-04-26 19:35:57 +01:00
# always include check for array type for parameters
# TODO: If defined as single params, relax array check
2018-04-25 19:18:42 +01:00
checks . add quote do :
if ` paramsIdent ` . elems [ ` pos ` ] . kind ! = JArray :
raise newException ( ValueError , " Expected " & ` paramTypeStr ` & " but got " & $ ` paramsIdent ` . elems [ ` pos ` ] . kind )
case listFormat
of ltArray :
let arrayLenStr = paramType [ 1 ] . repr
listType = paramType [ 2 ]
varDecl = quote do :
2018-04-24 19:21:51 +01:00
var ` name ` : ` paramType `
2018-04-25 19:18:42 +01:00
# arrays can only be up to the defined length
# note that passing smaller arrays is still valid and are padded with zeros
checks . add ( quote do :
if ` paramsIdent ` . elems [ ` pos ` ] . len > ` name ` . len :
raise newException ( ValueError , " Provided array is longer than parameter allows. Expected " & ` arrayLenStr ` & " , data length is " & $ ` paramsIdent ` . elems [ ` pos ` ] . len )
2018-04-24 19:21:51 +01:00
)
2018-04-25 19:18:42 +01:00
of ltSeq :
listType = paramType [ 1 ]
varDecl = quote do :
var ` name ` = newSeq [ ` listType ` ] ( ` paramsIdent ` . elems [ ` pos ` ] . len )
let
getFunc = jsonGetFunc ( $ listType )
idx = ident" i "
listParse = quote do :
for ` idx ` in 0 .. < ` paramsIdent ` . elems [ ` pos ` ] . len :
` name ` [ ` idx ` ] = ` listType ` ( ` paramsIdent ` . elems [ ` pos ` ] . elems [ ` idx ` ] . ` getFunc ` )
# assemble fetch parameters code
paramFetch . add ( quote do :
` varDecl `
` checks `
` listParse `
)
2018-04-24 17:37:01 +01:00
else :
# other types
var getFuncName = jsonGetFunc ( $ paramType )
2018-04-26 19:35:57 +01:00
if not getFuncName . isNil :
# fetch parameter
let getFunc = newIdentNode ( $ getFuncName )
paramFetch . add ( quote do :
var ` name ` : ` paramType ` = ` paramsIdent ` . elems [ ` pos ` ] . ` getFunc `
)
else :
# this type is probably a custom type, eg object
# bindObj creates assignments to the object fields
let paramTypeStr = $ paramType
paramFetch . add ( quote do :
var ` name ` : ` paramType `
if ` paramsIdent ` . elems [ ` pos ` ] . kind ! = JObject :
raise newException ( ValueError , " Expected " & ` paramTypeStr ` & " but got " & $ ` paramsIdent ` . elems [ ` pos ` ] . kind )
bindObj ( ` name ` , ` paramType ` , ` paramsIdent ` , ` pos ` )
)
2018-04-20 21:19:08 +01:00
# create RPC proc
let
pathStr = $ path
2018-04-24 13:41:59 +01:00
procName = ident ( pathStr . multiRemove ( " . " , " / " ) ) # TODO: Make this unique to avoid potential clashes, or allow people to know the name for calling?
2018-04-20 21:19:08 +01:00
paramsIdent = ident ( " params " )
2018-04-24 13:41:59 +01:00
var procBody : NimNode
2018-04-20 21:19:08 +01:00
if body . kind = = nnkStmtList : procBody = body
else : procBody = body . body
2018-04-24 13:41:59 +01:00
#
2018-04-24 19:21:51 +01:00
var checkTypeError : NimNode
if expectedParams > 0 :
checkTypeError = quote do :
if ` paramsIdent ` . kind ! = JArray :
raise newException ( ValueError , " Expected array but got " & $ ` paramsIdent ` . kind )
else : checkTypeError = newStmtList ( )
2018-04-20 21:19:08 +01:00
result = quote do :
proc `procName`*(`paramsIdent` : JsonNode ) : Future [ JsonNode ] {. async . } =
2018-04-24 19:21:51 +01:00
` checkTypeError `
2018-04-25 19:18:42 +01:00
` paramFetch `
2018-04-20 21:19:08 +01:00
` procBody `
` server ` . register ( ` path ` , ` procName ` )
2018-04-26 19:35:57 +01:00
when defined ( nimDumpRpcs ) :
echo result . repr
#[
2018-04-20 21:19:08 +01:00
when isMainModule :
2018-04-24 13:41:59 +01:00
import unittest
2018-04-20 21:19:08 +01:00
var s = newRpcServer ( " localhost " )
2018-04-26 19:35:57 +01:00
s . on ( " rpc.simplepath " ) :
2018-04-20 21:19:08 +01:00
echo " hello3 "
2018-04-24 13:41:59 +01:00
result = % 1
2018-04-26 19:35:57 +01:00
s . on ( " rpc.returnint " ) do ( ) - > int :
2018-04-24 17:37:01 +01:00
echo " hello2 "
2018-04-26 19:35:57 +01:00
s . on ( " rpc.differentparams " ) do ( a : int , b : string ) :
2018-04-24 17:37:01 +01:00
var node = % " test "
result = node
2018-04-26 19:35:57 +01:00
s . on ( " rpc.arrayparam " ) do ( arr : array [ 0 .. 5 , byte ] , b : string ) :
2018-04-24 17:37:01 +01:00
var res = newJArray ( )
2018-04-24 19:21:51 +01:00
for item in arr :
res . add % int ( item )
res . add % b
result = % res
2018-04-26 19:35:57 +01:00
s . on ( " rpc.seqparam " ) do ( b : string , s : seq [ int ] ) :
2018-04-24 19:21:51 +01:00
var res = newJArray ( )
res . add % b
2018-04-26 19:35:57 +01:00
for item in s :
2018-04-24 17:37:01 +01:00
res . add % int ( item )
result = res
2018-04-26 19:35:57 +01:00
type MyObject * = object
a : int
b : string
c : float
s . on ( " rpc.objparam " ) do ( b : string , obj : MyObject ) :
result = % obj
2018-04-24 13:41:59 +01:00
suite " Server types " :
test " On macro registration " :
2018-04-26 19:35:57 +01:00
check s . procs . hasKey ( " rpc.simplepath " )
check s . procs . hasKey ( " rpc.returnint " )
check s . procs . hasKey ( " rpc.returnint " )
2018-04-24 19:21:51 +01:00
test " Array/seq parameters " :
2018-04-26 19:35:57 +01:00
let r1 = waitfor rpcArrayParam ( % [ % [ 1 , 2 , 3 ] , % " hello " ] )
2018-04-24 19:21:51 +01:00
var ckR1 = % [ 1 , 2 , 3 , 0 , 0 , 0 ]
ckR1 . elems . add % " hello "
check r1 = = ckR1
2018-04-26 19:35:57 +01:00
let r2 = waitfor rpcSeqParam ( % [ % " abc " , % [ 1 , 2 , 3 , 4 , 5 ] ] )
2018-04-24 19:21:51 +01:00
var ckR2 = % [ " abc " ]
for i in 0 .. 4 : ckR2 . add % ( i + 1 )
check r2 = = ckR2
2018-04-26 19:35:57 +01:00
test " Object parameters " :
let
obj = % * { " a " : % 1 , " b " : % " hello " , " c " : % 1 .23 }
r = waitfor rpcObjParam ( % [ % " abc " , obj ] )
check r = = obj
2018-04-25 19:18:42 +01:00
test " Runtime errors " :
expect ValueError :
2018-04-26 19:35:57 +01:00
discard waitfor rpcArrayParam ( % [ % [ 0 , 1 , 2 , 3 , 4 , 5 , 6 ] , % " hello " ] )
] #