Skip to content
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

feat: adding support for OpenAPI 3.1 #187

Merged
merged 9 commits into from
Oct 15, 2021
14 changes: 12 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,22 @@ SwaggerParser.prototype.parse = async function (path, api, options, callback) {
}
}
else {
let supportedVersions = ["3.0.0", "3.0.1", "3.0.2", "3.0.3"];
let supportedVersions = ["3.0.0", "3.0.1", "3.0.2", "3.0.3", "3.1.0"];

// Verify that the parsed object is a Openapi API
if (schema.openapi === undefined || schema.info === undefined || schema.paths === undefined) {
if (schema.openapi === undefined || schema.info === undefined) {
throw ono.syntax(`${args.path || args.schema} is not a valid Openapi API definition`);
}
else if (schema.paths === undefined) {
if (schema.openapi === "3.1.0") {
if (schema.webhooks === undefined) {
throw ono.syntax(`${args.path || args.schema} is not a valid Openapi API definition`);
}
}
else {
throw ono.syntax(`${args.path || args.schema} is not a valid Openapi API definition`);
}
}
else if (typeof schema.openapi === "number") {
// This is a very common mistake, so give a helpful error message
throw ono.syntax('Openapi version number must be a string (e.g. "3.0.0") not a number.');
Expand Down
28 changes: 15 additions & 13 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,22 @@ function fixOasRelativeServers (schema, filePath) {
schema.servers.map(server => fixServers(server, filePath)); // Root level servers array's fixup
}

// Path or Operation level servers array's fixup
Object.keys(schema.paths).forEach(path => {
const pathItem = schema.paths[path];
Object.keys(pathItem).forEach(opItem => {
if (opItem === "servers") {
// servers at pathitem level
pathItem[opItem].map(server => fixServers(server, filePath));
}
else if (operationsList.includes(opItem)) {
// servers at operation level
if (pathItem[opItem].servers) {
pathItem[opItem].servers.map(server => fixServers(server, filePath));
// Path, Operation, or Webhook level servers array's fixup
["paths", "webhooks"].forEach(component => {
Object.keys(schema[component] || []).forEach(path => {
const pathItem = schema[component][path];
Object.keys(pathItem).forEach(opItem => {
if (opItem === "servers") {
// servers at pathitem level
pathItem[opItem].map(server => fixServers(server, filePath));
}
}
else if (operationsList.includes(opItem)) {
// servers at operation level
if (pathItem[opItem].servers) {
pathItem[opItem].servers.map(server => fixServers(server, filePath));
}
}
});
});
});
}
Expand Down
89 changes: 56 additions & 33 deletions lib/validators/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,69 +2,92 @@

const util = require("../util");
const { ono } = require("@jsdevtools/ono");
const ZSchema = require("z-schema");
const AjvDraft4 = require("ajv-draft-04");
const Ajv = require("ajv/dist/2020");
const { openapi } = require("@apidevtools/openapi-schemas");

module.exports = validateSchema;

let zSchema = initializeZSchema();

/**
* Validates the given Swagger API against the Swagger 2.0 or 3.0 schema.
* Validates the given Swagger API against the Swagger 2.0 or OpenAPI 3.0 and 3.1 schemas.
*
* @param {SwaggerObject} api
*/
function validateSchema (api) {
let ajv;

// Choose the appropriate schema (Swagger or OpenAPI)
let schema = api.swagger ? openapi.v2 : openapi.v3;
let schema;

// Validate against the schema
let isValid = zSchema.validate(api, schema);
if (api.swagger) {
schema = openapi.v2;
ajv = initializeAjv();
}
else {
if (api.openapi.startsWith("3.1")) {
schema = openapi.v31;

// There's a bug with Ajv in how it handles `$dynamicRef` in the way that it's used within the 3.1 schema so we
// need to do some adhoc workarounds.
// https://github.com/OAI/OpenAPI-Specification/issues/2689
// https://github.com/ajv-validator/ajv/issues/1573
const schemaDynamicRef = schema.$defs.schema;
delete schemaDynamicRef.$dynamicAnchor;

schema.$defs.components.properties.schemas.additionalProperties = schemaDynamicRef;
schema.$defs.header.dependentSchemas.schema.properties.schema = schemaDynamicRef;
schema.$defs["media-type"].properties.schema = schemaDynamicRef;
schema.$defs.parameter.properties.schema = schemaDynamicRef;

ajv = initializeAjv(false);
}
else {
schema = openapi.v3;
ajv = initializeAjv();
}
}

// Validate against the schema
let isValid = ajv.validate(schema, api);
if (!isValid) {
let err = zSchema.getLastError();
let message = "Swagger schema validation failed. \n" + formatZSchemaError(err.details);
throw ono.syntax(err, { details: err.details }, message);
let err = ajv.errors;
let message = "Swagger schema validation failed.\n" + formatAjvError(err);
throw ono.syntax(err, { details: err }, message);
}
}

/**
* Performs one-time initialization logic to prepare for Swagger Schema validation.
* Determines which version of Ajv to load and prepares it for use.
*
* @param {bool} draft04
* @returns {Ajv}
*/
function initializeZSchema () {
// HACK: Delete the OpenAPI schema IDs because ZSchema can't resolve them
delete openapi.v2.id;
delete openapi.v3.id;
function initializeAjv (draft04 = true) {
const opts = {
allErrors: false,
strict: false,
validateFormats: false,
};

// The OpenAPI 3.0 schema uses "uri-reference" formats.
// Assume that any non-whitespace string is valid.
ZSchema.registerFormat("uri-reference", (value) => value.trim().length > 0);
if (draft04) {
return new AjvDraft4(opts);
}

// Configure ZSchema
return new ZSchema({
breakOnFirstError: true,
noExtraKeywords: true,
ignoreUnknownFormats: false,
reportPathAsArray: true
});
return new Ajv(opts);
}

/**
* Z-Schema validation errors are a nested tree structure.
* This function crawls that tree and builds an error message string.
* Run through a set of Ajv errors and compile them into an error message string.
*
* @param {object[]} errors - The Z-Schema error details
* @param {object[]} errors - The Ajv errors
* @param {string} [indent] - The whitespace used to indent the error message
* @returns {string}
*/
function formatZSchemaError (errors, indent) {
function formatAjvError (errors, indent) {
indent = indent || " ";
let message = "";
for (let error of errors) {
message += util.format(`${indent}${error.message} at #/${error.path.join("/")}\n`);
if (error.inner) {
message += formatZSchemaError(error.inner, indent + " ");
}
message += util.format(`${indent}#${error.instancePath.length ? error.instancePath : "/"} ${error.message}\n`);
}
return message;
}
Loading