Skip to content

Add support for number/integer fields maximum, minimum, exclusiveMaximum, exclusiveMinimum #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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']);
});