Skip to content

Commit b49f355

Browse files
committed
Start refactoring request functions
1 parent 1cc6db5 commit b49f355

File tree

8 files changed

+128
-58
lines changed

8 files changed

+128
-58
lines changed

src/compileOperation.ts

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { namedTypes, builders } from 'ast-types';
33
import { Compiler } from './compiler';
44
import { OpenAPIOperation } from './types';
55
import { compileValueSchema } from './compileValueSchema';
6-
import { ValidationErrorIdentifier } from './error';
6+
import { ValidationErrorIdentifier, buildValidationError } from './error';
77

88
/**
99
* Compile an operation into a function.
@@ -17,15 +17,19 @@ import { ValidationErrorIdentifier } from './error';
1717
* }
1818
*/
1919
export function compileOperation(compiler: Compiler, operation: OpenAPIOperation) {
20-
return compiler.declareValidationFunction(operation, ({ value, path, error }) => {
20+
return compiler.declareForInput(operation, (functionId) => {
21+
const requestIdentifier = builders.identifier('request');
22+
const pathMatchIdentifier = builders.identifier('pathMatch');
23+
const contextIdentifier = builders.identifier('context');
24+
2125
const nodes: namedTypes.BlockStatement['body'] = [];
2226

2327
if (operation.operationId) {
2428
nodes.push(
2529
builders.expressionStatement(
2630
builders.assignmentExpression(
2731
'=',
28-
builders.memberExpression(value, builders.identifier('operationId')),
32+
builders.memberExpression(requestIdentifier, builders.identifier('operationId')),
2933
builders.literal(operation.operationId),
3034
),
3135
),
@@ -38,11 +42,11 @@ export function compileOperation(compiler: Compiler, operation: OpenAPIOperation
3842
builders.ifStatement(
3943
builders.binaryExpression(
4044
'===',
41-
builders.memberExpression(value, builders.identifier('body')),
45+
builders.memberExpression(requestIdentifier, builders.identifier('body')),
4246
builders.identifier('undefined'),
4347
),
4448
builders.blockStatement([
45-
builders.returnStatement(error('body is required')),
49+
builders.returnStatement(buildValidationError('body is required')),
4650
]),
4751
),
4852
);
@@ -59,10 +63,9 @@ export function compileOperation(compiler: Compiler, operation: OpenAPIOperation
5963
bodyResult,
6064
builders.callExpression(bodyFn, [
6165
builders.arrayExpression([
62-
builders.spreadElement(path),
6366
builders.literal('body'),
6467
]),
65-
builders.memberExpression(value, builders.identifier('body')),
68+
builders.memberExpression(requestIdentifier, builders.identifier('body')),
6669
]),
6770
),
6871
]),
@@ -80,7 +83,7 @@ export function compileOperation(compiler: Compiler, operation: OpenAPIOperation
8083
builders.expressionStatement(
8184
builders.assignmentExpression(
8285
'=',
83-
builders.memberExpression(value, builders.identifier('body')),
86+
builders.memberExpression(requestIdentifier, builders.identifier('body')),
8487
bodyResult,
8588
),
8689
),
@@ -93,18 +96,25 @@ export function compileOperation(compiler: Compiler, operation: OpenAPIOperation
9396
builders.ifStatement(
9497
builders.binaryExpression(
9598
'!==',
96-
builders.memberExpression(value, builders.identifier('body')),
99+
builders.memberExpression(requestIdentifier, builders.identifier('body')),
97100
builders.identifier('undefined'),
98101
),
99102
builders.blockStatement([
100-
builders.returnStatement(error('body is not allowed')),
103+
builders.returnStatement(buildValidationError('body is not allowed')),
101104
]),
102105
),
103106
);
104107
}
105108

106-
nodes.push(builders.returnStatement(value));
109+
nodes.push(builders.returnStatement(requestIdentifier));
107110

108-
return nodes;
111+
return builders.functionDeclaration(functionId,
112+
[
113+
requestIdentifier,
114+
pathMatchIdentifier,
115+
contextIdentifier,
116+
],
117+
builders.blockStatement(nodes)
118+
)
109119
});
110120
}

src/compilePath.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { namedTypes, builders } from 'ast-types';
33
import { Compiler } from './compiler';
44
import { OpenAPIPath } from './types';
55
import { compileOperation } from './compileOperation';
6+
import { buildValidationError } from './error';
67

78
/**
89
* Compile a path into a function.
@@ -16,7 +17,11 @@ import { compileOperation } from './compileOperation';
1617
* }
1718
*/
1819
export function compilePath(compiler: Compiler, pathOperations: OpenAPIPath) {
19-
return compiler.declareValidationFunction(pathOperations, ({ value, path, context, error }) => {
20+
return compiler.declareForInput(pathOperations, (functionId) => {
21+
const requestIdentifier = builders.identifier('request');
22+
const pathMatchIdentifier = builders.identifier('pathMatch');
23+
const contextIdentifier = builders.identifier('context');
24+
2025
const nodes: namedTypes.BlockStatement['body'] = [];
2126

2227
Object.entries(pathOperations).forEach(([method, operation]) => {
@@ -26,20 +31,27 @@ export function compilePath(compiler: Compiler, pathOperations: OpenAPIPath) {
2631
builders.ifStatement(
2732
builders.binaryExpression(
2833
'===',
29-
builders.memberExpression(value, builders.identifier('method')),
34+
builders.memberExpression(requestIdentifier, builders.identifier('method')),
3035
builders.literal(method),
3136
),
3237
builders.blockStatement([
3338
builders.returnStatement(
34-
builders.callExpression(fnOperation, [path, value, context]),
39+
builders.callExpression(fnOperation, [requestIdentifier, pathMatchIdentifier, contextIdentifier]),
3540
),
3641
]),
3742
),
3843
);
3944
});
4045

41-
nodes.push(builders.returnStatement(error('method not supported')));
46+
nodes.push(builders.returnStatement(buildValidationError('method not supported')));
4247

43-
return nodes;
48+
return builders.functionDeclaration(functionId,
49+
[
50+
requestIdentifier,
51+
pathMatchIdentifier,
52+
contextIdentifier,
53+
],
54+
builders.blockStatement(nodes)
55+
)
4456
});
4557
}

src/compileValidateRequest.ts

Lines changed: 63 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { pathToRegexp, Key } from 'path-to-regexp';
44
import { Compiler } from './compiler';
55
import { OpenAPISpec } from './types';
66
import { compilePath } from './compilePath';
7-
import { ValidationErrorIdentifier } from './error';
7+
import { buildValidationError } from './error';
88

99
/**
1010
* Compile a request validation function.
@@ -25,45 +25,43 @@ export function compileValidateRequest(compiler: Compiler, spec: OpenAPISpec) {
2525
const request = builders.identifier('request');
2626
const context = builders.identifier('context');
2727

28-
const nodes: namedTypes.Program['body'] = [];
29-
const functionNodes: namedTypes.FunctionDeclaration['body']['body'] = [];
28+
const nodes: namedTypes.FunctionDeclaration['body']['body'] = [];
3029

3130
Object.entries(spec.paths ?? {}).map(([path, pathOperations], index) => {
32-
const keys: Key[] = [];
33-
const regex = pathToRegexp(path);
34-
3531
const operationFn = compilePath(compiler, pathOperations);
3632

3733
// Declare the regexp globally to avoid recompiling it at each execution
38-
const regexpIdentifier = builders.identifier(`pathRegexp${index}`);
34+
const regexpIdentifier = compilePathRegexp(compiler, path)
35+
36+
const matchIdentifier = builders.identifier(`match${index}`);
37+
38+
// In the function, evaluate the regexp against the path
3939
nodes.push(
4040
builders.variableDeclaration('const', [
4141
builders.variableDeclarator(
42-
regexpIdentifier,
43-
builders.newExpression(builders.identifier('RegExp'), [
44-
builders.literal(regex.source),
45-
builders.literal(regex.flags),
46-
]),
42+
matchIdentifier,
43+
builders.callExpression(
44+
builders.memberExpression(
45+
builders.memberExpression(request, builders.identifier('path')),
46+
builders.identifier('match'),
47+
),
48+
[regexpIdentifier],
49+
),
4750
),
4851
]),
49-
);
50-
51-
// In the function, evaluate the regexp against the path
52-
functionNodes.push(
52+
)
53+
nodes.push(
5354
builders.ifStatement(
5455
builders.binaryExpression(
55-
'===',
56-
builders.callExpression(
57-
builders.memberExpression(regexpIdentifier, builders.identifier('test')),
58-
[builders.memberExpression(request, builders.identifier('path'))],
59-
),
60-
builders.literal(true),
56+
'!==',
57+
matchIdentifier,
58+
builders.literal(null),
6159
),
6260
builders.blockStatement([
6361
builders.returnStatement(
6462
builders.callExpression(operationFn, [
65-
builders.arrayExpression([]),
6663
request,
64+
matchIdentifier,
6765
context,
6866
]),
6967
),
@@ -73,24 +71,56 @@ export function compileValidateRequest(compiler: Compiler, spec: OpenAPISpec) {
7371
});
7472

7573
// Otherwise, return an error
76-
functionNodes.push(
74+
nodes.push(
7775
builders.returnStatement(
78-
builders.newExpression(ValidationErrorIdentifier, [
79-
builders.arrayExpression([]),
80-
builders.literal('no operation match path'),
81-
]),
76+
buildValidationError('no operation match path'),
8277
),
8378
);
8479

85-
nodes.push(
80+
return [
8681
builders.exportNamedDeclaration(
8782
builders.functionDeclaration(
8883
builders.identifier('validateRequest'),
8984
[request, context],
90-
builders.blockStatement(functionNodes),
85+
builders.blockStatement(nodes),
9186
),
92-
),
93-
);
87+
)
88+
];
89+
}
90+
91+
/**
92+
* Compile a path to a regex identifier.
93+
*/
94+
function compilePathRegexp(compiler: Compiler, path: string) {
95+
return compiler.declareForInput(path, (id) => {
96+
const { regex } = openapiPathToRegex(path);
97+
98+
return builders.variableDeclaration('const', [
99+
builders.variableDeclarator(
100+
id,
101+
builders.newExpression(builders.identifier('RegExp'), [
102+
builders.literal(regex.source),
103+
builders.literal(regex.flags),
104+
]),
105+
),
106+
])
107+
});
108+
}
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+
}
94122

95-
return nodes;
123+
const keys: Key[] = [];
124+
const regex = pathToRegexp(path, keys);
125+
return { regex, keys };
96126
}

src/compileValueSchema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ function compileObjectSchema(compiler: Compiler, schema: OpenAPIObjectSchema) {
224224
builders.binaryExpression(
225225
'===',
226226
value,
227-
builders.identifier('null'),
227+
builders.literal(null),
228228
)),
229229
builders.blockStatement([builders.returnStatement(error('Expected an object'))]),
230230
),
@@ -766,7 +766,7 @@ function compileNullableCheck(
766766

767767
return [
768768
builders.ifStatement(
769-
builders.binaryExpression('===', value, builders.identifier('null')),
769+
builders.binaryExpression('===', value, builders.literal(null)),
770770
builders.blockStatement([builders.returnStatement(value)]),
771771
),
772772
];

src/compiler.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export class Compiler {
1818
private objectHashes: WeakMap<object, string> = new WeakMap();
1919

2020
/** Counter to get a new identifier */
21-
private identifierCounter: number = 0;
21+
private declarationCounter: number = 0;
2222

2323
/** Map of identifiers defined globally */
2424
private globalDeclarations: (namedTypes.FunctionDeclaration | namedTypes.ClassDeclaration | namedTypes.VariableDeclaration)[] = [ValidationErrorClass];
@@ -44,7 +44,7 @@ export class Compiler {
4444
input: any,
4545
gen: (id: namedTypes.Identifier) => namedTypes.FunctionDeclaration | namedTypes.ClassDeclaration | namedTypes.VariableDeclaration,
4646
): namedTypes.Identifier {
47-
const hash = this.hashObject(input);
47+
const hash = this.hashInput(input);
4848
const identifier = builders.identifier(hash);
4949

5050
if (!this.processedHashes.has(hash)) {
@@ -130,7 +130,7 @@ export class Compiler {
130130
/**
131131
* Hash an object and return an identifier name.
132132
*/
133-
public hashObject(input: any): string {
133+
public hashInput(input: any): string {
134134
const isObject = typeof input === 'object' && input !== null;
135135

136136
// Fast track for objects
@@ -139,12 +139,11 @@ export class Compiler {
139139
}
140140

141141
const hashValue = hash(input);
142-
143142
if (this.hashes.has(hashValue)) {
144143
return this.hashes.get(hashValue)!;
145144
}
146145

147-
const name = `obj${this.identifierCounter++}`;
146+
const name = `obj${this.declarationCounter++}`;
148147
this.hashes.set(hashValue, name);
149148
if (isObject) {
150149
this.objectHashes.set(input, name);

src/error.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,14 @@ export const ValidationErrorClass = builders.classDeclaration.from({
3131
),
3232
]),
3333
});
34+
35+
/**
36+
* Build an empty error.
37+
*/
38+
export function buildValidationError(message: string) {
39+
return builders.newExpression(ValidationErrorIdentifier, [
40+
builders.arrayExpression([]),
41+
builders.literal(message),
42+
]);
43+
}
44+

src/hash.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const PRESERVE_PROPS = [
4141
* Normalize the input value as an object.
4242
*/
4343
function normalizeHashInput(input: any): object {
44-
if (typeof input !== 'object' || input === null) {
44+
if (typeof input === 'object' && input !== null) {
4545
// Remove all properties that are not important for the hash.
4646
input = Object.keys(input).reduce((acc, key) => {
4747
if (PRESERVE_PROPS.includes(key)) {

src/tests/hash.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { hash } from "../hash"
2+
3+
4+
describe('Strings', () => {
5+
test('different', () => {
6+
expect(hash('foo')).not.toEqual(hash('bar'))
7+
})
8+
})

0 commit comments

Comments
 (0)