diff --git a/eslint.config.js b/eslint.config.js index 3b56dc30a..b06621daa 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -85,10 +85,6 @@ const tsFactoryConcerns = [ "[arguments.0.callee.property.name='createPropertyAccessExpression']", message: "use makePropCall() helper", }, - { - selector: "Identifier[name='AmpersandAmpersandToken']", - message: "use makeAnd() helper", - }, { selector: "Identifier[name='EqualsEqualsEqualsToken']", message: "use makeEqual() helper", @@ -109,6 +105,11 @@ const tsFactoryConcerns = [ selector: "Literal[value='Promise']", message: "use makePromise() helper", }, + { + selector: + "CallExpression[callee.property.name='createTypeReferenceNode'][arguments.length=1]", + message: "use ensureTypeNode() helper", + }, ]; export default tsPlugin.config( @@ -155,7 +156,7 @@ export default tsPlugin.config( }, { name: "source/integration", - files: ["src/integration.ts", "src/zts.ts"], + files: ["src/integration.ts", "src/integration-base.ts", "src/zts.ts"], rules: { "no-restricted-syntax": [ "warn", diff --git a/example/example.client.ts b/example/example.client.ts index 8e7d51e07..584994f5a 100644 --- a/example/example.client.ts +++ b/example/example.client.ts @@ -414,6 +414,20 @@ export const endpointTags = { "get /v1/events/time": ["subscriptions"], }; +const parseRequest = (request: string) => + request.split(/ (.+)/, 2) as [Method, Path]; + +const substitute = (path: string, params: Record) => { + const rest = { ...params }; + for (const key in params) { + path = path.replace(`:${key}`, () => { + delete rest[key]; + return params[key]; + }); + } + return [path, rest] as const; +}; + export type Implementation = ( method: Method, path: string, @@ -426,25 +440,8 @@ export class ExpressZodAPIClient { request: K, params: Input[K], ): Promise { - const [method, path] = request.split(/ (.+)/, 2) as [Method, Path]; - return this.implementation( - method, - Object.keys(params).reduce( - (acc, key) => - acc.replace(`:${key}`, (params as Record)[key]), - path, - ), - Object.keys(params).reduce( - (acc, key) => - Object.assign( - acc, - !path.includes(`:${key}`) && { - [key]: (params as Record)[key], - }, - ), - {}, - ), - ); + const [method, path] = parseRequest(request); + return this.implementation(method, ...substitute(path, params)); } } diff --git a/src/integration-base.ts b/src/integration-base.ts index d07148ee9..a96a09725 100644 --- a/src/integration-base.ts +++ b/src/integration-base.ts @@ -3,8 +3,9 @@ import { ResponseVariant } from "./api-response"; import { contentTypes } from "./content-type"; import { Method, methods } from "./method"; import { + accessModifiers, + ensureTypeNode, f, - makeAnd, makeArrowFn, makeConst, makeDeconstruction, @@ -13,7 +14,6 @@ import { makeInterfaceProp, makeKeyOf, makeNew, - makeObjectKeysReducer, makeParam, makeParams, makePromise, @@ -26,7 +26,6 @@ import { makeTernary, makeType, propOf, - protectedReadonlyModifier, recordStringAny, } from "./typescript-api"; @@ -54,7 +53,8 @@ export abstract class IntegrationBase { paramsArgument: f.createIdentifier("params"), methodParameter: f.createIdentifier("method"), requestParameter: f.createIdentifier("request"), - accumulator: f.createIdentifier("acc"), + parseRequestFn: f.createIdentifier("parseRequest"), + substituteFn: f.createIdentifier("substitute"), provideMethod: f.createIdentifier("provide"), implementationArgument: f.createIdentifier("implementation"), headersProperty: f.createIdentifier("headers"), @@ -62,6 +62,7 @@ export abstract class IntegrationBase { undefinedValue: f.createIdentifier("undefined"), bodyProperty: f.createIdentifier("body"), responseConst: f.createIdentifier("response"), + restConst: f.createIdentifier("rest"), searchParamsConst: f.createIdentifier("searchParams"), exampleImplementationConst: f.createIdentifier("exampleImplementation"), clientConst: f.createIdentifier("client"), @@ -86,10 +87,7 @@ export abstract class IntegrationBase { // type SomeOf = T[keyof T]; protected someOfType = makeType( "SomeOf", - f.createIndexedAccessTypeNode( - f.createTypeReferenceNode("T"), - makeKeyOf("T"), - ), + f.createIndexedAccessTypeNode(ensureTypeNode("T"), makeKeyOf("T")), { params: { T: undefined } }, ); @@ -104,9 +102,7 @@ export abstract class IntegrationBase { /** @example SomeOf<_> */ protected someOf = ({ name }: ts.TypeAliasDeclaration) => - f.createTypeReferenceNode(this.someOfType.name, [ - f.createTypeReferenceNode(name), - ]); + f.createTypeReferenceNode(this.someOfType.name, [ensureTypeNode(name)]); // export type Path = "/v1/user/retrieve" | ___; protected makePathType = () => @@ -148,9 +144,7 @@ export abstract class IntegrationBase { f.createFunctionTypeNode( undefined, makeParams({ - [this.ids.methodParameter.text]: f.createTypeReferenceNode( - this.ids.methodType, - ), + [this.ids.methodParameter.text]: ensureTypeNode(this.ids.methodType), [this.ids.pathParameter.text]: f.createKeywordTypeNode( ts.SyntaxKind.StringKeyword, ), @@ -161,92 +155,130 @@ export abstract class IntegrationBase { { expose: true }, ); - // public provide(request: K, params: Input[K]): Promise {} - private makeProvider = () => { - // `:${key}` - const keyParamExpression = makeTemplate(":", [this.ids.keyParameter]); - - // Object.keys(params).reduce((acc, key) => acc.replace(___, params[key]), path) - const pathArgument = makeObjectKeysReducer( - this.ids.paramsArgument, - makePropCall(this.ids.accumulator, propOf("replace"), [ - keyParamExpression, - f.createElementAccessExpression( - f.createAsExpression(this.ids.paramsArgument, recordStringAny), - this.ids.keyParameter, + // const parseRequest = (request: string) => request.split(/ (.+)/, 2) as [Method, Path]; + protected makeParseRequestFn = () => + makeConst( + this.ids.parseRequestFn, + makeArrowFn( + { + [this.ids.requestParameter.text]: f.createKeywordTypeNode( + ts.SyntaxKind.StringKeyword, + ), + }, + f.createAsExpression( + makePropCall(this.ids.requestParameter, propOf("split"), [ + f.createRegularExpressionLiteral("/ (.+)/"), // split once + f.createNumericLiteral(2), // excludes third empty element + ]), + f.createTupleTypeNode([ + ensureTypeNode(this.ids.methodType), + ensureTypeNode(this.ids.pathType), + ]), ), - ]), - this.ids.pathParameter, + ), ); - // Object.keys(params).reduce((acc, key) => - // Object.assign(acc, !path.includes(`:${key}`) && {[key]: params[key]} ), {}) - const paramsArgument = makeObjectKeysReducer( - this.ids.paramsArgument, - makePropCall( - f.createIdentifier(Object.name), - propOf("assign"), - [ - this.ids.accumulator, - makeAnd( - f.createPrefixUnaryExpression( - ts.SyntaxKind.ExclamationToken, - makePropCall(this.ids.pathParameter, propOf("includes"), [ - keyParamExpression, - ]), + // const substitute = (path: string, params: Record) => { ___ return [path, rest] as const; } + protected makeSubstituteFn = () => + makeConst( + this.ids.substituteFn, + makeArrowFn( + { + [this.ids.pathParameter.text]: f.createKeywordTypeNode( + ts.SyntaxKind.StringKeyword, + ), + [this.ids.paramsArgument.text]: recordStringAny, + }, + f.createBlock([ + makeConst( + this.ids.restConst, + f.createObjectLiteralExpression([ + f.createSpreadAssignment(this.ids.paramsArgument), + ]), + ), + f.createForInStatement( + f.createVariableDeclarationList( + [f.createVariableDeclaration(this.ids.keyParameter)], + ts.NodeFlags.Const, ), - f.createObjectLiteralExpression( - [ - f.createPropertyAssignment( - f.createComputedPropertyName(this.ids.keyParameter), - f.createElementAccessExpression( - f.createAsExpression( - this.ids.paramsArgument, - recordStringAny, - ), - this.ids.keyParameter, + this.ids.paramsArgument, + f.createBlock([ + f.createExpressionStatement( + f.createBinaryExpression( + this.ids.pathParameter, + f.createToken(ts.SyntaxKind.EqualsToken), + makePropCall( + this.ids.pathParameter, + propOf("replace"), + [ + makeTemplate(":", [this.ids.keyParameter]), // `:${key}` + makeArrowFn( + [], + f.createBlock([ + f.createExpressionStatement( + f.createDeleteExpression( + f.createElementAccessExpression( + f.createIdentifier("rest"), + this.ids.keyParameter, + ), + ), + ), + f.createReturnStatement( + f.createElementAccessExpression( + this.ids.paramsArgument, + this.ids.keyParameter, + ), + ), + ]), + ), + ], ), ), - ], - false, + ), + ]), + ), + f.createReturnStatement( + f.createAsExpression( + f.createArrayLiteralExpression([ + this.ids.pathParameter, + this.ids.restConst, + ]), + ensureTypeNode("const"), ), ), - ], + ]), ), - f.createObjectLiteralExpression(), ); - return makePublicMethod( + // public provide(request: K, params: Input[K]): Promise {} + private makeProvider = () => + makePublicMethod( this.ids.provideMethod, makeParams({ - [this.ids.requestParameter.text]: f.createTypeReferenceNode("K"), + [this.ids.requestParameter.text]: ensureTypeNode("K"), [this.ids.paramsArgument.text]: f.createIndexedAccessTypeNode( - f.createTypeReferenceNode(this.ids.inputInterface), - f.createTypeReferenceNode("K"), + ensureTypeNode(this.ids.inputInterface), + ensureTypeNode("K"), ), }), f.createBlock([ makeConst( - // const [method, path, params] = + // const [method, path] = this.parseRequest(request); makeDeconstruction(this.ids.methodParameter, this.ids.pathParameter), - // request.split(/ (.+)/, 2) as [Method, Path]; - f.createAsExpression( - makePropCall(this.ids.requestParameter, propOf("split"), [ - f.createRegularExpressionLiteral("/ (.+)/"), // split once - f.createNumericLiteral(2), // excludes third empty element - ]), - f.createTupleTypeNode([ - f.createTypeReferenceNode(this.ids.methodType), - f.createTypeReferenceNode(this.ids.pathType), - ]), - ), + f.createCallExpression(this.ids.parseRequestFn, undefined, [ + this.ids.requestParameter, + ]), ), // return this.implementation(___) f.createReturnStatement( makePropCall(f.createThis(), this.ids.implementationArgument, [ this.ids.methodParameter, - pathArgument, - paramsArgument, + f.createSpreadElement( + f.createCallExpression(this.ids.substituteFn, undefined, [ + this.ids.pathParameter, + this.ids.paramsArgument, + ]), + ), ]), ), ]), @@ -254,13 +286,12 @@ export abstract class IntegrationBase { typeParams: { K: this.ids.requestType }, returns: makePromise( f.createIndexedAccessTypeNode( - f.createTypeReferenceNode(this.ids.responseInterface), - f.createTypeReferenceNode("K"), + ensureTypeNode(this.ids.responseInterface), + ensureTypeNode("K"), ), ), }, ); - }; // export class ExpressZodAPIClient { ___ } protected makeClientClass = () => @@ -270,8 +301,8 @@ export abstract class IntegrationBase { makeEmptyInitializingConstructor([ makeParam( this.ids.implementationArgument, - f.createTypeReferenceNode(this.ids.implementationType), - protectedReadonlyModifier, + ensureTypeNode(this.ids.implementationType), + accessModifiers.protectedReadonly, ), ]), [this.makeProvider()], @@ -429,10 +460,7 @@ export abstract class IntegrationBase { ]), { isAsync: true }, ), - { - expose: true, - type: f.createTypeReferenceNode(this.ids.implementationType), - }, + { expose: true, type: ensureTypeNode(this.ids.implementationType) }, ); }; diff --git a/src/integration.ts b/src/integration.ts index 2f7f5b14a..5da65aa1c 100644 --- a/src/integration.ts +++ b/src/integration.ts @@ -9,6 +9,7 @@ import { makeInterface, makeType, printNode, + ensureTypeNode, } from "./typescript-api"; import { makeCleanId } from "./common-helpers"; import { loadPeer } from "./peer-helpers"; @@ -81,7 +82,7 @@ export class Integration extends IntegrationBase { protected makeAlias( schema: z.ZodTypeAny, produce: () => ts.TypeNode, - ): ts.TypeReferenceNode { + ): ts.TypeNode { let name = this.aliases.get(schema)?.name?.text; if (!name) { name = `Type${this.aliases.size + 1}`; @@ -89,7 +90,7 @@ export class Integration extends IntegrationBase { this.aliases.set(schema, makeType(name, temp)); this.aliases.set(schema, makeType(name, produce())); } - return f.createTypeReferenceNode(name); + return ensureTypeNode(name); } public constructor({ @@ -124,10 +125,7 @@ export class Integration extends IntegrationBase { ); this.program.push(variantType); return statusCodes.map((code) => - makeInterfaceProp( - code, - f.createTypeReferenceNode(variantType.name), - ), + makeInterfaceProp(code, variantType.name), ); }, Array.from(responses.entries())); const dict = makeInterface( @@ -145,22 +143,22 @@ export class Integration extends IntegrationBase { f.createStringLiteral(request), ); this.registry.set(request, { - input: f.createTypeReferenceNode(input.name), + input: ensureTypeNode(input.name), positive: this.someOf(dictionaries.positive), negative: this.someOf(dictionaries.negative), response: f.createUnionTypeNode([ f.createIndexedAccessTypeNode( - f.createTypeReferenceNode(this.ids.posResponseInterface), + ensureTypeNode(this.ids.posResponseInterface), literalIdx, ), f.createIndexedAccessTypeNode( - f.createTypeReferenceNode(this.ids.negResponseInterface), + ensureTypeNode(this.ids.negResponseInterface), literalIdx, ), ]), encoded: f.createIntersectionTypeNode([ - f.createTypeReferenceNode(dictionaries.positive.name), - f.createTypeReferenceNode(dictionaries.negative.name), + ensureTypeNode(dictionaries.positive.name), + ensureTypeNode(dictionaries.negative.name), ]), }); this.tags.set(request, endpoint.getTags()); @@ -178,6 +176,8 @@ export class Integration extends IntegrationBase { this.program.push( this.makeEndpointTags(), + this.makeParseRequestFn(), + this.makeSubstituteFn(), this.makeImplementationType(), this.makeClientClass(), ); diff --git a/src/typescript-api.ts b/src/typescript-api.ts index bac5ea651..72c7f4ac7 100644 --- a/src/typescript-api.ts +++ b/src/typescript-api.ts @@ -6,12 +6,13 @@ const exportModifier = [f.createModifier(ts.SyntaxKind.ExportKeyword)]; const asyncModifier = [f.createModifier(ts.SyntaxKind.AsyncKeyword)]; -const publicModifier = [f.createModifier(ts.SyntaxKind.PublicKeyword)]; - -export const protectedReadonlyModifier = [ - f.createModifier(ts.SyntaxKind.ProtectedKeyword), - f.createModifier(ts.SyntaxKind.ReadonlyKeyword), -]; +export const accessModifiers = { + public: [f.createModifier(ts.SyntaxKind.PublicKeyword)], + protectedReadonly: [ + f.createModifier(ts.SyntaxKind.ProtectedKeyword), + f.createModifier(ts.SyntaxKind.ReadonlyKeyword), + ], +}; export const addJsDocComment = (node: T, text: string) => ts.addSyntheticLeadingComment( @@ -95,16 +96,23 @@ export const makeEmptyInitializingConstructor = ( params: ts.ParameterDeclaration[], ) => f.createConstructorDeclaration(undefined, params, f.createBlock([])); +export const ensureTypeNode = ( + subject: ts.TypeNode | ts.Identifier | string, +): ts.TypeNode => + typeof subject === "string" || ts.isIdentifier(subject) + ? f.createTypeReferenceNode(subject) + : subject; + export const makeInterfaceProp = ( name: string | number, - value: ts.TypeNode, + value: Parameters[0], { isOptional }: { isOptional?: boolean } = {}, ) => f.createPropertySignature( undefined, makePropertyIdentifier(name), isOptional ? f.createToken(ts.SyntaxKind.QuestionToken) : undefined, - value, + ensureTypeNode(value), ); export const makeDeconstruction = ( @@ -178,7 +186,7 @@ export const makePublicMethod = ( } = {}, ) => f.createMethodDeclaration( - publicModifier, + accessModifiers.public, undefined, name, undefined, @@ -198,11 +206,8 @@ export const makePublicClass = ( ...statements, ]); -export const makeKeyOf = (id: ts.Identifier | string) => - f.createTypeOperatorNode( - ts.SyntaxKind.KeyOfKeyword, - f.createTypeReferenceNode(id), - ); +export const makeKeyOf = (subj: Parameters[0]) => + f.createTypeOperatorNode(ts.SyntaxKind.KeyOfKeyword, ensureTypeNode(subj)); export const makePromise = (subject: ts.TypeNode | "any") => f.createTypeReferenceNode(Promise.name, [ @@ -230,15 +235,11 @@ export const makeTypeParams = ( params: Partial>, ) => Object.entries(params).map(([name, val]) => - f.createTypeParameterDeclaration( - [], - name, - val && ts.isIdentifier(val) ? f.createTypeReferenceNode(val) : val, - ), + f.createTypeParameterDeclaration([], name, val && ensureTypeNode(val)), ); export const makeArrowFn = ( - params: ts.Identifier[], + params: ts.Identifier[] | Parameters[0], body: ts.ConciseBody, { isAsync, @@ -251,43 +252,14 @@ export const makeArrowFn = ( f.createArrowFunction( isAsync ? asyncModifier : undefined, typeParams && makeTypeParams(typeParams), - params.map((key) => makeParam(key)), + Array.isArray(params) + ? params.map((key) => makeParam(key)) + : makeParams(params), undefined, undefined, body, ); -export const makeObjectKeysReducer = ( - obj: ts.Identifier, - exp: ts.Expression, - initial: ts.Expression, -) => - f.createCallExpression( - f.createPropertyAccessExpression( - f.createCallExpression( - f.createPropertyAccessExpression( - f.createIdentifier(Object.name), - propOf("keys"), - ), - undefined, - [obj], - ), - propOf("reduce"), - ), - undefined, - [ - f.createArrowFunction( - undefined, - undefined, - makeParams({ acc: undefined, key: undefined }), - undefined, - undefined, - exp, - ), - initial, - ], - ); - export const propOf = (name: keyof NoInfer) => name as string; export const makeTernary = ( @@ -319,13 +291,6 @@ export const makePropCall = ( args, ); -export const makeAnd = (left: ts.Expression, right: ts.Expression) => - f.createBinaryExpression( - left, - f.createToken(ts.SyntaxKind.AmpersandAmpersandToken), - right, - ); - export const makeNew = (cls: ts.Identifier, ...args: ts.Expression[]) => f.createNewExpression(cls, undefined, args); diff --git a/src/zts-helpers.ts b/src/zts-helpers.ts index 69b0d122f..d5fc624ea 100644 --- a/src/zts-helpers.ts +++ b/src/zts-helpers.ts @@ -7,10 +7,7 @@ export type LiteralType = string | number | boolean; export interface ZTSContext extends FlatObject { isResponse: boolean; - makeAlias: ( - schema: z.ZodTypeAny, - produce: () => ts.TypeNode, - ) => ts.TypeReferenceNode; + makeAlias: (schema: z.ZodTypeAny, produce: () => ts.TypeNode) => ts.TypeNode; optionalPropStyle: { withQuestionMark?: boolean; withUndefined?: boolean }; } diff --git a/src/zts.ts b/src/zts.ts index 2b18e1531..6d16fd9c9 100644 --- a/src/zts.ts +++ b/src/zts.ts @@ -10,6 +10,7 @@ import { ezRawBrand, RawSchema } from "./raw-schema"; import { HandlingRules, walkSchema } from "./schema-walker"; import { addJsDocComment, + ensureTypeNode, isPrimitive, makeInterfaceProp, } from "./typescript-api"; @@ -206,7 +207,7 @@ const onLazy: Producer = (lazy: z.ZodLazy, { makeAlias, next }) => const onFile: Producer = (schema: FileSchema) => { const subject = schema.unwrap(); const stringType = f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword); - const bufferType = f.createTypeReferenceNode("Buffer"); + const bufferType = ensureTypeNode("Buffer"); const unionType = f.createUnionTypeNode([stringType, bufferType]); return subject instanceof z.ZodString ? stringType diff --git a/tests/unit/__snapshots__/integration.spec.ts.snap b/tests/unit/__snapshots__/integration.spec.ts.snap index e945351d4..4fc533448 100644 --- a/tests/unit/__snapshots__/integration.spec.ts.snap +++ b/tests/unit/__snapshots__/integration.spec.ts.snap @@ -65,6 +65,20 @@ export type Request = keyof Input; export const endpointTags = { "post /v1/test-with-dashes": [] }; +const parseRequest = (request: string) => + request.split(/ (.+)/, 2) as [Method, Path]; + +const substitute = (path: string, params: Record) => { + const rest = { ...params }; + for (const key in params) { + path = path.replace(\`:\${key}\`, () => { + delete rest[key]; + return params[key]; + }); + } + return [path, rest] as const; +}; + export type Implementation = ( method: Method, path: string, @@ -77,25 +91,8 @@ export class ExpressZodAPIClient { request: K, params: Input[K], ): Promise { - const [method, path] = request.split(/ (.+)/, 2) as [Method, Path]; - return this.implementation( - method, - Object.keys(params).reduce( - (acc, key) => - acc.replace(\`:\${key}\`, (params as Record)[key]), - path, - ), - Object.keys(params).reduce( - (acc, key) => - Object.assign( - acc, - !path.includes(\`:\${key}\`) && { - [key]: (params as Record)[key], - }, - ), - {}, - ), - ); + const [method, path] = parseRequest(request); + return this.implementation(method, ...substitute(path, params)); } } @@ -192,6 +189,20 @@ export type Request = keyof Input; export const endpointTags = { "post /v1/test-with-dashes": [] }; +const parseRequest = (request: string) => + request.split(/ (.+)/, 2) as [Method, Path]; + +const substitute = (path: string, params: Record) => { + const rest = { ...params }; + for (const key in params) { + path = path.replace(\`:\${key}\`, () => { + delete rest[key]; + return params[key]; + }); + } + return [path, rest] as const; +}; + export type Implementation = ( method: Method, path: string, @@ -204,25 +215,8 @@ export class ExpressZodAPIClient { request: K, params: Input[K], ): Promise { - const [method, path] = request.split(/ (.+)/, 2) as [Method, Path]; - return this.implementation( - method, - Object.keys(params).reduce( - (acc, key) => - acc.replace(\`:\${key}\`, (params as Record)[key]), - path, - ), - Object.keys(params).reduce( - (acc, key) => - Object.assign( - acc, - !path.includes(\`:\${key}\`) && { - [key]: (params as Record)[key], - }, - ), - {}, - ), - ); + const [method, path] = parseRequest(request); + return this.implementation(method, ...substitute(path, params)); } } @@ -319,6 +313,20 @@ export type Request = keyof Input; export const endpointTags = { "post /v1/test-with-dashes": [] }; +const parseRequest = (request: string) => + request.split(/ (.+)/, 2) as [Method, Path]; + +const substitute = (path: string, params: Record) => { + const rest = { ...params }; + for (const key in params) { + path = path.replace(\`:\${key}\`, () => { + delete rest[key]; + return params[key]; + }); + } + return [path, rest] as const; +}; + export type Implementation = ( method: Method, path: string, @@ -331,25 +339,8 @@ export class ExpressZodAPIClient { request: K, params: Input[K], ): Promise { - const [method, path] = request.split(/ (.+)/, 2) as [Method, Path]; - return this.implementation( - method, - Object.keys(params).reduce( - (acc, key) => - acc.replace(\`:\${key}\`, (params as Record)[key]), - path, - ), - Object.keys(params).reduce( - (acc, key) => - Object.assign( - acc, - !path.includes(\`:\${key}\`) && { - [key]: (params as Record)[key], - }, - ), - {}, - ), - ); + const [method, path] = parseRequest(request); + return this.implementation(method, ...substitute(path, params)); } } @@ -864,6 +855,20 @@ export const endpointTags = { "get /v1/events/time": ["subscriptions"], }; +const parseRequest = (request: string) => + request.split(/ (.+)/, 2) as [Method, Path]; + +const substitute = (path: string, params: Record) => { + const rest = { ...params }; + for (const key in params) { + path = path.replace(\`:\${key}\`, () => { + delete rest[key]; + return params[key]; + }); + } + return [path, rest] as const; +}; + export type Implementation = ( method: Method, path: string, @@ -876,25 +881,8 @@ export class ExpressZodAPIClient { request: K, params: Input[K], ): Promise { - const [method, path] = request.split(/ (.+)/, 2) as [Method, Path]; - return this.implementation( - method, - Object.keys(params).reduce( - (acc, key) => - acc.replace(\`:\${key}\`, (params as Record)[key]), - path, - ), - Object.keys(params).reduce( - (acc, key) => - Object.assign( - acc, - !path.includes(\`:\${key}\`) && { - [key]: (params as Record)[key], - }, - ), - {}, - ), - ); + const [method, path] = parseRequest(request); + return this.implementation(method, ...substitute(path, params)); } } @@ -1470,6 +1458,20 @@ export type Request = keyof Input; export const endpointTags = { "post /v1/test-with-dashes": [] }; +const parseRequest = (request: string) => + request.split(/ (.+)/, 2) as [Method, Path]; + +const substitute = (path: string, params: Record) => { + const rest = { ...params }; + for (const key in params) { + path = path.replace(\`:\${key}\`, () => { + delete rest[key]; + return params[key]; + }); + } + return [path, rest] as const; +}; + export type Implementation = ( method: Method, path: string, @@ -1482,25 +1484,8 @@ export class ExpressZodAPIClient { request: K, params: Input[K], ): Promise { - const [method, path] = request.split(/ (.+)/, 2) as [Method, Path]; - return this.implementation( - method, - Object.keys(params).reduce( - (acc, key) => - acc.replace(\`:\${key}\`, (params as Record)[key]), - path, - ), - Object.keys(params).reduce( - (acc, key) => - Object.assign( - acc, - !path.includes(\`:\${key}\`) && { - [key]: (params as Record)[key], - }, - ), - {}, - ), - ); + const [method, path] = parseRequest(request); + return this.implementation(method, ...substitute(path, params)); } }