using util::JsonInStream
** An implementation of [JSON-RPC v2]`https://www.jsonrpc.org/`.
mixin JsonRpc {
** Creates an instace of 'JsonRpc'.
**
** 'sink' may either a single instance, or a 'Str:Obj' map of sink instances where 'Str' is a
** prefix that must match the RPC method name.
**
** pre>
** syntax: fantom
** jsonRpc := JsonRpc([
** "text/" : TextSink()
** "image/" : ImageSink()
** ])
** <pre
**
** Valid options are:
** - 'dispatchFn' - '|Obj sinks, Str rpcMethod -> Obj[]| { ... }'
** - 'invokeFn' - '|Obj sink, Method method, Obj? params -> Obj?| { ... }'
**
** 'dispatchFn' should return '[Obj sink, Method method]', or 'null' if the sink / method is not found.
**
** 'invokeFn' should invoke the 'method' on the 'sink' with the given 'params' and return the result.
static new make(Obj sink, [Str:Obj?]? opts := null) {
JsonRpcImpl(sink, opts)
}
** Invokes the (batch of) RPCs for given JSON request.
**
** The 'InStream' is guaranteed to be closed.
abstract Str? call(InStream jsonIn, Bool close := true)
** An optimised dispatch fn for searching a Map of method prefixs to sink instances.
**
** pre>
** JsonRpc(..., [
** "dispatchFn" : JsonRpc.multiSinkDispatchFn('/')
** ])
** <pre
static |Obj, Str->Obj[]?| multiSinkDispatchFn(Int delimiter) {
delimiterStr := delimiter.toChar
return |Str:Obj sinks, Str rpcMethod -> Obj[]?| {
idx := rpcMethod.indexr(delimiterStr)
prefix := idx != null ? rpcMethod[0..idx] : ""
methodN := idx != null ? rpcMethod[idx+1..-1] : rpcMethod
sink := sinks[prefix]
if (sink != null) {
method := sink.typeof.method(methodN, false)
if (method != null)
return [sink, method]
}
return null
}
}
// An invoke fn that lets you convert parameter and response values. Use for JSON <-> Fantom object mapping.
// 'paramType' is 'null' when converting the response object.
static |Obj, Method, Obj?->Obj?| convertingInvokeFn(|Type? paramType, Obj? paramVal->Obj?| fn) {
|Obj sink, Method method, Obj? params->Obj?| {
JsonRpcImpl.invokeMethod(sink, method, params, fn)
}
}
}
internal class JsonRpcImpl : JsonRpc {
private Obj sinks
private Str:Obj? opts
new make(Obj sink, [Str:Obj?]? opts := null) {
this.sinks = sink
this.opts = opts ?: Str:Obj[:]
if (this.opts.containsKey("dispatchFn") == false) {
if (sinks is Map) {
// order sink prefixes so more-specific prefixes are matched first
sinks := (Str:Obj?) this.sinks
sunks := Map.make(sinks.typeof) { it.ordered = true }
sinks.keys.sortr |p1, p2| { p1.size <=> p2.size }.each |key| {
sunks[key] = sinks[key]
}
this.sinks = sunks
this.opts["dispatchFn"] = #multiSinkDispatcher.func
} else
this.opts["dispatchFn"] = #singleSinkDispatcher.func
}
if (this.opts.containsKey("invokeFn") == false)
this.opts["invokeFn"] = #invokeMethod.func
}
override Str? call(InStream jsonIn, Bool close := true) {
try return doCall(jsonIn)
finally
if (close)
jsonIn.close
}
private Str? doCall(InStream jsonIn) {
reqObj := null
try reqObj = JsonInStream(jsonIn).readJson
catch (ParseErr perr)
return JsonRpcRes {
it.error = JsonRpcErr(JsonRpcErr.parseError, "Parse error - ${perr.msg}")
}.toJson
if (reqObj is Map) {
resRpc := callRpc(reqObj)
return resRpc?.toJson
}
if (reqObj is List) {
reqBat := (Obj[]) reqObj
if (reqBat.isEmpty)
return JsonRpcRes {
it.error = JsonRpcErr(JsonRpcErr.invalidRequest, "Invalid request")
}.toJson
resBat := StrBuf()
reqBat.each |reqRpc| {
resRpc := callRpc(reqRpc)
if (resRpc != null)
resBat.join(resRpc.toJson, ",\n")
}
if (resBat.isEmpty)
return null
resBat.insert(0, "[\n").add("\n]")
return resBat.toStr
}
return JsonRpcRes {
it.error = JsonRpcErr(JsonRpcErr.invalidRequest, "Invalid request")
}.toJson
}
private JsonRpcRes? callRpc(Obj reqObj) {
reqRpc := null as JsonRpcReq
error := null as JsonRpcErr
resObj := null
try reqRpc = JsonRpcReq(reqObj)
catch (JsonRpcErr rpcErr)
return JsonRpcRes {
it.error = rpcErr
}
catch (Err err)
return JsonRpcRes {
it.error = JsonRpcErr(JsonRpcErr.invalidRequest, "Invalid request")
}
try resObj = callSink(reqRpc)
catch (JsonRpcErr rpcErr)
error = rpcErr
catch (Err err)
error = JsonRpcErr(JsonRpcErr.applicationError, err.msg, err)
if (reqRpc.isNotification)
return null
resRpc := JsonRpcRes {
it.id = reqRpc.id
it.result = resObj
it.error = error
}
return resRpc
}
private Obj? callSink(JsonRpcReq rpcReq) {
dispatchObjs := dispatchFn()(sinks, rpcReq.method)
if (dispatchObjs == null)
throw JsonRpcErr(JsonRpcErr.methodNotFound, "Method not found: ${rpcReq.method}")
sink := dispatchObjs.getSafe(0)
meth := dispatchObjs.getSafe(1)
if (sink == null || meth isnot Method)
throw JsonRpcErr(JsonRpcErr.internalError, "Bad dispatcher return value")
return invokeFn()(sink, meth, rpcReq.params)
}
private |Obj, Str->Obj[]?| dispatchFn() {
opts["dispatchFn"]
}
private |Obj, Method, Obj?->Obj?| invokeFn() {
opts["invokeFn"]
}
private static Obj[]? singleSinkDispatcher(Obj sink, Str rpcMethod) {
method := sink.typeof.method(rpcMethod, false)
return method != null
? [sink, method]
: null
}
private static Obj[]? multiSinkDispatcher(Str:Obj sinks, Str rpcMethod) {
prefix := sinks.keys.find |prefix| {
rpcMethod.startsWith(prefix)
}
if (prefix != null) {
methodN := rpcMethod[prefix.size..-1]
sink := sinks[prefix]
method := sink.typeof.method(methodN, false)
if (method != null)
return [sink, method]
}
return null
}
static Obj? invokeMethod(Obj sink, Method method, Obj? params, |Type?, Obj?->Obj?|? convertFn := null) {
args := null as Obj?[]
try {
if (params is List) {
args = params
if (convertFn != null)
args = args.map |arg, i| {
convertFn(method.params[i].type, arg)
}
}
if (params is Map) {
obj := (Str:Obj?) params
args = method.params.map |param| {
if (obj.containsKey(param.name)) {
arg := obj[param.name]
if (convertFn != null)
arg = convertFn(param.type, arg)
return arg
}
if (param.hasDefault)
return method.paramDef(param, sink)
throw JsonRpcErr(JsonRpcErr.invalidParams, "Invalid param: ${param.name}")
}
}
} catch (JsonRpcErr err)
throw err
catch (Err err)
throw JsonRpcErr(JsonRpcErr.internalError, "Could not invoke method: ${err.msg}", err)
resp := method.callOn(sink, args)
if (convertFn != null)
try resp = convertFn(null, resp)
catch (Err err)
throw JsonRpcErr(JsonRpcErr.internalError, "Could not convert response: ${err.msg}", err)
return resp
}
}