Skip to content

Commit 491ecae

Browse files
authored
Add support for number/integer fields maximum, minimum, exclusiveMaximum, exclusiveMinimum (#2)
1 parent 71aaa9b commit 491ecae

6 files changed

+283
-3
lines changed

src/compileValueSchema.ts

+30
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,36 @@ function compileNumberSchema(
657657
),
658658
);
659659

660+
if (schema.maximum !== undefined) {
661+
nodes.push(
662+
builders.ifStatement(
663+
builders.binaryExpression(
664+
schema.exclusiveMaximum ? '>=' : '>',
665+
value,
666+
builders.literal(schema.maximum),
667+
),
668+
builders.blockStatement([
669+
builders.returnStatement(error('value greater than maximum')),
670+
]),
671+
),
672+
);
673+
}
674+
675+
if (schema.minimum !== undefined) {
676+
nodes.push(
677+
builders.ifStatement(
678+
builders.binaryExpression(
679+
schema.exclusiveMinimum ? '<=' : '<',
680+
value,
681+
builders.literal(schema.minimum),
682+
),
683+
builders.blockStatement([
684+
builders.returnStatement(error('value less than minimum')),
685+
]),
686+
),
687+
);
688+
}
689+
660690
nodes.push(builders.returnStatement(value));
661691

662692
return nodes;

src/tests/__snapshots__/compileValueSchema.test.ts.snap

+168
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,174 @@ function obj0(path, value, context) {
3939
}"
4040
`;
4141

42+
exports[`Number maximum 1`] = `
43+
"/**
44+
Validate a request against the OpenAPI spec
45+
@param {{ method: string; path: string; body?: any; query: Record<string, string>; headers: Record<string, string>; }} request - Input request to validate
46+
@param {{ stringFormats?: { [format: string]: (value: string, path: string[]) => ValidationError | null } }} [context] - Context object to pass to validation functions
47+
@returns {{ operationId?: string; params: Record<string, string>; query: Record<string, string>; body?: any; headers: Record<string, string>; }}
48+
*/
49+
export function validateRequest(request, context) {
50+
return new RequestError(404, 'no operation match path');
51+
}
52+
export class RequestError extends Error {
53+
/** @param {number} code HTTP code for the error
54+
@param {string} message The error message*/
55+
constructor(code, message) {
56+
super(message);
57+
/** @type {number} HTTP code for the error*/
58+
this.code = code;
59+
}
60+
}
61+
export class ValidationError extends RequestError {
62+
/** @param {string[]} path The path that failed validation
63+
@param {string} message The error message*/
64+
constructor(path, message) {
65+
super(409, message);
66+
/** @type {string[]} The path that failed validation*/
67+
this.path = path;
68+
}
69+
}
70+
function obj0(path, value, context) {
71+
if (typeof value === 'string') {
72+
value = Number(value);
73+
}
74+
if (typeof value !== 'number' || Number.isNaN(value)) {
75+
return new ValidationError(path, 'expected a number');
76+
}
77+
if (value > 10) {
78+
return new ValidationError(path, 'value greater than maximum');
79+
}
80+
return value;
81+
}"
82+
`;
83+
84+
exports[`Number maximum exclusiveMaximum 1`] = `
85+
"/**
86+
Validate a request against the OpenAPI spec
87+
@param {{ method: string; path: string; body?: any; query: Record<string, string>; headers: Record<string, string>; }} request - Input request to validate
88+
@param {{ stringFormats?: { [format: string]: (value: string, path: string[]) => ValidationError | null } }} [context] - Context object to pass to validation functions
89+
@returns {{ operationId?: string; params: Record<string, string>; query: Record<string, string>; body?: any; headers: Record<string, string>; }}
90+
*/
91+
export function validateRequest(request, context) {
92+
return new RequestError(404, 'no operation match path');
93+
}
94+
export class RequestError extends Error {
95+
/** @param {number} code HTTP code for the error
96+
@param {string} message The error message*/
97+
constructor(code, message) {
98+
super(message);
99+
/** @type {number} HTTP code for the error*/
100+
this.code = code;
101+
}
102+
}
103+
export class ValidationError extends RequestError {
104+
/** @param {string[]} path The path that failed validation
105+
@param {string} message The error message*/
106+
constructor(path, message) {
107+
super(409, message);
108+
/** @type {string[]} The path that failed validation*/
109+
this.path = path;
110+
}
111+
}
112+
function obj0(path, value, context) {
113+
if (typeof value === 'string') {
114+
value = Number(value);
115+
}
116+
if (typeof value !== 'number' || Number.isNaN(value)) {
117+
return new ValidationError(path, 'expected a number');
118+
}
119+
if (value >= 10) {
120+
return new ValidationError(path, 'value greater than maximum');
121+
}
122+
return value;
123+
}"
124+
`;
125+
126+
exports[`Number minimum 1`] = `
127+
"/**
128+
Validate a request against the OpenAPI spec
129+
@param {{ method: string; path: string; body?: any; query: Record<string, string>; headers: Record<string, string>; }} request - Input request to validate
130+
@param {{ stringFormats?: { [format: string]: (value: string, path: string[]) => ValidationError | null } }} [context] - Context object to pass to validation functions
131+
@returns {{ operationId?: string; params: Record<string, string>; query: Record<string, string>; body?: any; headers: Record<string, string>; }}
132+
*/
133+
export function validateRequest(request, context) {
134+
return new RequestError(404, 'no operation match path');
135+
}
136+
export class RequestError extends Error {
137+
/** @param {number} code HTTP code for the error
138+
@param {string} message The error message*/
139+
constructor(code, message) {
140+
super(message);
141+
/** @type {number} HTTP code for the error*/
142+
this.code = code;
143+
}
144+
}
145+
export class ValidationError extends RequestError {
146+
/** @param {string[]} path The path that failed validation
147+
@param {string} message The error message*/
148+
constructor(path, message) {
149+
super(409, message);
150+
/** @type {string[]} The path that failed validation*/
151+
this.path = path;
152+
}
153+
}
154+
function obj0(path, value, context) {
155+
if (typeof value === 'string') {
156+
value = Number(value);
157+
}
158+
if (typeof value !== 'number' || Number.isNaN(value)) {
159+
return new ValidationError(path, 'expected a number');
160+
}
161+
if (value < 10) {
162+
return new ValidationError(path, 'value less than minimum');
163+
}
164+
return value;
165+
}"
166+
`;
167+
168+
exports[`Number minimim exclusiveMinimum 1`] = `
169+
"/**
170+
Validate a request against the OpenAPI spec
171+
@param {{ method: string; path: string; body?: any; query: Record<string, string>; headers: Record<string, string>; }} request - Input request to validate
172+
@param {{ stringFormats?: { [format: string]: (value: string, path: string[]) => ValidationError | null } }} [context] - Context object to pass to validation functions
173+
@returns {{ operationId?: string; params: Record<string, string>; query: Record<string, string>; body?: any; headers: Record<string, string>; }}
174+
*/
175+
export function validateRequest(request, context) {
176+
return new RequestError(404, 'no operation match path');
177+
}
178+
export class RequestError extends Error {
179+
/** @param {number} code HTTP code for the error
180+
@param {string} message The error message*/
181+
constructor(code, message) {
182+
super(message);
183+
/** @type {number} HTTP code for the error*/
184+
this.code = code;
185+
}
186+
}
187+
export class ValidationError extends RequestError {
188+
/** @param {string[]} path The path that failed validation
189+
@param {string} message The error message*/
190+
constructor(path, message) {
191+
super(409, message);
192+
/** @type {string[]} The path that failed validation*/
193+
this.path = path;
194+
}
195+
}
196+
function obj0(path, value, context) {
197+
if (typeof value === 'string') {
198+
value = Number(value);
199+
}
200+
if (typeof value !== 'number' || Number.isNaN(value)) {
201+
return new ValidationError(path, 'expected a number');
202+
}
203+
if (value <= 10) {
204+
return new ValidationError(path, 'value less than minimum');
205+
}
206+
return value;
207+
}"
208+
`;
209+
42210
exports[`Integer basic 1`] = `
43211
"/**
44212
Validate a request against the OpenAPI spec

src/tests/compileValueSchema.test.ts

+38
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,44 @@ describe('Number', () => {
1010
});
1111
expect(compiler.compile()).toMatchSnapshot();
1212
});
13+
14+
test('maximum', () => {
15+
const compiler = new Compiler();
16+
compileValueSchema(compiler, {
17+
type: 'number',
18+
maximum: 10,
19+
});
20+
expect(compiler.compile()).toMatchSnapshot();
21+
});
22+
23+
test('maximum exclusiveMaximum', () => {
24+
const compiler = new Compiler();
25+
compileValueSchema(compiler, {
26+
type: 'number',
27+
maximum: 10,
28+
exclusiveMaximum: true,
29+
});
30+
expect(compiler.compile()).toMatchSnapshot();
31+
});
32+
33+
test('minimum', () => {
34+
const compiler = new Compiler();
35+
compileValueSchema(compiler, {
36+
type: 'number',
37+
minimum: 10,
38+
});
39+
expect(compiler.compile()).toMatchSnapshot();
40+
});
41+
42+
test('minimim exclusiveMinimum', () => {
43+
const compiler = new Compiler();
44+
compileValueSchema(compiler, {
45+
type: 'number',
46+
minimum: 10,
47+
exclusiveMinimum: true,
48+
});
49+
expect(compiler.compile()).toMatchSnapshot();
50+
});
1351
});
1452

1553
describe('Integer', () => {

src/types.ts

+16-2
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,25 @@ export interface OpenAPIStringSchema extends OpenAPINullableSchema, OpenAPIEnuma
7070
pattern?: string;
7171
}
7272

73-
export interface OpenAPINumberSchema extends OpenAPINullableSchema, OpenAPIEnumableSchema {
73+
interface CommonNumberSchema {
74+
maximum?: number;
75+
minimum?: number;
76+
exclusiveMinimum?: boolean;
77+
exclusiveMaximum?: boolean;
78+
}
79+
export interface OpenAPINumberSchema
80+
extends CommonNumberSchema,
81+
OpenAPINullableSchema,
82+
OpenAPIEnumableSchema {
7483
type: 'number';
84+
maximum?: number;
85+
minimum?: number;
7586
}
7687

77-
export interface OpenAPIIntegerSchema extends OpenAPINullableSchema, OpenAPIEnumableSchema {
88+
export interface OpenAPIIntegerSchema
89+
extends CommonNumberSchema,
90+
OpenAPINullableSchema,
91+
OpenAPIEnumableSchema {
7892
type: 'integer';
7993
format?: 'int32';
8094
}

tests/gitbook.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@
6767
"in": "query",
6868
"description": "The number of results per page",
6969
"schema": {
70-
"type": "number"
70+
"type": "number",
71+
"maximum": 100,
72+
"minimum": 0
7173
}
7274
},
7375
"listPage": {

tests/gitbook.test.ts

+28
Original file line numberDiff line numberDiff line change
@@ -318,3 +318,31 @@ test('GET spaces/space_iphone-doc/revisions/somerevision/files?metadata=true', (
318318
},
319319
});
320320
});
321+
322+
test('GET spaces/space_iphone-doc/revisions/somerevision/files?limit=1000 (invalid, number above maximum)', () => {
323+
const result = validateRequest({
324+
path: '/spaces/space_iphone-doc/revisions/somerevision/files',
325+
method: 'get',
326+
headers: {
327+
'content-type': 'application/json',
328+
},
329+
query: {
330+
limit: '1000',
331+
},
332+
});
333+
expect(result instanceof ValidationError ? result.path : null).toEqual(['query', 'limit']);
334+
});
335+
336+
test('GET spaces/space_iphone-doc/revisions/somerevision/files?limit=-1 (invalid, number below minimum)', () => {
337+
const result = validateRequest({
338+
path: '/spaces/space_iphone-doc/revisions/somerevision/files',
339+
method: 'get',
340+
headers: {
341+
'content-type': 'application/json',
342+
},
343+
query: {
344+
limit: '-1',
345+
},
346+
});
347+
expect(result instanceof ValidationError ? result.path : null).toEqual(['query', 'limit']);
348+
});

0 commit comments

Comments
 (0)