sourceafJsonRpc::JsonRpc.fan

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
    }
}