Skip to content

Commit c4cfcdc

Browse files
committed
Start compiling entire schemas and resolve refs
1 parent 81f99c3 commit c4cfcdc

11 files changed

+223
-77
lines changed

bun.lockb

-779 Bytes
Binary file not shown.

package.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,10 @@
1111
"license": "ISC",
1212
"dependencies": {
1313
"ast-types": "^0.14.2",
14-
"escodegen": "^2.1.0",
15-
"esprima": "^4.0.1"
14+
"escodegen": "^2.1.0"
1615
},
1716
"devDependencies": {
1817
"@types/escodegen": "^0.0.9",
19-
"@types/esprima": "^4.0.5",
20-
"@types/estree": "^1.0.3",
2118
"prettier": "^3.0.3"
2219
}
2320
}

src/compileSpec.ts

Lines changed: 0 additions & 4 deletions
This file was deleted.

src/compileValueSchema.ts

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { namedTypes, builders } from 'ast-types';
22

3-
import { Compiler, ValidationErrorIdentifier } from './compiler';
3+
import type { Compiler } from './compiler';
44
import {
55
OpenAPIAnyOfSchema,
66
OpenAPIArraySchema,
@@ -12,11 +12,16 @@ import {
1212
OpenAPIStringSchema,
1313
OpenAPIValueSchema,
1414
} from './types';
15+
import { ValidationErrorIdentifier } from './error';
1516

1617
/**
1718
* Compile a JSON schema into a validation function.
1819
*/
1920
export function compileValueSchema(compiler: Compiler, schema: OpenAPIValueSchema) {
21+
if ('$ref' in schema) {
22+
return compileValueSchema(compiler, compiler.resolveRef(schema));
23+
}
24+
2025
if ('anyOf' in schema) {
2126
return compileAnyOfSchema(compiler, schema);
2227
}
@@ -314,7 +319,6 @@ function compileArraySchema(compiler: Compiler, schema: OpenAPIArraySchema) {
314319
const nodes: namedTypes.BlockStatement['body'] = [];
315320

316321
nodes.push(...compileNullableCheck(compiler, schema, value));
317-
318322

319323
nodes.push(builders.returnStatement(value));
320324

@@ -358,7 +362,6 @@ function compileStringSchema(compiler: Compiler, schema: OpenAPIStringSchema) {
358362
return enumCheck;
359363
}
360364

361-
362365
const nodes: namedTypes.BlockStatement['body'] = [];
363366
nodes.push(...compileNullableCheck(compiler, schema, value));
364367
nodes.push(
@@ -368,10 +371,10 @@ function compileStringSchema(compiler: Compiler, schema: OpenAPIStringSchema) {
368371
builders.binaryExpression(
369372
'===',
370373
builders.unaryExpression('typeof', value),
371-
builders.literal('number'),
374+
builders.literal('string'),
372375
),
373376
),
374-
builders.blockStatement([builders.returnStatement(error('Expected a number'))]),
377+
builders.blockStatement([builders.returnStatement(error('Expected a string'))]),
375378
),
376379
);
377380

@@ -410,7 +413,11 @@ function compileBooleanSchema(compiler: Compiler, schema: OpenAPIBooleanSchema)
410413
});
411414
}
412415

413-
function compileNullableCheck(compiler: Compiler, schema: OpenAPINullableSchema, value: namedTypes.Identifier) {
416+
function compileNullableCheck(
417+
compiler: Compiler,
418+
schema: OpenAPINullableSchema,
419+
value: namedTypes.Identifier,
420+
) {
414421
if (!schema.nullable) {
415422
return [];
416423
}
@@ -419,35 +426,38 @@ function compileNullableCheck(compiler: Compiler, schema: OpenAPINullableSchema,
419426
builders.ifStatement(
420427
builders.binaryExpression('===', value, builders.identifier('null')),
421428
builders.blockStatement([builders.returnStatement(value)]),
422-
)
423-
]
429+
),
430+
];
424431
}
425432

426-
427-
function compileEnumableCheck(compiler: Compiler, schema: OpenAPIEnumableSchema, value: namedTypes.Identifier, error: (message: string) => namedTypes.NewExpression) {
433+
function compileEnumableCheck(
434+
compiler: Compiler,
435+
schema: OpenAPIEnumableSchema,
436+
value: namedTypes.Identifier,
437+
error: (message: string) => namedTypes.NewExpression,
438+
) {
428439
if (!schema.enum) {
429440
return null;
430441
}
431442

432443
return [
433444
builders.ifStatement(
434-
schema.enum.reduce((acc, val) => {
435-
const test = builders.binaryExpression('!==', value, builders.literal(val))
436-
437-
if (!acc) {
438-
return test;
439-
}
440-
441-
return builders.logicalExpression(
442-
'&&',
443-
acc,
444-
test
445-
)
446-
}, null as (namedTypes.BinaryExpression | namedTypes.LogicalExpression | null))!,
447-
builders.blockStatement([builders.returnStatement(
448-
error('Expected one of the enum value')
449-
)]),
445+
schema.enum.reduce(
446+
(acc, val) => {
447+
const test = builders.binaryExpression('!==', value, builders.literal(val));
448+
449+
if (!acc) {
450+
return test;
451+
}
452+
453+
return builders.logicalExpression('&&', acc, test);
454+
},
455+
null as namedTypes.BinaryExpression | namedTypes.LogicalExpression | null,
456+
)!,
457+
builders.blockStatement([
458+
builders.returnStatement(error('Expected one of the enum value')),
459+
]),
450460
),
451-
builders.returnStatement(value)
452-
]
453-
}
461+
builders.returnStatement(value),
462+
];
463+
}

src/compiler.ts

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,23 @@
11
import escodegen from 'escodegen';
2-
import esprima from 'esprima';
32
import { namedTypes, builders } from 'ast-types';
3+
import { ValidationErrorClass, ValidationErrorIdentifier } from './error';
4+
import { OpenAPIRef, OpenAPISpec } from './types';
5+
import { compileValueSchema } from './compileValueSchema';
46

5-
export const ValidationErrorIdentifier: namedTypes.Identifier = {
6-
type: 'Identifier',
7-
name: 'ValidationError',
8-
};
9-
10-
const ValidationErrorClass = esprima.parseScript(`
11-
class ValidationError extends Error {
12-
constructor(path, message) {
13-
super(message);
14-
this.path = path;
15-
}
16-
}
17-
`).body[0] as namedTypes.ClassDeclaration;
18-
7+
/**
8+
* Compiler for OpenAPI specs.
9+
*/
1910
export class Compiler {
11+
private input: OpenAPISpec;
12+
2013
private identifiers: WeakMap<object, string> = new WeakMap();
2114
private identifierCounter: number = 0;
2215

2316
private functions: Map<string, namedTypes.FunctionDeclaration | namedTypes.ClassDeclaration> =
24-
new Map();
17+
new Map([[ValidationErrorIdentifier.name, ValidationErrorClass]]);
2518

26-
constructor() {
27-
this.functions.set(ValidationErrorIdentifier.name, ValidationErrorClass);
19+
constructor(input: OpenAPISpec = {}) {
20+
this.input = input;
2821
}
2922

3023
/**
@@ -82,10 +75,27 @@ export class Compiler {
8275
});
8376
}
8477

78+
/**
79+
* Build the AST from the entire spec.
80+
*/
81+
public build() {
82+
// Index all the schema components.
83+
const schemas = this.input.components?.schemas ?? {};
84+
Object.values(schemas).forEach((schema) => {
85+
compileValueSchema(this, schema);
86+
});
87+
}
88+
89+
/**
90+
* Return the AST for the program.
91+
*/
8592
public ast() {
8693
return builders.program([...this.functions.values()]);
8794
}
8895

96+
/**
97+
* Generate the JS code for the AST.
98+
*/
8999
public compile() {
90100
return escodegen.generate(this.ast());
91101
}
@@ -102,4 +112,22 @@ export class Compiler {
102112
this.identifiers.set(input, name);
103113
return name;
104114
}
115+
116+
/**
117+
* Resolve a reference to part of the spec.
118+
* We only support "#/" type of references.
119+
*/
120+
public resolveRef(ref: OpenAPIRef) {
121+
const parts = ref.$ref.split('/').slice(1);
122+
123+
let value: any = this.input;
124+
for (const part of parts) {
125+
value = value[part];
126+
if (value === undefined) {
127+
throw new Error(`Could not resolve reference ${ref.$ref}`);
128+
}
129+
}
130+
131+
return value;
132+
}
105133
}

src/error.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { namedTypes, builders } from 'ast-types';
2+
3+
export const ValidationErrorIdentifier: namedTypes.Identifier = {
4+
type: 'Identifier',
5+
name: 'ValidationError',
6+
};
7+
8+
export const ValidationErrorClass = builders.classDeclaration(
9+
builders.identifier('ValidationError'),
10+
builders.classBody([
11+
builders.methodDefinition(
12+
'constructor',
13+
builders.identifier('constructor'),
14+
builders.functionExpression(
15+
null,
16+
[builders.identifier('path'), builders.identifier('message')],
17+
builders.blockStatement([
18+
builders.expressionStatement(
19+
builders.callExpression(builders.super(), [builders.identifier('message')]),
20+
),
21+
builders.expressionStatement(
22+
builders.assignmentExpression(
23+
'=',
24+
builders.memberExpression(
25+
builders.thisExpression(),
26+
builders.identifier('path'),
27+
),
28+
builders.identifier('path'),
29+
),
30+
),
31+
]),
32+
),
33+
),
34+
]),
35+
);

0 commit comments

Comments
 (0)