Skip to content

Commit 8344fb1

Browse files
committed
Process path params
1 parent b49f355 commit 8344fb1

File tree

4 files changed

+123
-28
lines changed

4 files changed

+123
-28
lines changed

src/compileOperation.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Compiler } from './compiler';
44
import { OpenAPIOperation } from './types';
55
import { compileValueSchema } from './compileValueSchema';
66
import { ValidationErrorIdentifier, buildValidationError } from './error';
7+
import { OpenAPIParsedPath, getPathParamIndex, openapiPathToRegex } from './paths';
78

89
/**
910
* Compile an operation into a function.
@@ -16,14 +17,19 @@ import { ValidationErrorIdentifier, buildValidationError } from './error';
1617
* headers: any;
1718
* }
1819
*/
19-
export function compileOperation(compiler: Compiler, operation: OpenAPIOperation) {
20+
export function compileOperation(
21+
compiler: Compiler,
22+
path: OpenAPIParsedPath,
23+
operation: OpenAPIOperation
24+
) {
2025
return compiler.declareForInput(operation, (functionId) => {
2126
const requestIdentifier = builders.identifier('request');
2227
const pathMatchIdentifier = builders.identifier('pathMatch');
2328
const contextIdentifier = builders.identifier('context');
2429

2530
const nodes: namedTypes.BlockStatement['body'] = [];
2631

32+
// Set the operationId on the request
2733
if (operation.operationId) {
2834
nodes.push(
2935
builders.expressionStatement(
@@ -36,6 +42,74 @@ export function compileOperation(compiler: Compiler, operation: OpenAPIOperation
3642
);
3743
}
3844

45+
// Extract and validate path params
46+
const pathParameters = (operation.parameters ?? []).filter((parameter) => parameter.in === 'path');
47+
nodes.push(
48+
builders.expressionStatement(
49+
builders.assignmentExpression(
50+
'=',
51+
builders.memberExpression(requestIdentifier, builders.identifier('params')),
52+
builders.objectExpression(
53+
pathParameters.map((parameter, index) => {
54+
const identifier = builders.identifier(`pathParam${index}`);
55+
const schemaFn = compileValueSchema(compiler, parameter.schema);
56+
const regexMatchIndex = getPathParamIndex(path, parameter.name);
57+
58+
nodes.push(
59+
builders.variableDeclaration(
60+
'const',
61+
[
62+
builders.variableDeclarator(
63+
identifier,
64+
builders.callExpression(schemaFn, [
65+
builders.arrayExpression([
66+
builders.literal('path'),
67+
builders.literal(parameter.name),
68+
]),
69+
builders.memberExpression(
70+
pathMatchIdentifier,
71+
builders.literal(regexMatchIndex),
72+
true,
73+
),
74+
contextIdentifier,
75+
]),
76+
),
77+
],
78+
)
79+
);
80+
81+
// Return an error if the parameter is invalid
82+
nodes.push(
83+
builders.ifStatement(
84+
builders.binaryExpression(
85+
'instanceof',
86+
identifier,
87+
ValidationErrorIdentifier,
88+
),
89+
builders.blockStatement([
90+
builders.returnStatement(identifier),
91+
]),
92+
),
93+
);
94+
95+
return builders.property(
96+
'init',
97+
builders.identifier(parameter.name),
98+
identifier,
99+
);
100+
}).flat()
101+
),
102+
),
103+
),
104+
);
105+
106+
// Validate query parameters
107+
const queryParameters = (operation.parameters ?? []).filter((parameter) => parameter.in === 'path');
108+
queryParameters.forEach((parameter) => {
109+
110+
});
111+
112+
// Validate the body against the schema
39113
if (operation.requestBody) {
40114
if (operation.requestBody.required) {
41115
nodes.push(

src/compilePath.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Compiler } from './compiler';
44
import { OpenAPIPath } from './types';
55
import { compileOperation } from './compileOperation';
66
import { buildValidationError } from './error';
7+
import { OpenAPIParsedPath } from './paths';
78

89
/**
910
* Compile a path into a function.
@@ -16,7 +17,11 @@ import { buildValidationError } from './error';
1617
* headers: any;
1718
* }
1819
*/
19-
export function compilePath(compiler: Compiler, pathOperations: OpenAPIPath) {
20+
export function compilePath(
21+
compiler: Compiler,
22+
path: OpenAPIParsedPath,
23+
pathOperations: OpenAPIPath
24+
) {
2025
return compiler.declareForInput(pathOperations, (functionId) => {
2126
const requestIdentifier = builders.identifier('request');
2227
const pathMatchIdentifier = builders.identifier('pathMatch');
@@ -25,7 +30,7 @@ export function compilePath(compiler: Compiler, pathOperations: OpenAPIPath) {
2530
const nodes: namedTypes.BlockStatement['body'] = [];
2631

2732
Object.entries(pathOperations).forEach(([method, operation]) => {
28-
const fnOperation = compileOperation(compiler, operation);
33+
const fnOperation = compileOperation(compiler,path, operation);
2934

3035
nodes.push(
3136
builders.ifStatement(

src/compileValidateRequest.ts

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { namedTypes, builders } from 'ast-types';
2-
import { pathToRegexp, Key } from 'path-to-regexp';
32

43
import { Compiler } from './compiler';
54
import { OpenAPISpec } from './types';
65
import { compilePath } from './compilePath';
76
import { buildValidationError } from './error';
7+
import { OpenAPIParsedPath, openapiPathToRegex } from './paths';
88

99
/**
1010
* Compile a request validation function.
@@ -28,14 +28,14 @@ export function compileValidateRequest(compiler: Compiler, spec: OpenAPISpec) {
2828
const nodes: namedTypes.FunctionDeclaration['body']['body'] = [];
2929

3030
Object.entries(spec.paths ?? {}).map(([path, pathOperations], index) => {
31-
const operationFn = compilePath(compiler, pathOperations);
31+
const parsedPath = openapiPathToRegex(path);
32+
const operationFn = compilePath(compiler, parsedPath, pathOperations);
3233

3334
// Declare the regexp globally to avoid recompiling it at each execution
34-
const regexpIdentifier = compilePathRegexp(compiler, path)
35-
36-
const matchIdentifier = builders.identifier(`match${index}`);
35+
const regexpIdentifier = compilePathRegexp(compiler, path, parsedPath)
3736

3837
// In the function, evaluate the regexp against the path
38+
const matchIdentifier = builders.identifier(`match${index}`);
3939
nodes.push(
4040
builders.variableDeclaration('const', [
4141
builders.variableDeclarator(
@@ -91,9 +91,9 @@ export function compileValidateRequest(compiler: Compiler, spec: OpenAPISpec) {
9191
/**
9292
* Compile a path to a regex identifier.
9393
*/
94-
function compilePathRegexp(compiler: Compiler, path: string) {
94+
function compilePathRegexp(compiler: Compiler, path: string, parsed: OpenAPIParsedPath) {
9595
return compiler.declareForInput(path, (id) => {
96-
const { regex } = openapiPathToRegex(path);
96+
const { regex } = parsed;
9797

9898
return builders.variableDeclaration('const', [
9999
builders.variableDeclarator(
@@ -106,21 +106,3 @@ function compilePathRegexp(compiler: Compiler, path: string) {
106106
])
107107
});
108108
}
109-
110-
/**
111-
* Compile an OpenAPI path to a regex.
112-
*/
113-
export function openapiPathToRegex(path: string): {
114-
regex: RegExp;
115-
keys: Key[];
116-
} {
117-
// Normalize the path to convert {param} as :param
118-
const rxPathParam = /{([^}]+)}/;
119-
while (rxPathParam.test(path)) {
120-
path = path.replace(rxPathParam, ':$1');
121-
}
122-
123-
const keys: Key[] = [];
124-
const regex = pathToRegexp(path, keys);
125-
return { regex, keys };
126-
}

src/paths.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { pathToRegexp, Key } from 'path-to-regexp';
2+
3+
export interface OpenAPIParsedPath {
4+
regex: RegExp;
5+
keys: Key[];
6+
}
7+
8+
/**
9+
* Compile an OpenAPI path to a regex.
10+
*/
11+
export function openapiPathToRegex(path: string): OpenAPIParsedPath {
12+
// Normalize the path to convert {param} as :param
13+
const rxPathParam = /{([^}]+)}/;
14+
while (rxPathParam.test(path)) {
15+
path = path.replace(rxPathParam, ':$1');
16+
}
17+
18+
const keys: Key[] = [];
19+
const regex = pathToRegexp(path, keys);
20+
return { regex, keys };
21+
}
22+
23+
/**
24+
* Get the index of the value in the regexp match corresponding to a named parameter.
25+
*/
26+
export function getPathParamIndex(parsed: OpenAPIParsedPath, name: string) {
27+
const index = parsed.keys.findIndex((key) => key.name === name);
28+
if (index === -1) {
29+
throw new Error(`could not find parameter ${name} in path`);
30+
}
31+
32+
// Add 1 because the first value is the full match
33+
return index + 1;
34+
}

0 commit comments

Comments
 (0)