diff --git a/src/client.ts b/src/client.ts index f164b12..acfb9be 100644 --- a/src/client.ts +++ b/src/client.ts @@ -23,6 +23,8 @@ type Promisify = T extends (...args: any[]) => Promise ? T // already a promise : T extends (...args: infer A) => infer R ? (...args: A) => Promise + : T extends object + ? PromisifyMethods : T; // not a function; type PromisifyMethods = { @@ -60,21 +62,39 @@ export function rpcClient(url: string, options?: RpcOptions) { return result; }; + + function get(prop: string): any { + return new Proxy( + (...args: any) => request(prop.toString(), args), + { + get(_, childProp) { + if (isValidProp(childProp)) + return get(`${prop}.${childProp}`); + } + } + ) + } + return new Proxy( {}, { - /* istanbul ignore next */ - get(target, prop, receiver) { - if (typeof prop === "symbol") return; - if (prop.startsWith("$")) return; - if (prop in Object.prototype) return; - if (prop === "toJSON") return; - return (...args: any) => request(prop.toString(), args); - }, + get(_, prop) { + if (isValidProp(prop)) + return get(prop); + } } ) as PromisifyMethods; } +/* istanbul ignore next */ +function isValidProp(prop: string | symbol): prop is string { + if (typeof prop === "symbol") return false; + if (prop.startsWith("$")) return false; + if (prop in Object.prototype) return false; + if (prop === "toJSON") return false; + return true; +} + function removeTrailingUndefs(values: any[]) { const a = [...values]; while (a.length && a[a.length - 1] === undefined) a.length--; diff --git a/src/server.ts b/src/server.ts index 04eefc8..0f085d8 100644 --- a/src/server.ts +++ b/src/server.ts @@ -90,7 +90,12 @@ export async function handleRpc( error: { code: -32600, message: "Invalid Request" }, }; } - const { jsonrpc, method, params } = request; + const { jsonrpc, params } = request; + const path = request.method.split('.'); + const method = path.pop()!; + for (const property of path) + if (hasProperty(service, property)) + service = service[property] as object; if (!hasMethod(service, method)) { console.log("Method %s not found", method, service); return { diff --git a/src/test/RequestAwareService.ts b/src/test/RequestAwareService.ts index 76c6994..49c371a 100644 --- a/src/test/RequestAwareService.ts +++ b/src/test/RequestAwareService.ts @@ -23,4 +23,12 @@ export class RequestAwareService implements Service { echoHeader(name: string) { return this.headers?.[name.toLowerCase()]; } + + get recurse() { + return { + method() { + return 'recurse.method'; + } + } + } } diff --git a/src/test/client.ts b/src/test/client.ts index 3389839..1784b6d 100644 --- a/src/test/client.ts +++ b/src/test/client.ts @@ -49,3 +49,9 @@ tap.test("should throw on errors", async (t) => { const promise = client.sorry("Dave"); t.rejects(promise, new RpcError("Sorry Dave.", -32000)); }); + +tap.test("should support recursion on property access", async (t) => { + const client = rpcClient(apiUrl); + const result = await client.recurse.method(); + t.equal(result, "recurse.method"); +}); \ No newline at end of file diff --git a/src/test/service.ts b/src/test/service.ts index 07bc5f8..852d2d9 100644 --- a/src/test/service.ts +++ b/src/test/service.ts @@ -14,6 +14,12 @@ export const service = { echoHeader(name: string): string | string[] | undefined { throw new Error("This service can't access request headers"); }, + + recurse: { + method() { + return 'recurse.method'; + } + } }; export type Service = typeof service;