using util::JsonInStream
** An implementation of [JSON-RPC v2]`https://www.jsonrpc.org/`.
mixin JsonRpc {
** Creates an instance 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
**
** Options defaults are:
**
** pathDelimiter : '/'
** errFn : |JsonRpcErr rpcErr | { }
** fromJsonFn : |Obj? jsonVal, Type argType ->Obj?| { jsonVal }
** toJsonFn : |Obj? returnVal ->Obj?| { returnVal }
** rpcHookFn : |Obj sink, Method method, Obj?[]? args->Obj?| { method.callOn(sink, args) }
**
** 'fromJsonFn' is used to convert method arguments and 'toJsonFn' is used to convert the
** method's return value.
**
** 'rpcHookFn' allows you to intercept the final method invocation, make changes, and
** optionally continue with the invocation.
**
static new make(Obj sink, [Str:Obj]? opts := null) {
JsonRpcMutantImpl(sink, opts)
}
** Invokes the (batch of) RPCs for the given JSON request, and returns the JSON response.
**
** 'null' is returned, should the request be a notification.
abstract Str? call(InStream jsonIn)
** Converts this class into an *immutable* function.
**
** Note that the handler and all options must be immutable too.
abstract |InStream->Str?| toImmutableFn()
}
internal mixin JsonRpcMixin : JsonRpc {
abstract Str:Obj sinks()
abstract Str:Obj opts()
override Str? call(InStream jsonIn) {
reqObj := null
try reqObj = JsonInStream(jsonIn).readJson
catch (ParseErr perr)
return JsonRpcRes {
it.error = JsonRpcErr(JsonRpcErr.parseError, "Parse error - ${perr.msg}")
}.toJson(errFn)
if (reqObj is Map) {
resRpc := callRpc(reqObj)
return resRpc?.toJson(errFn)
}
if (reqObj is List) {
reqBat := (Obj[]) reqObj
if (reqBat.isEmpty)
return JsonRpcRes {
it.error = JsonRpcErr(JsonRpcErr.invalidRequest, "Invalid request")
}.toJson(errFn)
resBat := StrBuf()
reqBat.each |reqRpc| {
resRpc := callRpc(reqRpc)
if (resRpc != null)
resBat.join(resRpc.toJson(errFn), ",\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(errFn)
}
private JsonRpcRes? callRpc(Obj reqObj) {
reqRpc := null as JsonRpcReq
error := null as JsonRpcErr
resObj := null
try reqRpc = JsonRpcReq.fromObj(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) {
// errors elsewhere are taken care of when returning a JsonRpcRes
if (error != null)
errFn()?.call(error)
return null
}
resRpc := JsonRpcRes {
it.id = reqRpc.id
it.result = resObj
it.error = error
}
return resRpc
}
private Obj? callSink(JsonRpcReq rpcReq) {
idx := rpcReq.method.indexr(optPathDelimiter.toChar)
prefix := idx != null ? rpcReq.method[0..idx] : ""
methodN := idx != null ? rpcReq.method[idx+1..-1] : rpcReq.method
sink := sinks[prefix]
if (sink == null)
throw JsonRpcErr(JsonRpcErr.methodNotFound, "Method not found: ${rpcReq.method}")
// fall back to looking for a method without the "on" prefix
method := sink.typeof.method("on" + methodN.capitalize, false) ?: sink.typeof.method(methodN, false)
if (method == null)
throw JsonRpcErr(JsonRpcErr.methodNotFound, "Method not found: ${rpcReq.method}")
params := rpcReq.params
args := null as Obj?[]
try {
if (params is List)
args = ((Obj?[]) params).map |arg, i| {
fromJsonFn(arg, method.params[i].type)
}
else
if (params is Map) {
obj := (Str:Obj?) params
args = method.params.map |param, i| {
if (obj.containsKey(param.name)) {
val := obj[param.name]
arg := fromJsonFn(val, param.type)
return arg
}
// a fudge the weird interpretation of the JSON-RPC spec by LSP
// put this *before* default 'cos we expect ALL the params to be supplied
if (param.name == "params" && method.params.size == 1)
return fromJsonFn(obj, param.type)
if (param.hasDefault)
return method.paramDef(param, sink)
str := method.qname + "("
i.times { str += "..., " }
str += param.name + ") <-- " + obj.keys
throw JsonRpcErr(JsonRpcErr.invalidParams, "Unknown param: ${str}")
}
}
}
catch (JsonRpcErr err) throw err
catch (Err err) throw JsonRpcErr(JsonRpcErr.internalError, "Could not invoke method: ${err.msg}", err)
ret := rpcHookFn(sink, method, args)
try ret = toJsonFn(ret)
catch (Err err) throw JsonRpcErr(JsonRpcErr.internalError, "Could not convert response: ${err.msg}", err)
return ret
}
Int optPathDelimiter() {
opts["pathDelimiter"] ?: '/'
}
private |JsonRpcErr|? errFn() {
opts["errFn"]
}
private Obj? fromJsonFn(Obj? jsonVal, Type argType) {
((|Obj?, Type->Obj?|?) opts["fromJsonFn"])?.call(jsonVal, argType) ?: jsonVal
}
private Obj? toJsonFn(Obj? val) {
((|Obj?->Obj?|?) opts["toJsonFn"])?.call(val) ?: val
}
private Obj? rpcHookFn(Obj sink, Method method, Obj?[]? args) {
fn := (|Obj, Method, Obj?[]?->Obj?|?) opts["rpcHookFn"]
return fn != null
? fn.call(sink, method, args)
: method.callOn(sink, args)
}
}
internal class JsonRpcMutantImpl : JsonRpcMixin {
override Str:Obj sinks
override Str:Obj opts
new make(Obj sink, [Str:Obj]? opts := null) {
this.opts = opts ?: Str:Obj[:]
if (sink isnot Map)
sink = Str:Obj[:].set("", sink)
// order sink prefixes so more-specific prefixes are matched first
sinks := (Str:Obj) sink
sunks := Str:Obj[:] { it.ordered = true }
sinks.keys
.sortr |p1, p2| { p1.split(optPathDelimiter).size <=> p2.split(optPathDelimiter).size }
.each |key| {
sunks[key] = sinks[key]
}
this.sinks = sunks
}
override |InStream->Str?| toImmutableFn() {
JsonRpcConstImpl(sinks, opts).toImmutableFn
}
}
internal const class JsonRpcConstImpl : JsonRpcMixin {
override const Str:Obj sinks
override const Str:Obj opts
override const |InStream->Str?| toImmutableFn
new make(Str:Obj sinks, Str:Obj opts) {
this.sinks = sinks
this.opts = opts
this.toImmutableFn = #call.func.bind([this])
}
}