Skip to content

Commit c5bf125

Browse files
committed
Validate minLength/maxLength/pattern on string
1 parent f02085c commit c5bf125

File tree

10 files changed

+172
-48
lines changed

10 files changed

+172
-48
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
11
# `openapi-to-validate`
22

33
CLI to compile an OpenAPI specification as JavaScript validation file, optimized for performances.
4+
5+
## TODOs
6+
7+
- Path/Operations
8+
- [ ] Validate parameters
9+
- JSONSchema `string` validation of:
10+
- [x] `minLength`
11+
- [x] `maxLength`
12+
- [ ] `format`
13+
- [x] `pattern`
14+
- JSONSchema `array` validation of:
15+
- [ ] `minItems`
16+
- [ ] `maxItems`
17+
- [ ] `uniqueItems`
18+
- JSONSchema `integer`
19+
- [ ] no float

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "openapi-to-validate",
33
"version": "0.0.0",
4-
"description": "Compile an OpenAPI file to a JS validation function for requests",
4+
"description": "Statically compile an OpenAPI spec into a JS validation function",
55
"main": "index.js",
66
"type": "module",
77
"scripts": {

src/compileOperation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { ValidationErrorIdentifier } from './error';
1717
* }
1818
*/
1919
export function compileOperation(compiler: Compiler, operation: OpenAPIOperation) {
20-
return compiler.defineValidationFunction(operation, ({ value, path, error }) => {
20+
return compiler.declareValidationFunction(operation, ({ value, path, error }) => {
2121
const nodes: namedTypes.BlockStatement['body'] = [];
2222

2323
if (operation.operationId) {

src/compileParameter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { compileValueSchema } from './compileValueSchema';
1717
* }
1818
*/
1919
export function compileParameter(compiler: Compiler, parameter: OpenAPIParameter) {
20-
return compiler.defineValidationFunction(parameter, ({ value, path, error }) => {
20+
return compiler.declareValidationFunction(parameter, ({ value, path, error }) => {
2121
const nodes: namedTypes.BlockStatement['body'] = [];
2222

2323
const paramValue = builders.memberExpression(

src/compilePath.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { compileOperation } from './compileOperation';
1616
* }
1717
*/
1818
export function compilePath(compiler: Compiler, pathOperations: OpenAPIPath) {
19-
return compiler.defineValidationFunction(pathOperations, ({ value, path, error }) => {
19+
return compiler.declareValidationFunction(pathOperations, ({ value, path, error }) => {
2020
const nodes: namedTypes.BlockStatement['body'] = [];
2121

2222
Object.entries(pathOperations).forEach(([method, operation]) => {

src/compileValueSchema.ts

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export function compileValueSchema(compiler: Compiler, schema: OpenAPIValueSchem
5959
}
6060

6161
function compileAnyOfSchema(compiler: Compiler, schema: OpenAPIAnyOfSchema) {
62-
return compiler.defineValidationFunction(schema, ({ value, path, error }) => {
62+
return compiler.declareValidationFunction(schema, ({ value, path, error }) => {
6363
const nodes: namedTypes.BlockStatement['body'] = [];
6464

6565
schema.anyOf.forEach((subSchema, index) => {
@@ -100,7 +100,7 @@ function compileAnyOfSchema(compiler: Compiler, schema: OpenAPIAnyOfSchema) {
100100
}
101101

102102
function compileOneOfSchema(compiler: Compiler, schema: OpenAPIOneOfSchema) {
103-
return compiler.defineValidationFunction(schema, ({ value, path, error }) => {
103+
return compiler.declareValidationFunction(schema, ({ value, path, error }) => {
104104
const nodes: namedTypes.BlockStatement['body'] = [];
105105

106106
// Declare the variable to use as a result, then iterate over each schema
@@ -165,7 +165,7 @@ function compileOneOfSchema(compiler: Compiler, schema: OpenAPIOneOfSchema) {
165165
}
166166

167167
function compileAllOfSchema(compiler: Compiler, schema: OpenAPIAllOfSchema) {
168-
return compiler.defineValidationFunction(schema, ({ value, path, error }) => {
168+
return compiler.declareValidationFunction(schema, ({ value, path, error }) => {
169169
const nodes: namedTypes.BlockStatement['body'] = [];
170170

171171
const resultIdentifier = builders.identifier('result');
@@ -207,7 +207,7 @@ function compileAllOfSchema(compiler: Compiler, schema: OpenAPIAllOfSchema) {
207207
}
208208

209209
function compileObjectSchema(compiler: Compiler, schema: OpenAPIObjectSchema) {
210-
return compiler.defineValidationFunction(schema, ({ path, value, error }) => {
210+
return compiler.declareValidationFunction(schema, ({ path, value, error }) => {
211211
const nodes: namedTypes.BlockStatement['body'] = [];
212212
const endNodes: namedTypes.BlockStatement['body'] = [];
213213

@@ -436,7 +436,7 @@ function compileObjectSchema(compiler: Compiler, schema: OpenAPIObjectSchema) {
436436
}
437437

438438
function compileArraySchema(compiler: Compiler, schema: OpenAPIArraySchema) {
439-
return compiler.defineValidationFunction(schema, ({ value, error }) => {
439+
return compiler.declareValidationFunction(schema, ({ value, error }) => {
440440
const nodes: namedTypes.BlockStatement['body'] = [];
441441

442442
nodes.push(...compileNullableCheck(compiler, schema, value));
@@ -451,7 +451,7 @@ function compileNumberSchema(
451451
compiler: Compiler,
452452
schema: OpenAPINumberSchema | OpenAPIIntegerSchema,
453453
) {
454-
return compiler.defineValidationFunction(schema, ({ value, error }) => {
454+
return compiler.declareValidationFunction(schema, ({ value, error }) => {
455455
const enumCheck = compileEnumableCheck(compiler, schema, value, error);
456456
if (enumCheck) {
457457
return enumCheck;
@@ -480,7 +480,7 @@ function compileNumberSchema(
480480
}
481481

482482
function compileStringSchema(compiler: Compiler, schema: OpenAPIStringSchema) {
483-
return compiler.defineValidationFunction(schema, ({ value, error }) => {
483+
return compiler.declareValidationFunction(schema, ({ value, error }) => {
484484
const enumCheck = compileEnumableCheck(compiler, schema, value, error);
485485
if (enumCheck) {
486486
return enumCheck;
@@ -502,14 +502,79 @@ function compileStringSchema(compiler: Compiler, schema: OpenAPIStringSchema) {
502502
),
503503
);
504504

505+
if (schema.minLength) {
506+
nodes.push(
507+
builders.ifStatement(
508+
builders.binaryExpression(
509+
'<',
510+
builders.memberExpression(value, builders.identifier('length')),
511+
builders.literal(schema.minLength),
512+
),
513+
builders.blockStatement([
514+
builders.returnStatement(
515+
error(`Expected at least ${schema.minLength} characters`),
516+
),
517+
]),
518+
),
519+
);
520+
}
521+
522+
if (schema.maxLength) {
523+
nodes.push(
524+
builders.ifStatement(
525+
builders.binaryExpression(
526+
'>',
527+
builders.memberExpression(value, builders.identifier('length')),
528+
builders.literal(schema.maxLength),
529+
),
530+
builders.blockStatement([
531+
builders.returnStatement(
532+
error(`Expected at most ${schema.maxLength} characters`),
533+
),
534+
]),
535+
),
536+
);
537+
}
538+
539+
if (schema.pattern) {
540+
const patternStr = schema.pattern;
541+
const patternRegexp = compiler.declareForInput(patternStr, (id) => {
542+
return builders.variableDeclaration('const', [
543+
builders.variableDeclarator(
544+
id,
545+
builders.newExpression(builders.identifier('RegExp'), [
546+
builders.literal(patternStr),
547+
]),
548+
),
549+
]);
550+
});
551+
552+
nodes.push(
553+
builders.ifStatement(
554+
builders.unaryExpression(
555+
'!',
556+
builders.callExpression(
557+
builders.memberExpression(patternRegexp, builders.identifier('test')),
558+
[value],
559+
),
560+
),
561+
builders.blockStatement([
562+
builders.returnStatement(
563+
error(`Expected to match the pattern "${schema.pattern}"`),
564+
),
565+
]),
566+
),
567+
);
568+
}
569+
505570
nodes.push(builders.returnStatement(value));
506571

507572
return nodes;
508573
});
509574
}
510575

511576
function compileBooleanSchema(compiler: Compiler, schema: OpenAPIBooleanSchema) {
512-
return compiler.defineValidationFunction(schema, ({ value, error }) => {
577+
return compiler.declareValidationFunction(schema, ({ value, error }) => {
513578
const enumCheck = compileEnumableCheck(compiler, schema, value, error);
514579
if (enumCheck) {
515580
return enumCheck;
@@ -538,7 +603,7 @@ function compileBooleanSchema(compiler: Compiler, schema: OpenAPIBooleanSchema)
538603
}
539604

540605
function compileAnySchema(compiler: Compiler, schema: object) {
541-
return compiler.defineValidationFunction(schema, ({ value }) => {
606+
return compiler.declareValidationFunction(schema, ({ value }) => {
542607
return [builders.returnStatement(value)];
543608
});
544609
}

src/compiler.ts

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,42 +12,55 @@ import { compileValidateRequest } from './compileValidateRequest';
1212
export class Compiler {
1313
private input: OpenAPISpec;
1414

15-
/** Map of hash to an object in the `identifiers` map */
16-
private hashes: Map<string, object> = new Map();
17-
18-
/** Map of objects from the spect to identifier name */
19-
private identifiers: WeakMap<object, string> = new WeakMap();
15+
/** Map of hash to the identifier */
16+
private hashes: Map<string, string> = new Map();
17+
/** Map of objects to the identifier */
18+
private objectHashes: WeakMap<object, string> = new WeakMap();
2019

2120
/** Counter to get a new identifier */
2221
private identifierCounter: number = 0;
2322

24-
private functions: Map<string, namedTypes.FunctionDeclaration | namedTypes.ClassDeclaration> =
25-
new Map([[ValidationErrorIdentifier.name, ValidationErrorClass]]);
23+
/** Map of identifiers defined globally */
24+
private globalDeclarations: (namedTypes.FunctionDeclaration | namedTypes.ClassDeclaration | namedTypes.VariableDeclaration)[] = [ValidationErrorClass];
25+
26+
/** Map of hashes already processed */
27+
private processedHashes: Set<string> = new Set();
2628

2729
constructor(input: OpenAPISpec = {}) {
2830
this.input = input;
2931
}
3032

3133
/**
32-
* Define a function generated from an object.
34+
* Define a global identifier with a nickname.
3335
*/
34-
public defineFunction(
35-
input: object,
36-
gen: (id: string) => namedTypes.FunctionDeclaration,
36+
public declareGlobally(declaration: namedTypes.FunctionDeclaration | namedTypes.ClassDeclaration | namedTypes.VariableDeclaration) {
37+
this.globalDeclarations.push(declaration);
38+
}
39+
40+
/**
41+
* Declare something globally basded on an input and return an identifier.
42+
*/
43+
public declareForInput(
44+
input: any,
45+
gen: (id: namedTypes.Identifier) => namedTypes.FunctionDeclaration | namedTypes.ClassDeclaration | namedTypes.VariableDeclaration,
3746
): namedTypes.Identifier {
3847
const hash = this.hashObject(input);
39-
if (!this.functions.has(hash)) {
40-
const fn = gen(hash);
41-
this.functions.set(hash, fn);
48+
const identifier = builders.identifier(hash);
49+
50+
if (!this.processedHashes.has(hash)) {
51+
const fn = gen(identifier);
52+
this.declareGlobally(fn);
53+
54+
this.processedHashes.add(hash);
4255
}
4356

44-
return builders.identifier(hash);
57+
return identifier;
4558
}
4659

4760
/**
4861
* Define a function to validate an input.
4962
*/
50-
public defineValidationFunction(
63+
public declareValidationFunction(
5164
input: object,
5265
gen: (args: {
5366
/** Identifier for the value argument being passed to the function */
@@ -68,9 +81,9 @@ export class Compiler {
6881
]);
6982
};
7083

71-
return this.defineFunction(input, (id) => {
84+
return this.declareForInput(input, (id) => {
7285
return builders.functionDeclaration(
73-
builders.identifier(id),
86+
id,
7487
[pathIdentifier, valueIdentifier],
7588
builders.blockStatement(
7689
gen({
@@ -100,7 +113,7 @@ export class Compiler {
100113
public ast() {
101114
return builders.program([
102115
...compileValidateRequest(this, this.input),
103-
...this.functions.values(),
116+
...this.globalDeclarations,
104117
]);
105118
}
106119

@@ -114,19 +127,25 @@ export class Compiler {
114127
/**
115128
* Hash an object and return an identifier name.
116129
*/
117-
public hashObject(input: object): string {
118-
if (this.identifiers.has(input)) {
119-
return this.identifiers.get(input)!;
130+
public hashObject(input: any): string {
131+
const isObject = typeof input === 'object' && input !== null;
132+
133+
// Fast track for objects
134+
if (isObject && this.objectHashes.has(input)) {
135+
return this.objectHashes.get(input)!;
120136
}
121137

122138
const hashValue = hash(input);
123139
if (this.hashes.has(hashValue)) {
124-
return this.identifiers.get(this.hashes.get(hashValue)!)!;
140+
return this.hashes.get(hashValue)!;
125141
}
126142

127-
this.hashes.set(hashValue, input);
128143
const name = `obj${this.identifierCounter++}`;
129-
this.identifiers.set(input, name);
144+
this.hashes.set(hashValue, name);
145+
if (isObject) {
146+
this.objectHashes.set(input, name);
147+
}
148+
130149
return name;
131150
}
132151

src/hash.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ const PRESERVE_PROPS = [
1818
'items',
1919
'minItems',
2020
'maxItems',
21+
'minLength',
22+
'maxLength',
23+
'pattern',
2124
'format',
2225
'properties',
2326
'additionalProperties',
@@ -35,17 +38,26 @@ const PRESERVE_PROPS = [
3538
];
3639

3740
/**
38-
* Hash an object only taking the important properties into account.
41+
* Normalize the input value as an object.
3942
*/
40-
export function hash(input: object): string {
41-
// Remove all properties that are not important for the hash.
42-
const cleanInput = Object.keys(input).reduce((acc, key) => {
43-
if (PRESERVE_PROPS.includes(key)) {
44-
acc[key] = input[key];
45-
}
43+
function normalizeHashInput(input: any): object {
44+
if (typeof input !== 'object' || input === null) {
45+
// Remove all properties that are not important for the hash.
46+
input = Object.keys(input).reduce((acc, key) => {
47+
if (PRESERVE_PROPS.includes(key)) {
48+
acc[key] = input[key];
49+
}
50+
51+
return acc;
52+
}, {} as any);
53+
}
4654

47-
return acc;
48-
}, {} as any);
55+
return { input };
56+
}
4957

50-
return hashObject(cleanInput);
58+
/**
59+
* Hash an object only taking the important properties into account.
60+
*/
61+
export function hash(input: any): string {
62+
return hashObject(normalizeHashInput(input));
5163
}

src/tests/compileValueSchema.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ describe('String', () => {
4242
});
4343
expect(compiler.compile()).toMatchSnapshot();
4444
});
45+
46+
test('with pattern', () => {
47+
const compiler = new Compiler();
48+
compileValueSchema(compiler, {
49+
type: 'string',
50+
pattern: '^[a-z]+$',
51+
});
52+
expect(compiler.compile()).toMatchSnapshot();
53+
});
4554
});
4655

4756
describe('Objects', () => {

0 commit comments

Comments
 (0)