Skip to content

Commit c4ad101

Browse files
authored
fix: error if route placeholder present without matching path parameter (#331)
closes #330
1 parent 07fcbe7 commit c4ad101

8 files changed

Lines changed: 65 additions & 62 deletions

File tree

packages/openapi-code-generator/src/typescript/client/client-operation-builder.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,38 @@ export class ClientOperationBuilder {
5252
return this.operation.servers.length > 0
5353
}
5454

55+
routeToTemplateString(paramName = "p"): string {
56+
const {route, parameters} = this.operation
57+
const placeholder = /{([^{}]+)}/g
58+
59+
return Array.from(route.matchAll(placeholder)).reduce((result, match) => {
60+
const wholeString = match[0]
61+
const placeholderName = match[1]
62+
63+
if (!placeholderName) {
64+
throw new Error(
65+
`invalid route parameter placeholder in route '${placeholder}'`,
66+
)
67+
}
68+
69+
const parameter = parameters.find(
70+
(it) => it.name === placeholderName && it.in === "path",
71+
)
72+
73+
if (!parameter) {
74+
throw new Error(
75+
`invalid route ${route}. missing path parameter for '${placeholderName}'`,
76+
)
77+
}
78+
79+
return result.replace(
80+
wholeString,
81+
// TODO: why do we camelCase here? Feels presumptuous
82+
`\${${paramName}["${camelCase(placeholderName)}"]}`,
83+
)
84+
}, route)
85+
}
86+
5587
methodParameter(): MethodParameterDefinition | undefined {
5688
const {parameters} = this.operation
5789
const {requestBodyParameter} = this.requestBodyAsParameter()

packages/openapi-code-generator/src/typescript/client/typescript-angular/angular-service-builder.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import type {ImportBuilder} from "../../common/import-builder"
22
import {union} from "../../common/type-utils"
3-
import {
4-
buildMethod,
5-
routeToTemplateString,
6-
} from "../../common/typescript-common"
3+
import {buildMethod} from "../../common/typescript-common"
74
import {AbstractClientBuilder} from "../abstract-client-builder"
85
import type {ClientOperationBuilder} from "../client-operation-builder"
96

@@ -19,7 +16,7 @@ export class AngularServiceBuilder extends AbstractClientBuilder {
1916
}
2017

2118
protected buildOperation(builder: ClientOperationBuilder): string {
22-
const {operationId, route, method, hasServers} = builder
19+
const {operationId, method, hasServers} = builder
2320
const {requestBodyParameter} = builder.requestBodyAsParameter()
2421

2522
const operationParameter = builder.methodParameter()
@@ -35,7 +32,7 @@ export class AngularServiceBuilder extends AbstractClientBuilder {
3532
.concat(["HttpResponse<unknown>"])
3633
.join(" | ")
3734

38-
const url = routeToTemplateString(route)
35+
const url = builder.routeToTemplateString()
3936

4037
const body = `
4138
${[

packages/openapi-code-generator/src/typescript/client/typescript-axios/typescript-axios-client-builder.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import type {ImportBuilder} from "../../common/import-builder"
22
import {union} from "../../common/type-utils"
3-
import {
4-
asyncMethod,
5-
routeToTemplateString,
6-
} from "../../common/typescript-common"
3+
import {asyncMethod} from "../../common/typescript-common"
74
import {AbstractClientBuilder} from "../abstract-client-builder"
85
import type {ClientOperationBuilder} from "../client-operation-builder"
96

@@ -19,7 +16,7 @@ export class TypescriptAxiosClientBuilder extends AbstractClientBuilder {
1916
}
2017

2118
protected buildOperation(builder: ClientOperationBuilder): string {
22-
const {operationId, route, method, hasServers} = builder
19+
const {operationId, method, hasServers} = builder
2320
const {requestBodyParameter} = builder.requestBodyAsParameter()
2421

2522
const operationParameter = builder.methodParameter()
@@ -64,7 +61,7 @@ export class TypescriptAxiosClientBuilder extends AbstractClientBuilder {
6461
.join(",\n")}})`
6562

6663
const body = `
67-
const url = \`${routeToTemplateString(route)}\`
64+
const url = \`${builder.routeToTemplateString()}\`
6865
${[
6966
headers
7067
? `const headers = this._headers(${headers}, opts.headers)`

packages/openapi-code-generator/src/typescript/client/typescript-fetch/typescript-fetch-client-builder.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@ import type {ImportBuilder} from "../../common/import-builder"
22
import {JoiBuilder} from "../../common/schema-builders/joi-schema-builder"
33
import {ZodBuilder} from "../../common/schema-builders/zod-schema-builder"
44
import {union} from "../../common/type-utils"
5-
import {
6-
asyncMethod,
7-
routeToTemplateString,
8-
} from "../../common/typescript-common"
5+
import {asyncMethod} from "../../common/typescript-common"
96
import {AbstractClientBuilder} from "../abstract-client-builder"
107
import type {ClientOperationBuilder} from "../client-operation-builder"
118

@@ -39,7 +36,7 @@ export class TypescriptFetchClientBuilder extends AbstractClientBuilder {
3936
}
4037

4138
protected buildOperation(builder: ClientOperationBuilder): string {
42-
const {operationId, route, method, hasServers} = builder
39+
const {operationId, method, hasServers} = builder
4340
const {requestBodyParameter} = builder.requestBodyAsParameter()
4441

4542
const operationParameter = builder.methodParameter()
@@ -69,7 +66,7 @@ export class TypescriptFetchClientBuilder extends AbstractClientBuilder {
6966
.join("\n")}}, timeout)`
7067

7168
const body = `
72-
const url = ${hasServers ? "basePath" : "this.basePath"} + \`${routeToTemplateString(route)}\`
69+
const url = ${hasServers ? "basePath" : "this.basePath"} + \`${builder.routeToTemplateString()}\`
7370
${[
7471
headers
7572
? `const headers = this._headers(${headers}, opts.headers)`

packages/openapi-code-generator/src/typescript/common/typescript-common.ts

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -60,26 +60,6 @@ export function combineAndDestructureParams(
6060
}
6161
}
6262

63-
export function routeToTemplateString(route: string, paramName = "p"): string {
64-
const placeholder = /{([^{}]+)}/g
65-
66-
return Array.from(route.matchAll(placeholder)).reduce((result, match) => {
67-
const wholeString = match[0]
68-
const placeholderName = match[1]
69-
70-
if (!placeholderName) {
71-
throw new Error(
72-
`invalid route parameter placeholder in route '${placeholder}'`,
73-
)
74-
}
75-
76-
return result.replace(
77-
wholeString,
78-
`\${${paramName}["${camelCase(placeholderName)}"]}`,
79-
)
80-
}, route)
81-
}
82-
8363
export function buildMethod({
8464
name,
8565
parameters,

packages/openapi-code-generator/src/typescript/server/server-operation-builder.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,26 @@ export class ServerOperationBuilder {
9191
}
9292

9393
get route(): string {
94-
return this.operation.route
94+
const {route, parameters} = this.operation
95+
96+
const placeholder = /{([^{}]+)}/g
97+
98+
return Array.from(route.matchAll(placeholder)).reduce((result, match) => {
99+
const wholeString = match[0]
100+
const placeholderName = match[1]
101+
102+
const parameter = parameters.find(
103+
(it) => it.name === placeholderName && it.in === "path",
104+
)
105+
106+
if (!parameter) {
107+
throw new Error(
108+
`invalid route ${route}. missing path parameter for '${placeholderName}'`,
109+
)
110+
}
111+
112+
return result.replace(wholeString, `:${placeholderName}`)
113+
}, route)
95114
}
96115

97116
get method(): string {

packages/openapi-code-generator/src/typescript/server/typescript-express/typescript-express-router-builder.ts

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
import type {Input} from "../../../core/input"
2-
import {isDefined, titleCase} from "../../../core/utils"
2+
import {titleCase} from "../../../core/utils"
33
import type {ServerImplementationMethod} from "../../../templates.types"
44
import type {ImportBuilder} from "../../common/import-builder"
55
import {JoiBuilder} from "../../common/schema-builders/joi-schema-builder"
66
import type {SchemaBuilder} from "../../common/schema-builders/schema-builder"
77
import {ZodBuilder} from "../../common/schema-builders/zod-schema-builder"
88
import type {TypeBuilder} from "../../common/type-builder"
99
import {constStatement, object} from "../../common/type-utils"
10-
import {
11-
buildExport,
12-
routeToTemplateString,
13-
} from "../../common/typescript-common"
10+
import {buildExport} from "../../common/typescript-common"
1411
import {
1512
AbstractRouterBuilder,
1613
type ServerSymbols,
@@ -126,7 +123,7 @@ export class ExpressRouterBuilder extends AbstractRouterBuilder {
126123
const ${symbols.responseBodyValidator} = ${builder.responseValidator()}
127124
128125
// ${builder.operationId}
129-
router.${builder.method.toLowerCase()}(\`${route(builder.route)}\`, async (req: Request, res: Response, next: NextFunction) => {
126+
router.${builder.method.toLowerCase()}(\`${builder.route}\`, async (req: Request, res: Response, next: NextFunction) => {
130127
try {
131128
const input = {
132129
params: ${params.path.schema ? `parseRequestInput(${symbols.paramSchema}, req.params, RequestInputType.RouteParam)` : "undefined"},
@@ -240,11 +237,3 @@ export ${this.implementationMethod === "type" || this.implementationMethod === "
240237
}
241238
}
242239
}
243-
244-
function route(route: string): string {
245-
const placeholder = /{([^{}]+)}/g
246-
247-
return Array.from(route.matchAll(placeholder)).reduce((result, match) => {
248-
return result.replace(match[0], `:${match[1]}`)
249-
}, route)
250-
}

packages/openapi-code-generator/src/typescript/server/typescript-koa/typescript-koa-router-builder.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export class KoaRouterBuilder extends AbstractRouterBuilder {
132132
statements.push(`
133133
const ${symbols.responseBodyValidator} = ${builder.responseValidator()}
134134
135-
router.${builder.method.toLowerCase()}('${symbols.implPropName}','${route(builder.route)}', async (ctx, next) => {
135+
router.${builder.method.toLowerCase()}('${symbols.implPropName}','${builder.route}', async (ctx, next) => {
136136
const input = {
137137
params: ${params.path.schema ? `parseRequestInput(${symbols.paramSchema}, ctx.params, RequestInputType.RouteParam)` : "undefined"},
138138
query: ${params.query.schema ? `parseRequestInput(${symbols.querySchema}, ctx.query, RequestInputType.QueryString)` : "undefined"},
@@ -241,11 +241,3 @@ export ${this.implementationMethod === "type" || this.implementationMethod === "
241241
`
242242
}
243243
}
244-
245-
function route(route: string): string {
246-
const placeholder = /{([^{}]+)}/g
247-
248-
return Array.from(route.matchAll(placeholder)).reduce((result, match) => {
249-
return result.replace(match[0], `:${match[1]}`)
250-
}, route)
251-
}

0 commit comments

Comments
 (0)