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
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
@@ -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;
168 changes: 168 additions & 0 deletions src/tests/__snapshots__/compileValueSchema.test.ts.snap
Original file line number Diff line number Diff line change
@@ -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
38 changes: 38 additions & 0 deletions src/tests/compileValueSchema.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
18 changes: 16 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -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';
}
4 changes: 3 additions & 1 deletion tests/gitbook.json
Original file line number Diff line number Diff line change
@@ -67,7 +67,9 @@
"in": "query",
"description": "The number of results per page",
"schema": {
"type": "number"
"type": "number",
"maximum": 100,
"minimum": 0
}
},
"listPage": {
28 changes: 28 additions & 0 deletions tests/gitbook.test.ts
Original file line number Diff line number Diff line change
@@ -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']);
});