Skip to content

Commit 8b889c2

Browse files
committed
Start validating operation
1 parent 6a65d23 commit 8b889c2

File tree

8 files changed

+313
-18
lines changed

8 files changed

+313
-18
lines changed

bun.lockb

361 Bytes
Binary file not shown.

package.json

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
{
2-
"name": "openapi-to-validate",
3-
"version": "0.0.0",
4-
"description": "Compile an OpenAPI file to a JS validation function for requests",
5-
"main": "index.js",
6-
"scripts": {
7-
"test": "bun test",
8-
"format": "prettier ./ --ignore-unknown --write"
9-
},
10-
"author": "",
11-
"license": "ISC",
12-
"dependencies": {
13-
"ast-types": "^0.14.2",
14-
"escodegen": "^2.1.0"
15-
},
16-
"devDependencies": {
17-
"@types/escodegen": "^0.0.9",
18-
"prettier": "^3.0.3"
19-
}
2+
"name": "openapi-to-validate",
3+
"version": "0.0.0",
4+
"description": "Compile an OpenAPI file to a JS validation function for requests",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "bun test",
8+
"format": "prettier ./ --ignore-unknown --write"
9+
},
10+
"author": "",
11+
"license": "ISC",
12+
"dependencies": {
13+
"ast-types": "^0.14.2",
14+
"escodegen": "^2.1.0",
15+
"hash-object": "^0.1.7"
16+
},
17+
"devDependencies": {
18+
"@types/escodegen": "^0.0.9",
19+
"prettier": "^3.0.3"
20+
}
2021
}

src/compileOperation.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { namedTypes, builders } from 'ast-types';
2+
3+
import { Compiler } from "./compiler";
4+
import { OpenAPIOperation } from "./types";
5+
import { compileValueSchema } from './compileValueSchema';
6+
import { ValidationErrorIdentifier } from './error';
7+
8+
9+
/**
10+
* Compile an operation into a function.
11+
* The value input is an object:
12+
* {
13+
* path: string;
14+
* method: string;
15+
* body: any;
16+
* query: any;
17+
* headers: any;
18+
* }
19+
*/
20+
export function compileOperation(compiler: Compiler, operation: OpenAPIOperation) {
21+
return compiler.defineValidationFunction(operation, ({ value, path, error }) => {
22+
const nodes: namedTypes.BlockStatement['body'] = [];
23+
24+
if (operation.requestBody) {
25+
if (operation.requestBody.required) {
26+
nodes.push(builders.ifStatement(
27+
builders.binaryExpression(
28+
'===',
29+
builders.memberExpression(
30+
value,
31+
builders.identifier('body'),
32+
),
33+
builders.identifier('undefined'),
34+
),
35+
builders.blockStatement([
36+
builders.returnStatement(
37+
error('body is required')
38+
),
39+
]),
40+
));
41+
}
42+
43+
const contentTypeSchema = operation.requestBody.content?.['application/json']?.schema;
44+
if (contentTypeSchema) {
45+
const bodyFn = compileValueSchema(compiler, contentTypeSchema);
46+
const bodyResult = builders.identifier('body');
47+
48+
nodes.push(
49+
builders.variableDeclaration(
50+
'const',
51+
[
52+
builders.variableDeclarator(
53+
bodyResult,
54+
builders.callExpression(bodyFn, [
55+
builders.arrayExpression([
56+
builders.spreadElement(path),
57+
builders.literal('body'),
58+
]),
59+
builders.memberExpression(
60+
value,
61+
builders.identifier('body'),
62+
),
63+
]),
64+
)
65+
]
66+
)
67+
);
68+
69+
nodes.push(builders.ifStatement(
70+
builders.binaryExpression(
71+
'instanceof',
72+
bodyResult,
73+
ValidationErrorIdentifier,
74+
),
75+
builders.blockStatement([
76+
builders.returnStatement(bodyResult),
77+
]),
78+
builders.blockStatement([
79+
builders.expressionStatement(
80+
builders.assignmentExpression(
81+
'=',
82+
builders.memberExpression(
83+
value,
84+
builders.identifier('body'),
85+
),
86+
bodyResult,
87+
)
88+
)
89+
])
90+
));
91+
}
92+
93+
} else {
94+
nodes.push(builders.ifStatement(
95+
builders.binaryExpression(
96+
'!==',
97+
builders.memberExpression(
98+
value,
99+
builders.identifier('body'),
100+
),
101+
builders.identifier('undefined'),
102+
),
103+
builders.blockStatement([
104+
builders.returnStatement(
105+
error('body is not allowed')
106+
),
107+
]),
108+
));
109+
}
110+
111+
nodes.push(
112+
builders.returnStatement(
113+
value
114+
)
115+
)
116+
117+
118+
return nodes;
119+
});
120+
}

src/compiler.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,21 @@ import { namedTypes, builders } from 'ast-types';
33
import { ValidationErrorClass, ValidationErrorIdentifier } from './error';
44
import { OpenAPIRef, OpenAPISpec } from './types';
55
import { compileValueSchema } from './compileValueSchema';
6+
import { hash } from './hash';
67

78
/**
89
* Compiler for OpenAPI specs.
910
*/
1011
export class Compiler {
1112
private input: OpenAPISpec;
1213

14+
/** Map of hash to an object in the `identifiers` map */
15+
private hashes: Map<string, object> = new Map();
16+
17+
/** Map of objects from the spect to identifier name */
1318
private identifiers: WeakMap<object, string> = new WeakMap();
19+
20+
/** Counter to get a new identifier */
1421
private identifierCounter: number = 0;
1522

1623
private functions: Map<string, namedTypes.FunctionDeclaration | namedTypes.ClassDeclaration> =
@@ -108,6 +115,12 @@ export class Compiler {
108115
return this.identifiers.get(input)!;
109116
}
110117

118+
const hashValue = hash(input);
119+
if (this.hashes.has(hashValue)) {
120+
return this.identifiers.get(this.hashes.get(hashValue)!)!;
121+
}
122+
123+
this.hashes.set(hashValue, input);
111124
const name = `obj${this.identifierCounter++}`;
112125
this.identifiers.set(input, name);
113126
return name;

src/hash.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import hashObject from 'hash-object';
2+
3+
const PRESERVE_PROPS = [
4+
'$ref',
5+
'name',
6+
'in',
7+
'type',
8+
'required',
9+
'schema',
10+
'enum',
11+
'nullable',
12+
'minimum',
13+
'maximum',
14+
'allOf',
15+
'anyOf',
16+
'oneOf',
17+
'not',
18+
'items',
19+
'minItems',
20+
'maxItems',
21+
'format',
22+
'properties',
23+
'additionalProperties',
24+
'minProperties',
25+
'maxProperties',
26+
]
27+
28+
/**
29+
* Hash an object only taking the important properties into account.
30+
*/
31+
export function hash(input: object): string {
32+
// Remove all properties that are not important for the hash.
33+
const cleanInput = Object.keys(input).reduce((acc, key) => {
34+
if (PRESERVE_PROPS.includes(key)) {
35+
acc[key] = input[key];
36+
}
37+
38+
return acc;
39+
}, {} as any);
40+
41+
return hashObject(cleanInput);
42+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Bun Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`With body without 1`] = `
4+
"class ValidationError {
5+
constructor(path, message) {
6+
super(message);
7+
this.path = path;
8+
}
9+
}
10+
function obj0(path, value) {
11+
if (value.body !== undefined) {
12+
return new ValidationError(path, 'body is not allowed');
13+
}
14+
return value;
15+
}"
16+
`;
17+
18+
exports[`With body required 1`] = `
19+
"class ValidationError {
20+
constructor(path, message) {
21+
super(message);
22+
this.path = path;
23+
}
24+
}
25+
function obj2(path, value) {
26+
if (!(typeof value === 'number')) {
27+
return new ValidationError(path, 'Expected a number');
28+
}
29+
return value;
30+
}
31+
function obj1(path, value) {
32+
const keys = new Set(Object.keys(value));
33+
const value0 = value['foo'];
34+
if (value0 !== undefined) {
35+
const result0 = obj2([
36+
...path,
37+
'foo'
38+
], value0);
39+
if (result0 instanceof ValidationError) {
40+
return result0;
41+
}
42+
value['foo'] = result0;
43+
}
44+
if (keys.size > 0) {
45+
return new ValidationError(path, 'Unexpected properties');
46+
}
47+
return value;
48+
}
49+
function obj0(path, value) {
50+
if (value.body === undefined) {
51+
return new ValidationError(path, 'body is required');
52+
}
53+
const body = obj1([
54+
...path,
55+
'body'
56+
], value.body);
57+
if (body instanceof ValidationError) {
58+
return body;
59+
} else {
60+
value.body = body;
61+
}
62+
return value;
63+
}"
64+
`;

src/tests/compileOperation.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Compiler } from '../compiler';
2+
import { compileOperation } from '../compileOperation';
3+
4+
describe('With body', () => {
5+
test('without', () => {
6+
const compiler = new Compiler();
7+
compileOperation(compiler, {});
8+
expect(compiler.compile()).toMatchSnapshot();
9+
});
10+
11+
test('required', () => {
12+
const compiler = new Compiler();
13+
compileOperation(compiler, {
14+
requestBody: {
15+
required: true,
16+
content: {
17+
'application/json': {
18+
schema: {
19+
type: 'object',
20+
properties: {
21+
foo: {
22+
type: 'number',
23+
},
24+
},
25+
},
26+
},
27+
},
28+
}
29+
});
30+
console.log(compiler.compile());
31+
expect(compiler.compile()).toMatchSnapshot();
32+
});
33+
});

src/types.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,28 @@ export interface OpenAPISpec {
77
[key: string]: OpenAPIValueSchema;
88
};
99
};
10+
paths?: {
11+
[key: string]: OpenAPIPath;
12+
};
13+
}
14+
15+
export interface OpenAPIPath {
16+
[httpMethod: string]: OpenAPIOperation;
17+
}
18+
19+
export interface OpenAPIOperation {
20+
operationId?: string;
21+
parameters?: OpenAPIParameter[];
22+
requestBody?: OpenAPIRequestBody;
23+
}
24+
25+
export interface OpenAPIRequestBody {
26+
required?: boolean;
27+
content?: {
28+
[contentType: string]: {
29+
schema?: OpenAPIValueSchema;
30+
};
31+
};
1032
}
1133

1234
export interface OpenAPIParameter {

0 commit comments

Comments
 (0)