diff --git a/src/compileValueSchema.ts b/src/compileValueSchema.ts index 6b39d20..517c0e7 100644 --- a/src/compileValueSchema.ts +++ b/src/compileValueSchema.ts @@ -17,6 +17,19 @@ import { } from './types'; import { ValidationErrorIdentifier } from './error'; +/** + * Creates a literal value that handles negative numbers properly for escodegen. + * For negative numbers, creates a unary expression instead of a negative literal. + */ +function createSafeLiteral( + value: string | number | boolean, +): namedTypes.Literal | namedTypes.UnaryExpression { + if (typeof value === 'number' && value < 0) { + return builders.unaryExpression('-', builders.literal(-value)); + } + return builders.literal(value); +} + /** * Compile a JSON schema into a validation function. */ @@ -727,7 +740,7 @@ function compileNumberSchema( builders.binaryExpression( schema.exclusiveMaximum ? '>=' : '>', value, - builders.literal(schema.maximum), + createSafeLiteral(schema.maximum), ), builders.blockStatement([ builders.returnStatement(error('value greater than maximum')), @@ -742,7 +755,7 @@ function compileNumberSchema( builders.binaryExpression( schema.exclusiveMinimum ? '<=' : '<', value, - builders.literal(schema.minimum), + createSafeLiteral(schema.minimum), ), builders.blockStatement([ builders.returnStatement(error('value less than minimum')), @@ -987,7 +1000,8 @@ function compileEnumableCheck( builders.ifStatement( schema.enum.reduce( (acc, val) => { - const test = builders.binaryExpression('!==', value, builders.literal(val)); + const literalValue = createSafeLiteral(val); + const test = builders.binaryExpression('!==', value, literalValue); if (!acc) { return test; diff --git a/src/tests/__snapshots__/compileValueSchema.test.ts.snap b/src/tests/__snapshots__/compileValueSchema.test.ts.snap index f44bae8..ce1391d 100644 --- a/src/tests/__snapshots__/compileValueSchema.test.ts.snap +++ b/src/tests/__snapshots__/compileValueSchema.test.ts.snap @@ -1254,3 +1254,420 @@ function obj0(path, value, context) { return result; }" `; + +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 +@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. +{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; + } +} +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 (value !== -1 && value !== 0 && value !== 1) { + return new ValidationError(path, 'expected one of the enum value'); + } + return value; +}" +`; + +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 +@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. +{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; + } +} +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 === '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[`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 +@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. +{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; + } +} +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 === '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[`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 +@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. +{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; + } +} +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 === '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[`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 +@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. +{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; + } +} +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 === '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[`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 +@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. +{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; + } +} +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 === '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[`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 +@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. +{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; + } +} +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 === '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[`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 +@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. +{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; + } +} +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 === '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[`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 +@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. +{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; + } +} +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 === '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; +}" +`; diff --git a/src/tests/compileValueSchema.test.ts b/src/tests/compileValueSchema.test.ts index a86bef1..e88abec 100644 --- a/src/tests/compileValueSchema.test.ts +++ b/src/tests/compileValueSchema.test.ts @@ -48,6 +48,44 @@ describe('Number', () => { }); expect(compiler.compile()).toMatchSnapshot(); }); + + test('negative maximum', () => { + const compiler = new Compiler(); + compileValueSchema(compiler, { + type: 'number', + maximum: -5, + }); + expect(compiler.compile()).toMatchSnapshot(); + }); + + test('negative minimum', () => { + const compiler = new Compiler(); + compileValueSchema(compiler, { + type: 'number', + minimum: -10, + }); + expect(compiler.compile()).toMatchSnapshot(); + }); + + test('negative maximum exclusiveMaximum', () => { + const compiler = new Compiler(); + compileValueSchema(compiler, { + type: 'number', + maximum: -5, + exclusiveMaximum: true, + }); + expect(compiler.compile()).toMatchSnapshot(); + }); + + test('negative minimum exclusiveMinimum', () => { + const compiler = new Compiler(); + compileValueSchema(compiler, { + type: 'number', + minimum: -10, + exclusiveMinimum: true, + }); + expect(compiler.compile()).toMatchSnapshot(); + }); }); describe('Integer', () => { @@ -59,6 +97,53 @@ describe('Integer', () => { }); expect(compiler.compile()).toMatchSnapshot(); }); + + test('negative', () => { + const compiler = new Compiler(); + compileValueSchema(compiler, { + type: 'integer', + enum: [-1, 0, 1], + }); + expect(compiler.compile()).toMatchSnapshot(); + }); + + test('negative maximum', () => { + const compiler = new Compiler(); + compileValueSchema(compiler, { + type: 'integer', + maximum: -5, + }); + expect(compiler.compile()).toMatchSnapshot(); + }); + + test('negative minimum', () => { + const compiler = new Compiler(); + compileValueSchema(compiler, { + type: 'integer', + minimum: -10, + }); + expect(compiler.compile()).toMatchSnapshot(); + }); + + test('negative maximum exclusiveMaximum', () => { + const compiler = new Compiler(); + compileValueSchema(compiler, { + type: 'integer', + maximum: -5, + exclusiveMaximum: true, + }); + expect(compiler.compile()).toMatchSnapshot(); + }); + + test('negative minimum exclusiveMinimum', () => { + const compiler = new Compiler(); + compileValueSchema(compiler, { + type: 'integer', + minimum: -10, + exclusiveMinimum: true, + }); + expect(compiler.compile()).toMatchSnapshot(); + }); }); describe('Nullable', () => {