Skip to content

Commit

Permalink
Add support for number/integer fields maximum, minimum, exclusiveMaxi…
Browse files Browse the repository at this point in the history
…mum, exclusiveMinimum (#2)
  • Loading branch information
emmerich authored Mar 14, 2024
1 parent 71aaa9b commit 491ecae
Show file tree
Hide file tree
Showing 6 changed files with 283 additions and 3 deletions.
30 changes: 30 additions & 0 deletions src/compileValueSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,36 @@ function compileNumberSchema(
),
);

if (schema.maximum !== undefined) {
nodes.push(
builders.ifStatement(
builders.binaryExpression(
schema.exclusiveMaximum ? '>=' : '>',
value,
builders.literal(schema.maximum),
),
builders.blockStatement([
builders.returnStatement(error('value greater than maximum')),
]),
),
);
}

if (schema.minimum !== undefined) {
nodes.push(
builders.ifStatement(
builders.binaryExpression(
schema.exclusiveMinimum ? '<=' : '<',
value,
builders.literal(schema.minimum),
),
builders.blockStatement([
builders.returnStatement(error('value less than minimum')),
]),
),
);
}

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

return nodes;
Expand Down
168 changes: 168 additions & 0 deletions src/tests/__snapshots__/compileValueSchema.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,174 @@ function obj0(path, value, context) {
}"
`;

exports[`Number maximum 1`] = `
"/**
Validate a request against the OpenAPI spec
@param {{ method: string; path: string; body?: any; query: Record<string, string>; headers: Record<string, string>; }} request - Input request to validate
@param {{ stringFormats?: { [format: string]: (value: string, path: string[]) => ValidationError | null } }} [context] - Context object to pass to validation functions
@returns {{ operationId?: string; params: Record<string, string>; query: Record<string, string>; body?: any; headers: Record<string, string>; }}
*/
export function validateRequest(request, context) {
return new RequestError(404, 'no operation match path');
}
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 greater than maximum');
}
return value;
}"
`;

exports[`Number maximum exclusiveMaximum 1`] = `
"/**
Validate a request against the OpenAPI spec
@param {{ method: string; path: string; body?: any; query: Record<string, string>; headers: Record<string, string>; }} request - Input request to validate
@param {{ stringFormats?: { [format: string]: (value: string, path: string[]) => ValidationError | null } }} [context] - Context object to pass to validation functions
@returns {{ operationId?: string; params: Record<string, string>; query: Record<string, string>; body?: any; headers: Record<string, string>; }}
*/
export function validateRequest(request, context) {
return new RequestError(404, 'no operation match path');
}
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 greater than maximum');
}
return value;
}"
`;

exports[`Number minimum 1`] = `
"/**
Validate a request against the OpenAPI spec
@param {{ method: string; path: string; body?: any; query: Record<string, string>; headers: Record<string, string>; }} request - Input request to validate
@param {{ stringFormats?: { [format: string]: (value: string, path: string[]) => ValidationError | null } }} [context] - Context object to pass to validation functions
@returns {{ operationId?: string; params: Record<string, string>; query: Record<string, string>; body?: any; headers: Record<string, string>; }}
*/
export function validateRequest(request, context) {
return new RequestError(404, 'no operation match path');
}
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 minimim exclusiveMinimum 1`] = `
"/**
Validate a request against the OpenAPI spec
@param {{ method: string; path: string; body?: any; query: Record<string, string>; headers: Record<string, string>; }} request - Input request to validate
@param {{ stringFormats?: { [format: string]: (value: string, path: string[]) => ValidationError | null } }} [context] - Context object to pass to validation functions
@returns {{ operationId?: string; params: Record<string, string>; query: Record<string, string>; body?: any; headers: Record<string, string>; }}
*/
export function validateRequest(request, context) {
return new RequestError(404, 'no operation match path');
}
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 basic 1`] = `
"/**
Validate a request against the OpenAPI spec
Expand Down
38 changes: 38 additions & 0 deletions src/tests/compileValueSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,44 @@ describe('Number', () => {
});
expect(compiler.compile()).toMatchSnapshot();
});

test('maximum', () => {
const compiler = new Compiler();
compileValueSchema(compiler, {
type: 'number',
maximum: 10,
});
expect(compiler.compile()).toMatchSnapshot();
});

test('maximum exclusiveMaximum', () => {
const compiler = new Compiler();
compileValueSchema(compiler, {
type: 'number',
maximum: 10,
exclusiveMaximum: true,
});
expect(compiler.compile()).toMatchSnapshot();
});

test('minimum', () => {
const compiler = new Compiler();
compileValueSchema(compiler, {
type: 'number',
minimum: 10,
});
expect(compiler.compile()).toMatchSnapshot();
});

test('minimim exclusiveMinimum', () => {
const compiler = new Compiler();
compileValueSchema(compiler, {
type: 'number',
minimum: 10,
exclusiveMinimum: true,
});
expect(compiler.compile()).toMatchSnapshot();
});
});

describe('Integer', () => {
Expand Down
18 changes: 16 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,25 @@ export interface OpenAPIStringSchema extends OpenAPINullableSchema, OpenAPIEnuma
pattern?: string;
}

export interface OpenAPINumberSchema extends OpenAPINullableSchema, OpenAPIEnumableSchema {
interface CommonNumberSchema {
maximum?: number;
minimum?: number;
exclusiveMinimum?: boolean;
exclusiveMaximum?: boolean;
}
export interface OpenAPINumberSchema
extends CommonNumberSchema,
OpenAPINullableSchema,
OpenAPIEnumableSchema {
type: 'number';
maximum?: number;
minimum?: number;
}

export interface OpenAPIIntegerSchema extends OpenAPINullableSchema, OpenAPIEnumableSchema {
export interface OpenAPIIntegerSchema
extends CommonNumberSchema,
OpenAPINullableSchema,
OpenAPIEnumableSchema {
type: 'integer';
format?: 'int32';
}
Expand Down
4 changes: 3 additions & 1 deletion tests/gitbook.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@
"in": "query",
"description": "The number of results per page",
"schema": {
"type": "number"
"type": "number",
"maximum": 100,
"minimum": 0
}
},
"listPage": {
Expand Down
28 changes: 28 additions & 0 deletions tests/gitbook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,3 +318,31 @@ test('GET spaces/space_iphone-doc/revisions/somerevision/files?metadata=true', (
},
});
});

test('GET spaces/space_iphone-doc/revisions/somerevision/files?limit=1000 (invalid, number above maximum)', () => {
const result = validateRequest({
path: '/spaces/space_iphone-doc/revisions/somerevision/files',
method: 'get',
headers: {
'content-type': 'application/json',
},
query: {
limit: '1000',
},
});
expect(result instanceof ValidationError ? result.path : null).toEqual(['query', 'limit']);
});

test('GET spaces/space_iphone-doc/revisions/somerevision/files?limit=-1 (invalid, number below minimum)', () => {
const result = validateRequest({
path: '/spaces/space_iphone-doc/revisions/somerevision/files',
method: 'get',
headers: {
'content-type': 'application/json',
},
query: {
limit: '-1',
},
});
expect(result instanceof ValidationError ? result.path : null).toEqual(['query', 'limit']);
});

0 comments on commit 491ecae

Please sign in to comment.