Skip to content
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
49 changes: 49 additions & 0 deletions src/compileValueSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
OpenAPIOneOfSchema,
OpenAPIPropertyNamesSchema,
OpenAPIStringSchema,
OpenAPITypeArraySchema,
OpenAPIValueSchema,
} from './types';
import { ValidationErrorIdentifier } from './error';
Expand Down Expand Up @@ -52,6 +53,11 @@ export function compileValueSchema(compiler: Compiler, schema: OpenAPIValueSchem
}

if ('type' in schema) {
// OpenAPI 3.1: type can be an array, e.g. ["string", "null"]
if (Array.isArray(schema.type)) {
return compileTypeArraySchema(compiler, schema as OpenAPITypeArraySchema);
}

switch (schema.type) {
case 'object':
return compileObjectSchema(compiler, schema);
Expand All @@ -64,6 +70,8 @@ export function compileValueSchema(compiler: Compiler, schema: OpenAPIValueSchem
return compileBooleanSchema(compiler, schema);
case 'array':
return compileArraySchema(compiler, schema);
case 'null':
return compileNullTypeSchema(compiler, schema);
default:
throw new Error(`Unsupported schema: ${JSON.stringify(schema)}`);
}
Expand All @@ -83,6 +91,42 @@ function normalizePropertyNamesSchema(schema: OpenAPIPropertyNamesSchema): OpenA
};
}

/**
* OpenAPI 3.1: type: 'null' as a standalone type.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll have to test this one well, because I'm wondering if we actually get null as an input value.

*/
function compileNullTypeSchema(compiler: Compiler, schema: object) {
return compiler.declareValidationFunction(schema, ({ value, error }) => {
return [
builders.ifStatement(
builders.binaryExpression('!==', value, builders.literal(null)),
builders.blockStatement([builders.returnStatement(error('expected null'))]),
),
builders.returnStatement(value),
];
});
}

/**
* OpenAPI 3.1: type as array, e.g. type: ['string', 'null']
* Converts to an anyOf schema internally.
*/
function compileTypeArraySchema(compiler: Compiler, schema: OpenAPITypeArraySchema) {
const { type, ...rest } = schema;

const typesWithoutNull = type.filter((t) => t !== 'null');
const hasNull = type.includes('null');

const anyOf: OpenAPIValueSchema[] = typesWithoutNull.map(
(t) => ({ ...rest, type: t }) as OpenAPIValueSchema,
);

if (hasNull) {
anyOf.push({ type: 'null' } as OpenAPIValueSchema);
}

return compileAnyOfSchema(compiler, { anyOf });
}

function compileAnyOfSchema(compiler: Compiler, schema: OpenAPIAnyOfSchema) {
return compiler.declareValidationFunction(schema, ({ value, path, context, error }) => {
const nodes: namedTypes.BlockStatement['body'] = [];
Expand Down Expand Up @@ -646,6 +690,11 @@ function compileArraySchema(compiler: Compiler, schema: OpenAPIArraySchema) {
);
}

if (!schema.items) {
nodes.push(builders.returnStatement(value));
return nodes;
}

const valueSet = builders.identifier('valueSet');

if (schema.uniqueItems) {
Expand Down
303 changes: 303 additions & 0 deletions src/tests/__snapshots__/compileValueSchema.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1492,6 +1492,88 @@ function obj0(path, value, context) {
}"
`;

exports[`Array without items 1`] = `
"/**
Validate a request against the OpenAPI spec
@param {{ method: string; path: string; body?: any; query: Record<string, string | string[]>; headers: Record<string, string>; }} 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<string, string>; query: Record<string, string | string[]>; body?: any; headers: Record<string, string>; }}
*/
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.<string, <T>(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 (!Array.isArray(value)) {
return new ValidationError(path, 'expected an array');
}
return value;
}"
`;

exports[`Array without items and uniqueItems 1`] = `
"/**
Validate a request against the OpenAPI spec
@param {{ method: string; path: string; body?: any; query: Record<string, string | string[]>; headers: Record<string, string>; }} 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<string, string>; query: Record<string, string | string[]>; body?: any; headers: Record<string, string>; }}
*/
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.<string, <T>(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 (!Array.isArray(value)) {
return new ValidationError(path, 'expected an array');
}
return value;
}"
`;

exports[`anyOf 1`] = `
"/**
Validate a request against the OpenAPI spec
Expand Down Expand Up @@ -1732,3 +1814,224 @@ function obj0(path, value, context) {
return result;
}"
`;

exports[`OpenAPI 3.1 type: null 1`] = `
"/**
Validate a request against the OpenAPI spec
@param {{ method: string; path: string; body?: any; query: Record<string, string | string[]>; headers: Record<string, string>; }} 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<string, string>; query: Record<string, string | string[]>; body?: any; headers: Record<string, string>; }}
*/
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.<string, <T>(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 !== null) {
return new ValidationError(path, 'expected null');
}
return value;
}"
`;

exports[`OpenAPI 3.1 type array type: [string, null] 1`] = `
"/**
Validate a request against the OpenAPI spec
@param {{ method: string; path: string; body?: any; query: Record<string, string | string[]>; headers: Record<string, string>; }} 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<string, string>; query: Record<string, string | string[]>; body?: any; headers: Record<string, string>; }}
*/
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.<string, <T>(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 obj1(path, value, context) {
if (typeof value !== 'string') {
return new ValidationError(path, 'expected a string');
}
return value;
}
function obj2(path, value, context) {
if (value !== null) {
return new ValidationError(path, 'expected null');
}
return value;
}
function obj0(path, value, context) {
const value0 = obj1(path, value, context);
if (!(value0 instanceof ValidationError)) {
return value0;
}
const value1 = obj2(path, value, context);
if (!(value1 instanceof ValidationError)) {
return value1;
}
return new ValidationError(path, 'expected one of the anyOf schemas to match');
}"
`;

exports[`OpenAPI 3.1 type array type: [string, number] 1`] = `
"/**
Validate a request against the OpenAPI spec
@param {{ method: string; path: string; body?: any; query: Record<string, string | string[]>; headers: Record<string, string>; }} 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<string, string>; query: Record<string, string | string[]>; body?: any; headers: Record<string, string>; }}
*/
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.<string, <T>(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 obj1(path, value, context) {
if (typeof value !== 'string') {
return new ValidationError(path, 'expected a string');
}
return value;
}
function obj2(path, value, context) {
if (typeof value === 'string') {
value = Number(value);
}
if (typeof value !== 'number' || Number.isNaN(value)) {
return new ValidationError(path, 'expected a number');
}
return value;
}
function obj0(path, value, context) {
const value0 = obj1(path, value, context);
if (!(value0 instanceof ValidationError)) {
return value0;
}
const value1 = obj2(path, value, context);
if (!(value1 instanceof ValidationError)) {
return value1;
}
return new ValidationError(path, 'expected one of the anyOf schemas to match');
}"
`;

exports[`OpenAPI 3.1 type array type: [string, null] with minLength 1`] = `
"/**
Validate a request against the OpenAPI spec
@param {{ method: string; path: string; body?: any; query: Record<string, string | string[]>; headers: Record<string, string>; }} 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<string, string>; query: Record<string, string | string[]>; body?: any; headers: Record<string, string>; }}
*/
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.<string, <T>(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 obj1(path, value, context) {
if (typeof value !== 'string') {
return new ValidationError(path, 'expected a string');
}
if (value.length < 1) {
return new ValidationError(path, 'expected at least 1 characters');
}
return value;
}
function obj2(path, value, context) {
if (value !== null) {
return new ValidationError(path, 'expected null');
}
return value;
}
function obj0(path, value, context) {
const value0 = obj1(path, value, context);
if (!(value0 instanceof ValidationError)) {
return value0;
}
const value1 = obj2(path, value, context);
if (!(value1 instanceof ValidationError)) {
return value1;
}
return new ValidationError(path, 'expected one of the anyOf schemas to match');
}"
`;
Loading