import * as server from './server'; import { assert } from './assert'; type HasOnlyOptionalTypes = Partial extends O ? true : O extends void ? true : false; type MakeOptional = true extends HasOnlyOptionalTypes ? { [P in K]?: O } : { [P in K]: O }; export type ClientArg< H extends server.Headers | void, Q extends server.Query | void, B extends server.RequestBody | void > = MakeOptional & MakeOptional & MakeOptional & { readonly signal?: AbortSignal }; export type ClientEndpoint< H extends server.Headers | void, Q extends server.Query | void, B extends server.RequestBody | void, R extends server.Response> > = object extends ClientArg ? (ctx?: ClientArg) => Promise : (ctx: ClientArg) => Promise; type PathParam = (param: string | number) => ClientSpec | ClientEndpoint; export interface ClientSpec { readonly [part: string]: ClientSpec | PathParam | ClientEndpoint; } export type RequestContext = { path: string }; export type ClientAdapter = server.Endpoint< server.Headers | undefined, server.Params | undefined, server.Query | undefined, server.RequestBody | undefined, server.Response>, RequestContext >; export type ClientFactory = (adapter: ClientAdapter) => Spec; interface RequestBody { contentType: ContentType; value: T; } export function body( contentType: ContentType, value: T ): RequestBody { return { contentType, value }; } export const noContentContentType = 'oatsNoContent' as const; export function noContent(): RequestBody { return body(noContentContentType, null); } export function json(data: T): RequestBody { return body('application/json', data); } export function formData(data: T): RequestBody { return body('multipart/form-data', data); } export function formUrlEncoded(data: T): RequestBody { return body('application/x-www-form-urlencoded', data); } export default function literal(literal: T): T { return literal; } export function emptyTree(): OpTree { return { methods: {}, parts: {} }; } export function identifier(part: string) { return part; } export function addPath( opTree: OpTree, path: string, method: server.Methods, value: T ): OpTree { const parts: string[] = path.split('/').filter(n => n); function addPath_(parts: string[], opTree: OpTree | undefined): OpTree { opTree = opTree || emptyTree(); const part = parts[0]; if (part === undefined) { assert(opTree.methods[method] === undefined, 'duplicate method definition for ' + path); return { ...opTree, methods: { ...opTree.methods, [method]: value } }; } const param = part.match('(.*){([^}]+)}'); if (param) { assert( !/{.*}/.test(param[1]), 'only single path parameter is supported per url segment. Got multiple in ' + path ); if (param[1] === '') { return { ...opTree, param: { name: identifier(param[2]), tree: addPath_(parts.slice(1), opTree.param?.tree) } }; } else { const id = identifier(param[1]); return { ...opTree, parts: { ...opTree.parts, [id]: addPath_(['{' + param[2] + '}', ...parts.slice(1)], opTree.parts[id]) } }; } } const id = identifier(part); return { ...opTree, parts: { ...opTree.parts, [id]: addPath_(parts.slice(1), opTree.parts[id]) } }; } return addPath_(parts, opTree); } export interface OpTree { methods: { [method: string]: T; }; parts: { [part: string]: OpTree; }; param?: { name: string; tree: OpTree; }; } function makeParam( adapter: ClientAdapter, param: { name: string; tree: OpTree }, pathParams: string[], opts: server.HandlerOptions | undefined ) { return (value: string) => fromTree(adapter, param.tree, [...pathParams, value], opts); } function paramObject(pathParams: string[], path: string) { const matches = path.match(/{([^}]+)}/g) || []; assert(matches.length === pathParams.length, 'mismatch in path param sizes for ' + path); if (matches.length === 0) { return undefined; } const params: any = {}; for (let i = 0; i < matches.length; i++) { params[matches[i].replace(/{|}/g, '')] = pathParams[i]; } return params; } function fillInPathParams(params: { [key: string]: string }, path: string) { return path.replace(/{([^}]+)}/g, (m, param) => { assert(params[param] != null, 'missing path param ' + param); return params[param]; }); } function makeMethod( adapter: ClientAdapter, handler: server.Handler, pathParams: string[], opts?: server.HandlerOptions ) { const params = paramObject(pathParams, handler.path); const call = server.safe( handler.headers, handler.params, handler.query, handler.body, handler.response, ctx => adapter({ ...ctx, path: fillInPathParams(params, handler.path), servers: handler.servers }), { ...opts, mode: 'client' } ); return (ctx: ClientArg) => call({ path: handler.path, servers: handler.servers, method: handler.method, params, headers: ctx?.headers, query: ctx?.query, body: ctx?.body, signal: ctx?.signal, requestContext: { path: handler.path } }); } function fromTree( adapter: ClientAdapter, tree: OpTree, pathParams: string[], opts: server.HandlerOptions | undefined ): ClientSpec { const node: any = tree.param ? makeParam(adapter, tree.param, pathParams, opts) : {}; Object.keys(tree.methods).forEach(key => { assert(!node[key], 'duplicate path part ' + key); node[key] = makeMethod(adapter, tree.methods[key], pathParams, opts); }); Object.keys(tree.parts).forEach(key => { assert(!node[key], 'duplicate path part ' + key); node[key] = fromTree(adapter, tree.parts[key], pathParams, opts); }); return node; } export function createClientFactory( handlers: server.Handler[], opts?: server.HandlerOptions ): ClientFactory { return (adapter: ClientAdapter): Spec => { const tree: OpTree = handlers.reduce((memo, handler) => { return addPath(memo, handler.path, handler.method, handler); }, emptyTree()); return fromTree(adapter, tree, [], opts) as unknown as Spec; }; }