diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..eb906dc --- /dev/null +++ b/bun.lock @@ -0,0 +1,51 @@ +{ + "lockfileVersion": 1, + "configVersion": 0, + "workspaces": { + "": { + "name": "openapi-to-validate", + "dependencies": { + "ast-types": "^0.14.2", + "escodegen": "^2.1.0", + "object-hash": "^3.0.0", + "path-to-regexp": "^6.2.1", + }, + "devDependencies": { + "@types/escodegen": "^0.0.9", + "@types/object-hash": "^3.0.5", + "bun-types": "^1.0.6", + "prettier": "^3.0.3", + "typescript": "^5.2.2", + }, + }, + }, + "packages": { + "@types/escodegen": ["@types/escodegen@0.0.9", "", {}, "sha512-8HOVgEhBwGgKWvcdu17wkFgv8Br2UgL0bw4UM11iVhKTnO3DX415PBmyxBYvVRejqyVVZF2HRfuFKCc5arC1FQ=="], + + "@types/object-hash": ["@types/object-hash@3.0.5", "", {}, "sha512-WFGeSazfL5BWbEh5ACaAIs5RT6sbVIwBs1rgHUp+kZzX/gub41LEEYWTWbYnE/sKb7hDdPEsGa1Vmcaay2fS5g=="], + + "ast-types": ["ast-types@0.14.2", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA=="], + + "bun-types": ["bun-types@1.0.6", "", {}, "sha512-5QwynfXiRCRxPW3ZnC0Dv+sHHmctP4SHIuzsRKOWYO0HF/qUpsxQVexoviaxpmwDsF1hoVDDFdc4xUuafOzx1g=="], + + "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + + "path-to-regexp": ["path-to-regexp@6.2.1", "", {}, "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw=="], + + "prettier": ["prettier@3.0.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="], + + "typescript": ["typescript@5.2.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w=="], + } +} diff --git a/bun.lockb b/bun.lockb deleted file mode 100755 index 20d7ad4..0000000 Binary files a/bun.lockb and /dev/null differ diff --git a/src/compileValueSchema.ts b/src/compileValueSchema.ts index 517c0e7..2dfa09f 100644 --- a/src/compileValueSchema.ts +++ b/src/compileValueSchema.ts @@ -12,6 +12,7 @@ import { OpenAPINumberSchema, OpenAPIObjectSchema, OpenAPIOneOfSchema, + OpenAPIPropertyNamesSchema, OpenAPIStringSchema, OpenAPIValueSchema, } from './types'; @@ -71,6 +72,17 @@ export function compileValueSchema(compiler: Compiler, schema: OpenAPIValueSchem return compileAnySchema(compiler, schema); } +function normalizePropertyNamesSchema(schema: OpenAPIPropertyNamesSchema): OpenAPIValueSchema { + if ('type' in schema || 'anyOf' in schema || 'oneOf' in schema || 'allOf' in schema) { + return schema; + } + + return { + type: 'string', + ...schema, + }; +} + function compileAnyOfSchema(compiler: Compiler, schema: OpenAPIAnyOfSchema) { return compiler.declareValidationFunction(schema, ({ value, path, context, error }) => { const nodes: namedTypes.BlockStatement['body'] = []; @@ -337,6 +349,49 @@ function compileObjectSchema(compiler: Compiler, schema: OpenAPIObjectSchema) { ]), ); + if (schema.propertyNames) { + const keyIdentifier = builders.identifier('key'); + const keyResultIdentifier = builders.identifier('keyResult'); + const propertyNamesSchemaIdentifier = compileValueSchema( + compiler, + normalizePropertyNamesSchema(schema.propertyNames), + ); + + nodes.push( + builders.forOfStatement( + builders.variableDeclaration('const', [ + builders.variableDeclarator(keyIdentifier), + ]), + keysIdentifier, + builders.blockStatement([ + builders.variableDeclaration('const', [ + builders.variableDeclarator( + keyResultIdentifier, + builders.callExpression(propertyNamesSchemaIdentifier, [ + builders.arrayExpression([ + builders.spreadElement(path), + keyIdentifier, + ]), + keyIdentifier, + context, + ]), + ), + ]), + builders.ifStatement( + builders.binaryExpression( + 'instanceof', + keyResultIdentifier, + ValidationErrorIdentifier, + ), + builders.blockStatement([ + builders.returnStatement(keyResultIdentifier), + ]), + ), + ]), + ), + ); + } + if (schema.minProperties) { nodes.push( builders.ifStatement( diff --git a/src/hash.ts b/src/hash.ts index b7b3be1..8b8fec1 100644 --- a/src/hash.ts +++ b/src/hash.ts @@ -23,6 +23,7 @@ const PRESERVE_PROPS = [ 'pattern', 'format', 'properties', + 'propertyNames', 'additionalProperties', 'minProperties', 'maxProperties', diff --git a/src/tests/__snapshots__/compileValueSchema.test.ts.snap b/src/tests/__snapshots__/compileValueSchema.test.ts.snap index ce1391d..f831ce6 100644 --- a/src/tests/__snapshots__/compileValueSchema.test.ts.snap +++ b/src/tests/__snapshots__/compileValueSchema.test.ts.snap @@ -1,4 +1,4 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots exports[`Number basic 1`] = ` "/** @@ -232,7 +232,7 @@ function obj0(path, value, context) { }" `; -exports[`Integer basic 1`] = ` +exports[`Number negative maximum 1`] = ` "/** Validate a request against the OpenAPI spec @param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate @@ -272,11 +272,14 @@ function obj0(path, value, context) { if (typeof value !== 'number' || Number.isNaN(value)) { return new ValidationError(path, 'expected a number'); } + if (value > -5) { + return new ValidationError(path, 'value greater than maximum'); + } return value; }" `; -exports[`Nullable nullable: true 1`] = ` +exports[`Number negative minimum 1`] = ` "/** Validate a request against the OpenAPI spec @param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate @@ -310,20 +313,20 @@ export class ValidationError extends RequestError { } } function obj0(path, value, context) { - if (value === null) { - return value; - } if (typeof value === 'string') { value = Number(value); } if (typeof value !== 'number' || Number.isNaN(value)) { return new ValidationError(path, 'expected a number'); } + if (value < -10) { + return new ValidationError(path, 'value less than minimum'); + } return value; }" `; -exports[`String with enum 1`] = ` +exports[`Number negative maximum exclusiveMaximum 1`] = ` "/** Validate a request against the OpenAPI spec @param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate @@ -357,14 +360,20 @@ export class ValidationError extends RequestError { } } function obj0(path, value, context) { - if (value !== 'a' && value !== 'b' && value !== 'c') { - return new ValidationError(path, 'expected one of the enum value'); + if (typeof value === 'string') { + value = Number(value); + } + if (typeof value !== 'number' || Number.isNaN(value)) { + return new ValidationError(path, 'expected a number'); + } + if (value >= -5) { + return new ValidationError(path, 'value greater than maximum'); } return value; }" `; -exports[`String with pattern 1`] = ` +exports[`Number negative minimum exclusiveMinimum 1`] = ` "/** Validate a request against the OpenAPI spec @param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate @@ -397,19 +406,21 @@ export class ValidationError extends RequestError { this.path = path; } } -const obj1 = new RegExp('^[a-z]+$'); function obj0(path, value, context) { - if (typeof value !== 'string') { - return new ValidationError(path, 'expected a string'); + if (typeof value === 'string') { + value = Number(value); } - if (!obj1.test(value)) { - return new ValidationError(path, 'expected to match the pattern "^[a-z]+$"'); + if (typeof value !== 'number' || Number.isNaN(value)) { + return new ValidationError(path, 'expected a number'); + } + if (value <= -10) { + return new ValidationError(path, 'value less than minimum'); } return value; }" `; -exports[`String with pattern 2`] = ` +exports[`Integer basic 1`] = ` "/** Validate a request against the OpenAPI spec @param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate @@ -443,21 +454,17 @@ export class ValidationError extends RequestError { } } function obj0(path, value, context) { - if (typeof value !== 'string') { - return new ValidationError(path, 'expected a string'); - } - const formatResult = context?.stringFormats?.['uri']?.(value, path); - if (formatResult instanceof ValidationError) { - return formatResult; + if (typeof value === 'string') { + value = Number(value); } - if (typeof formatResult === 'string') { - value = formatResult; + if (typeof value !== 'number' || Number.isNaN(value)) { + return new ValidationError(path, 'expected a number'); } return value; }" `; -exports[`String with format 1`] = ` +exports[`Integer negative 1`] = ` "/** Validate a request against the OpenAPI spec @param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate @@ -491,21 +498,14 @@ export class ValidationError extends RequestError { } } function obj0(path, value, context) { - if (typeof value !== 'string') { - return new ValidationError(path, 'expected a string'); - } - const formatResult = context?.stringFormats?.['uri']?.(value, path); - if (formatResult instanceof ValidationError) { - return formatResult; - } - if (typeof formatResult === 'string') { - value = formatResult; + if (value !== -1 && value !== 0 && value !== 1) { + return new ValidationError(path, 'expected one of the enum value'); } return value; }" `; -exports[`Objects with a required prop 1`] = ` +exports[`Integer negative maximum 1`] = ` "/** Validate a request against the OpenAPI spec @param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate @@ -538,57 +538,21 @@ export class ValidationError extends RequestError { this.path = path; } } -function obj1(path, value, context) { +function obj0(path, value, context) { if (typeof value === 'string') { value = Number(value); } if (typeof value !== 'number' || Number.isNaN(value)) { return new ValidationError(path, 'expected a number'); } - return value; -} -function obj2(path, value, context) { - if (typeof value !== 'string') { - return new ValidationError(path, 'expected a string'); - } - return value; -} -function obj0(path, value, context) { - if (typeof value !== 'object' || value === null) { - return new ValidationError(path, 'expected an object'); - } - const keys = new Set(Object.keys(value)); - const value0 = value['foo']; - if (value0 !== undefined) { - const result0 = obj1([ - ...path, - 'foo' - ], value0, context); - if (result0 instanceof ValidationError) { - return result0; - } - value['foo'] = result0; - keys.delete('foo'); - } else { - return new ValidationError(path, 'expected "foo" to be defined'); - } - const value1 = value['bar']; - if (value1 !== undefined) { - const result1 = obj2([ - ...path, - 'bar' - ], value1, context); - if (result1 instanceof ValidationError) { - return result1; - } - value['bar'] = result1; - keys.delete('bar'); + if (value > -5) { + return new ValidationError(path, 'value greater than maximum'); } return value; }" `; -exports[`Objects with a default value 1`] = ` +exports[`Integer negative minimum 1`] = ` "/** Validate a request against the OpenAPI spec @param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate @@ -621,39 +585,21 @@ export class ValidationError extends RequestError { this.path = path; } } -function obj1(path, value, context) { +function obj0(path, value, context) { if (typeof value === 'string') { value = Number(value); } if (typeof value !== 'number' || Number.isNaN(value)) { return new ValidationError(path, 'expected a number'); } - return value; -} -function obj0(path, value, context) { - if (typeof value !== 'object' || value === null) { - return new ValidationError(path, 'expected an object'); - } - const keys = new Set(Object.keys(value)); - const value0 = value['foo']; - if (value0 !== undefined) { - const result0 = obj1([ - ...path, - 'foo' - ], value0, context); - if (result0 instanceof ValidationError) { - return result0; - } - value['foo'] = result0; - keys.delete('foo'); - } else { - value['foo'] = 10; + if (value < -10) { + return new ValidationError(path, 'value less than minimum'); } return value; }" `; -exports[`Objects as free form object 1`] = ` +exports[`Integer negative maximum exclusiveMaximum 1`] = ` "/** Validate a request against the OpenAPI spec @param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate @@ -687,15 +633,20 @@ export class ValidationError extends RequestError { } } function obj0(path, value, context) { - if (typeof value !== 'object' || value === null) { - return new ValidationError(path, 'expected an object'); + if (typeof value === 'string') { + value = Number(value); + } + if (typeof value !== 'number' || Number.isNaN(value)) { + return new ValidationError(path, 'expected a number'); + } + if (value >= -5) { + return new ValidationError(path, 'value greater than maximum'); } - const keys = new Set(Object.keys(value)); return value; }" `; -exports[`Objects with additionalProperties: true 1`] = ` +exports[`Integer negative minimum exclusiveMinimum 1`] = ` "/** Validate a request against the OpenAPI spec @param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate @@ -728,39 +679,21 @@ export class ValidationError extends RequestError { this.path = path; } } -function obj1(path, value, context) { +function obj0(path, value, context) { if (typeof value === 'string') { value = Number(value); } if (typeof value !== 'number' || Number.isNaN(value)) { return new ValidationError(path, 'expected a number'); } - return value; -} -function obj0(path, value, context) { - if (typeof value !== 'object' || value === null) { - return new ValidationError(path, 'expected an object'); - } - const keys = new Set(Object.keys(value)); - const value0 = value['foo']; - if (value0 !== undefined) { - const result0 = obj1([ - ...path, - 'foo' - ], value0, context); - if (result0 instanceof ValidationError) { - return result0; - } - value['foo'] = result0; - keys.delete('foo'); - } else { - value['foo'] = 10; + if (value <= -10) { + return new ValidationError(path, 'value less than minimum'); } return value; }" `; -exports[`Objects with additionalProperties: {} 1`] = ` +exports[`Nullable nullable: true 1`] = ` "/** Validate a request against the OpenAPI spec @param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate @@ -793,7 +726,10 @@ export class ValidationError extends RequestError { this.path = path; } } -function obj1(path, value, context) { +function obj0(path, value, context) { + if (value === null) { + return value; + } if (typeof value === 'string') { value = Number(value); } @@ -801,47 +737,10 @@ function obj1(path, value, context) { return new ValidationError(path, 'expected a number'); } return value; -} -function obj2(path, value, context) { - if (typeof value !== 'string') { - return new ValidationError(path, 'expected a string'); - } - return value; -} -function obj0(path, value, context) { - if (typeof value !== 'object' || value === null) { - return new ValidationError(path, 'expected an object'); - } - const keys = new Set(Object.keys(value)); - const value0 = value['foo']; - if (value0 !== undefined) { - const result0 = obj1([ - ...path, - 'foo' - ], value0, context); - if (result0 instanceof ValidationError) { - return result0; - } - value['foo'] = result0; - keys.delete('foo'); - } else { - value['foo'] = 10; - } - for (const key of keys) { - const result = obj2([ - ...path, - key - ], value[key], context); - if (result instanceof ValidationError) { - return result; - } - value[key] = result; - } - return value; }" `; -exports[`Objects with minProperties/maxProperties 1`] = ` +exports[`String with enum 1`] = ` "/** Validate a request against the OpenAPI spec @param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate @@ -875,21 +774,14 @@ export class ValidationError extends RequestError { } } function obj0(path, value, context) { - if (typeof value !== 'object' || value === null) { - return new ValidationError(path, 'expected an object'); - } - const keys = new Set(Object.keys(value)); - if (keys.size < 1) { - return new ValidationError(path, 'expected at least 1 properties'); - } - if (keys.size > 10) { - return new ValidationError(path, 'expected at most 10 properties'); + if (value !== 'a' && value !== 'b' && value !== 'c') { + return new ValidationError(path, 'expected one of the enum value'); } return value; }" `; -exports[`Array minItems / maxItems 1`] = ` +exports[`String with pattern 1`] = ` "/** Validate a request against the OpenAPI spec @param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate @@ -922,45 +814,27 @@ export class ValidationError extends RequestError { this.path = path; } } -function obj1(path, value, context) { +const obj1 = new RegExp('^[a-z]+$'); +function obj0(path, value, context) { if (typeof value !== 'string') { return new ValidationError(path, 'expected a string'); } + if (!obj1.test(value)) { + return new ValidationError(path, 'expected to match the pattern "^[a-z]+$"'); + } return value; -} -function obj0(path, value, context) { - if (!Array.isArray(value)) { - return new ValidationError(path, 'expected an array'); - } - if (value.length < 1) { - return new ValidationError(path, 'expected at least 1 items'); - } - if (value.length > 10) { - return new ValidationError(path, 'expected at most 10 items'); - } - for (let i = 0; i < value.length; i++) { - const itemResult = obj1([ - ...path, - i - ], value[i], context); - if (itemResult instanceof ValidationError) { - return itemResult; - } - value[i] = itemResult; - } - return value; -}" -`; - -exports[`Array uniqueItems 1`] = ` -"/** -Validate a request against the OpenAPI spec -@param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate -@param {{ stringFormats?: { [format: string]: (value: string, path: string[]) => ValidationError | string | null } }} [context] - Context object to pass to validation functions -@returns {{ operationId?: string; params: Record; query: Record; body?: any; headers: Record; }} -*/ -export function validateRequest(request, context) { - return new RequestError(404, 'no operation match path'); +}" +`; + +exports[`String with pattern 2`] = ` +"/** +Validate a request against the OpenAPI spec +@param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate +@param {{ stringFormats?: { [format: string]: (value: string, path: string[]) => ValidationError | string | null } }} [context] - Context object to pass to validation functions +@returns {{ operationId?: string; params: Record; query: Record; body?: any; headers: Record; }} +*/ +export function validateRequest(request, context) { + return new RequestError(404, 'no operation match path'); } /** Map of all components defined in the spec to their validation functions. @@ -985,36 +859,22 @@ export class ValidationError extends RequestError { this.path = path; } } -function obj1(path, value, context) { +function obj0(path, value, context) { if (typeof value !== 'string') { return new ValidationError(path, 'expected a string'); } - return value; -} -function obj0(path, value, context) { - if (!Array.isArray(value)) { - return new ValidationError(path, 'expected an array'); + const formatResult = context?.stringFormats?.['uri']?.(value, path); + if (formatResult instanceof ValidationError) { + return formatResult; } - const valueSet = new Set(); - for (let i = 0; i < value.length; i++) { - const itemResult = obj1([ - ...path, - i - ], value[i], context); - if (valueSet.has(itemResult)) { - return new ValidationError(path, 'expected unique items'); - } - valueSet.add(itemResult); - if (itemResult instanceof ValidationError) { - return itemResult; - } - value[i] = itemResult; + if (typeof formatResult === 'string') { + value = formatResult; } return value; }" `; -exports[`anyOf 1`] = ` +exports[`String with format 1`] = ` "/** Validate a request against the OpenAPI spec @param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate @@ -1047,35 +907,22 @@ export class ValidationError extends RequestError { this.path = path; } } -function obj1(path, value, context) { - if (typeof value === 'string') { - value = Number(value); - } - if (typeof value !== 'number' || Number.isNaN(value)) { - return new ValidationError(path, 'expected a number'); - } - return value; -} -function obj2(path, value, context) { +function obj0(path, value, context) { if (typeof value !== 'string') { return new ValidationError(path, 'expected a string'); } - return value; -} -function obj0(path, value, context) { - const value0 = obj1(path, value, context); - if (!(value0 instanceof ValidationError)) { - return value0; + const formatResult = context?.stringFormats?.['uri']?.(value, path); + if (formatResult instanceof ValidationError) { + return formatResult; } - const value1 = obj2(path, value, context); - if (!(value1 instanceof ValidationError)) { - return value1; + if (typeof formatResult === 'string') { + value = formatResult; } - return new ValidationError(path, 'expected one of the anyOf schemas to match'); + return value; }" `; -exports[`oneOf 1`] = ` +exports[`Objects with a required prop 1`] = ` "/** Validate a request against the OpenAPI spec @param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate @@ -1124,38 +971,41 @@ function obj2(path, value, context) { return value; } function obj0(path, value, context) { - let result; - let error; - const alt0 = obj1(path, value, context); - if (alt0 instanceof ValidationError) { - if (error === undefined || error.path.length < alt0.path.length) { - error = alt0; - } - } else { - result = alt0; + if (typeof value !== 'object' || value === null) { + return new ValidationError(path, 'expected an object'); } - const alt1 = obj2(path, value, context); - if (alt1 instanceof ValidationError) { - if (error === undefined || error.path.length < alt1.path.length) { - error = alt1; + const keys = new Set(Object.keys(value)); + const value0 = value['foo']; + if (value0 !== undefined) { + const result0 = obj1([ + ...path, + 'foo' + ], value0, context); + if (result0 instanceof ValidationError) { + return result0; } + value['foo'] = result0; + keys.delete('foo'); } else { - if (result !== undefined) { - return new ValidationError(path, 'expected to only match one of the schemas'); - } - result = alt1; + return new ValidationError(path, 'expected "foo" to be defined'); } - if (result === undefined) { - if (error && error.path.length > path.length + 1) { - return error; - } else - return new ValidationError(path, 'expected to match one'); + const value1 = value['bar']; + if (value1 !== undefined) { + const result1 = obj2([ + ...path, + 'bar' + ], value1, context); + if (result1 instanceof ValidationError) { + return result1; + } + value['bar'] = result1; + keys.delete('bar'); } - return result; + return value; }" `; -exports[`allOf 1`] = ` +exports[`Objects with a default value 1`] = ` "/** Validate a request against the OpenAPI spec @param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate @@ -1188,7 +1038,7 @@ export class ValidationError extends RequestError { this.path = path; } } -function obj2(path, value, context) { +function obj1(path, value, context) { if (typeof value === 'string') { value = Number(value); } @@ -1197,65 +1047,72 @@ function obj2(path, value, context) { } return value; } -function obj1(path, value, context) { +function obj0(path, value, context) { if (typeof value !== 'object' || value === null) { return new ValidationError(path, 'expected an object'); } const keys = new Set(Object.keys(value)); - const value0 = value['a']; + const value0 = value['foo']; if (value0 !== undefined) { - const result0 = obj2([ + const result0 = obj1([ ...path, - 'a' + 'foo' ], value0, context); if (result0 instanceof ValidationError) { return result0; } - value['a'] = result0; - keys.delete('a'); + value['foo'] = result0; + keys.delete('foo'); + } else { + value['foo'] = 10; } return value; +}" +`; + +exports[`Objects as free form object 1`] = ` +"/** +Validate a request against the OpenAPI spec +@param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate +@param {{ stringFormats?: { [format: string]: (value: string, path: string[]) => ValidationError | string | null } }} [context] - Context object to pass to validation functions +@returns {{ operationId?: string; params: Record; query: Record; body?: any; headers: Record; }} +*/ +export function validateRequest(request, context) { + return new RequestError(404, 'no operation match path'); } -function obj4(path, value, context) { - if (typeof value !== 'string') { - return new ValidationError(path, 'expected a string'); +/** +Map of all components defined in the spec to their validation functions. +{Object.(path: string[], value: T, context: any) => (T | ValidationError)>} +*/ +export const componentSchemas = {}; +export class RequestError extends Error { + /** @param {number} code HTTP code for the error +@param {string} message The error message*/ + constructor(code, message) { + super(message); + /** @type {number} HTTP code for the error*/ + this.code = code; } - return value; } -function obj3(path, value, context) { +export class ValidationError extends RequestError { + /** @param {string[]} path The path that failed validation +@param {string} message The error message*/ + constructor(path, message) { + super(409, message); + /** @type {string[]} The path that failed validation*/ + this.path = path; + } +} +function obj0(path, value, context) { if (typeof value !== 'object' || value === null) { return new ValidationError(path, 'expected an object'); } const keys = new Set(Object.keys(value)); - const value0 = value['b']; - if (value0 !== undefined) { - const result0 = obj4([ - ...path, - 'b' - ], value0, context); - if (result0 instanceof ValidationError) { - return result0; - } - value['b'] = result0; - keys.delete('b'); - } return value; -} -function obj0(path, value, context) { - let result = value; - result = obj1(path, result, context); - if (result instanceof ValidationError) { - return result; - } - result = obj3(path, result, context); - if (result instanceof ValidationError) { - return result; - } - return result; }" `; -exports[`Integer negative 1`] = ` +exports[`Objects with additionalProperties: true 1`] = ` "/** Validate a request against the OpenAPI spec @param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate @@ -1288,15 +1145,39 @@ export class ValidationError extends RequestError { this.path = path; } } +function obj1(path, value, context) { + if (typeof value === 'string') { + value = Number(value); + } + if (typeof value !== 'number' || Number.isNaN(value)) { + return new ValidationError(path, 'expected a number'); + } + return value; +} function obj0(path, value, context) { - if (value !== -1 && value !== 0 && value !== 1) { - return new ValidationError(path, 'expected one of the enum value'); + if (typeof value !== 'object' || value === null) { + return new ValidationError(path, 'expected an object'); + } + const keys = new Set(Object.keys(value)); + const value0 = value['foo']; + if (value0 !== undefined) { + const result0 = obj1([ + ...path, + 'foo' + ], value0, context); + if (result0 instanceof ValidationError) { + return result0; + } + value['foo'] = result0; + keys.delete('foo'); + } else { + value['foo'] = 10; } return value; }" `; -exports[`Number negative maximum 1`] = ` +exports[`Objects with additionalProperties: {} 1`] = ` "/** Validate a request against the OpenAPI spec @param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate @@ -1329,21 +1210,55 @@ export class ValidationError extends RequestError { this.path = path; } } -function obj0(path, value, context) { +function obj1(path, value, context) { if (typeof value === 'string') { value = Number(value); } if (typeof value !== 'number' || Number.isNaN(value)) { return new ValidationError(path, 'expected a number'); } - if (value > -5) { - return new ValidationError(path, 'value greater than maximum'); + return value; +} +function obj2(path, value, context) { + if (typeof value !== 'string') { + return new ValidationError(path, 'expected a string'); + } + return value; +} +function obj0(path, value, context) { + if (typeof value !== 'object' || value === null) { + return new ValidationError(path, 'expected an object'); + } + const keys = new Set(Object.keys(value)); + const value0 = value['foo']; + if (value0 !== undefined) { + const result0 = obj1([ + ...path, + 'foo' + ], value0, context); + if (result0 instanceof ValidationError) { + return result0; + } + value['foo'] = result0; + keys.delete('foo'); + } else { + value['foo'] = 10; + } + for (const key of keys) { + const result = obj2([ + ...path, + key + ], value[key], context); + if (result instanceof ValidationError) { + return result; + } + value[key] = result; } return value; }" `; -exports[`Number negative minimum 1`] = ` +exports[`Objects with minProperties/maxProperties 1`] = ` "/** Validate a request against the OpenAPI spec @param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate @@ -1377,20 +1292,21 @@ export class ValidationError extends RequestError { } } function obj0(path, value, context) { - if (typeof value === 'string') { - value = Number(value); + if (typeof value !== 'object' || value === null) { + return new ValidationError(path, 'expected an object'); } - if (typeof value !== 'number' || Number.isNaN(value)) { - return new ValidationError(path, 'expected a number'); + const keys = new Set(Object.keys(value)); + if (keys.size < 1) { + return new ValidationError(path, 'expected at least 1 properties'); } - if (value < -10) { - return new ValidationError(path, 'value less than minimum'); + if (keys.size > 10) { + return new ValidationError(path, 'expected at most 10 properties'); } return value; }" `; -exports[`Number negative maximum exclusiveMaximum 1`] = ` +exports[`Objects with propertyNames 1`] = ` "/** Validate a request against the OpenAPI spec @param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate @@ -1423,21 +1339,35 @@ export class ValidationError extends RequestError { this.path = path; } } -function obj0(path, value, context) { - if (typeof value === 'string') { - value = Number(value); +const obj2 = new RegExp('^[a-z]+$'); +function obj1(path, value, context) { + if (typeof value !== 'string') { + return new ValidationError(path, 'expected a string'); } - if (typeof value !== 'number' || Number.isNaN(value)) { - return new ValidationError(path, 'expected a number'); + if (!obj2.test(value)) { + return new ValidationError(path, 'expected to match the pattern "^[a-z]+$"'); } - if (value >= -5) { - return new ValidationError(path, 'value greater than maximum'); + return value; +} +function obj0(path, value, context) { + if (typeof value !== 'object' || value === null) { + return new ValidationError(path, 'expected an object'); + } + const keys = new Set(Object.keys(value)); + for (const key of keys) { + const keyResult = obj1([ + ...path, + key + ], key, context); + if (keyResult instanceof ValidationError) { + return keyResult; + } } return value; }" `; -exports[`Number negative minimum exclusiveMinimum 1`] = ` +exports[`Array minItems / maxItems 1`] = ` "/** Validate a request against the OpenAPI spec @param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate @@ -1470,21 +1400,37 @@ export class ValidationError extends RequestError { this.path = path; } } +function obj1(path, value, context) { + if (typeof value !== 'string') { + return new ValidationError(path, 'expected a string'); + } + return value; +} function obj0(path, value, context) { - if (typeof value === 'string') { - value = Number(value); + if (!Array.isArray(value)) { + return new ValidationError(path, 'expected an array'); } - if (typeof value !== 'number' || Number.isNaN(value)) { - return new ValidationError(path, 'expected a number'); + if (value.length < 1) { + return new ValidationError(path, 'expected at least 1 items'); } - if (value <= -10) { - return new ValidationError(path, 'value less than minimum'); + if (value.length > 10) { + return new ValidationError(path, 'expected at most 10 items'); + } + for (let i = 0; i < value.length; i++) { + const itemResult = obj1([ + ...path, + i + ], value[i], context); + if (itemResult instanceof ValidationError) { + return itemResult; + } + value[i] = itemResult; } return value; }" `; -exports[`Integer negative maximum 1`] = ` +exports[`Array uniqueItems 1`] = ` "/** Validate a request against the OpenAPI spec @param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate @@ -1517,21 +1463,36 @@ export class ValidationError extends RequestError { this.path = path; } } -function obj0(path, value, context) { - if (typeof value === 'string') { - value = Number(value); +function obj1(path, value, context) { + if (typeof value !== 'string') { + return new ValidationError(path, 'expected a string'); } - if (typeof value !== 'number' || Number.isNaN(value)) { - return new ValidationError(path, 'expected a number'); + return value; +} +function obj0(path, value, context) { + if (!Array.isArray(value)) { + return new ValidationError(path, 'expected an array'); } - if (value > -5) { - return new ValidationError(path, 'value greater than maximum'); + const valueSet = new Set(); + for (let i = 0; i < value.length; i++) { + const itemResult = obj1([ + ...path, + i + ], value[i], context); + if (valueSet.has(itemResult)) { + return new ValidationError(path, 'expected unique items'); + } + valueSet.add(itemResult); + if (itemResult instanceof ValidationError) { + return itemResult; + } + value[i] = itemResult; } return value; }" `; -exports[`Integer negative minimum 1`] = ` +exports[`anyOf 1`] = ` "/** Validate a request against the OpenAPI spec @param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate @@ -1564,21 +1525,35 @@ export class ValidationError extends RequestError { this.path = path; } } -function obj0(path, value, context) { +function obj1(path, value, context) { if (typeof value === 'string') { value = Number(value); } if (typeof value !== 'number' || Number.isNaN(value)) { return new ValidationError(path, 'expected a number'); } - if (value < -10) { - return new ValidationError(path, 'value less than minimum'); + return value; +} +function obj2(path, value, context) { + if (typeof value !== 'string') { + return new ValidationError(path, 'expected a string'); } return value; +} +function obj0(path, value, context) { + const value0 = obj1(path, value, context); + if (!(value0 instanceof ValidationError)) { + return value0; + } + const value1 = obj2(path, value, context); + if (!(value1 instanceof ValidationError)) { + return value1; + } + return new ValidationError(path, 'expected one of the anyOf schemas to match'); }" `; -exports[`Integer negative maximum exclusiveMaximum 1`] = ` +exports[`oneOf 1`] = ` "/** Validate a request against the OpenAPI spec @param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate @@ -1611,21 +1586,54 @@ export class ValidationError extends RequestError { this.path = path; } } -function obj0(path, value, context) { +function obj1(path, value, context) { if (typeof value === 'string') { value = Number(value); } if (typeof value !== 'number' || Number.isNaN(value)) { return new ValidationError(path, 'expected a number'); } - if (value >= -5) { - return new ValidationError(path, 'value greater than maximum'); + return value; +} +function obj2(path, value, context) { + if (typeof value !== 'string') { + return new ValidationError(path, 'expected a string'); } return value; +} +function obj0(path, value, context) { + let result; + let error; + const alt0 = obj1(path, value, context); + if (alt0 instanceof ValidationError) { + if (error === undefined || error.path.length < alt0.path.length) { + error = alt0; + } + } else { + result = alt0; + } + const alt1 = obj2(path, value, context); + if (alt1 instanceof ValidationError) { + if (error === undefined || error.path.length < alt1.path.length) { + error = alt1; + } + } else { + if (result !== undefined) { + return new ValidationError(path, 'expected to only match one of the schemas'); + } + result = alt1; + } + if (result === undefined) { + if (error && error.path.length > path.length + 1) { + return error; + } else + return new ValidationError(path, 'expected to match one'); + } + return result; }" `; -exports[`Integer negative minimum exclusiveMinimum 1`] = ` +exports[`allOf 1`] = ` "/** Validate a request against the OpenAPI spec @param {{ method: string; path: string; body?: any; query: Record; headers: Record; }} request - Input request to validate @@ -1658,16 +1666,69 @@ export class ValidationError extends RequestError { this.path = path; } } -function obj0(path, value, context) { +function obj2(path, value, context) { if (typeof value === 'string') { value = Number(value); } if (typeof value !== 'number' || Number.isNaN(value)) { return new ValidationError(path, 'expected a number'); } - if (value <= -10) { - return new ValidationError(path, 'value less than minimum'); + return value; +} +function obj1(path, value, context) { + if (typeof value !== 'object' || value === null) { + return new ValidationError(path, 'expected an object'); + } + const keys = new Set(Object.keys(value)); + const value0 = value['a']; + if (value0 !== undefined) { + const result0 = obj2([ + ...path, + 'a' + ], value0, context); + if (result0 instanceof ValidationError) { + return result0; + } + value['a'] = result0; + keys.delete('a'); + } + return value; +} +function obj4(path, value, context) { + if (typeof value !== 'string') { + return new ValidationError(path, 'expected a string'); + } + return value; +} +function obj3(path, value, context) { + if (typeof value !== 'object' || value === null) { + return new ValidationError(path, 'expected an object'); + } + const keys = new Set(Object.keys(value)); + const value0 = value['b']; + if (value0 !== undefined) { + const result0 = obj4([ + ...path, + 'b' + ], value0, context); + if (result0 instanceof ValidationError) { + return result0; + } + value['b'] = result0; + keys.delete('b'); } return value; +} +function obj0(path, value, context) { + let result = value; + result = obj1(path, result, context); + if (result instanceof ValidationError) { + return result; + } + result = obj3(path, result, context); + if (result instanceof ValidationError) { + return result; + } + return result; }" `; diff --git a/src/tests/compileValueSchema.test.ts b/src/tests/compileValueSchema.test.ts index e88abec..6580efa 100644 --- a/src/tests/compileValueSchema.test.ts +++ b/src/tests/compileValueSchema.test.ts @@ -276,6 +276,17 @@ describe('Objects', () => { }); expect(compiler.compile()).toMatchSnapshot(); }); + + test('with propertyNames', () => { + const compiler = new Compiler(); + compileValueSchema(compiler, { + type: 'object', + propertyNames: { + pattern: '^[a-z]+$', + }, + }); + expect(compiler.compile()).toMatchSnapshot(); + }); }); describe('Array', () => { diff --git a/src/tests/hash.test.ts b/src/tests/hash.test.ts index 1d75564..b81af62 100644 --- a/src/tests/hash.test.ts +++ b/src/tests/hash.test.ts @@ -6,3 +6,29 @@ describe('Strings', () => { expect(hash('foo')).not.toEqual(hash('bar')); }); }); + +describe('Objects', () => { + test('with different propertyNames', () => { + const baseSchema = { + type: 'object', + properties: { + foo: { type: 'string' }, + }, + }; + expect( + hash({ + ...baseSchema, + propertyNames: { + pattern: '^[a-z]+$', + }, + }), + ).not.toEqual( + hash({ + ...baseSchema, + propertyNames: { + pattern: '^[A-Z]+$', + }, + }), + ); + }); +}); diff --git a/src/types.ts b/src/types.ts index 6f69c80..51df833 100644 --- a/src/types.ts +++ b/src/types.ts @@ -70,6 +70,8 @@ export interface OpenAPIStringSchema extends OpenAPINullableSchema, OpenAPIEnuma pattern?: string; } +export type OpenAPIPropertyNamesSchema = OpenAPIValueSchema | Omit; + interface CommonNumberSchema { maximum?: number; minimum?: number; @@ -103,6 +105,7 @@ export interface OpenAPIObjectSchema extends OpenAPINullableSchema { properties?: { [key: string]: OpenAPIValueSchema & { default?: string | number | boolean }; }; + propertyNames?: OpenAPIPropertyNamesSchema; additionalProperties?: boolean | OpenAPIValueSchema; minProperties?: number; maxProperties?: number;