@seriousme/openapi-schema-validator
Advanced tools
Comparing version 1.0.0 to 1.0.1
@@ -6,3 +6,9 @@ # Changelog | ||
## [v1.0.0] 30-05-2021 | ||
## [v1.0.1] 09-05-2021 | ||
### Changed | ||
- Updated dependencies | ||
- Reworked README | ||
- Applied LGTM suggestions | ||
## [v1.0.0] 09-05-2021 | ||
Initial version |
146
index.js
@@ -1,94 +0,90 @@ | ||
import Ajv from 'ajv'; | ||
import Ajv2020 from 'ajv/dist/2020.js'; | ||
import { createRequire } from 'module'; | ||
import JSYaml from 'js-yaml'; | ||
import { readFile, stat } from 'fs/promises' | ||
import { resolve } from './resolve.js' | ||
import Ajv from "ajv"; | ||
import Ajv2020 from "ajv/dist/2020.js"; | ||
import { createRequire } from "module"; | ||
import JSYaml from "js-yaml"; | ||
import { readFile } from "fs/promises"; | ||
import { resolve } from "./resolve.js"; | ||
const importJSON = createRequire(import.meta.url); | ||
const openApiVersions = new Set(['2.0', '3.0', '3.1']); | ||
const openApiVersions = new Set(["2.0", "3.0", "3.1"]); | ||
const ajvVersions = { | ||
"http://json-schema.org/draft-07/schema": Ajv, | ||
"https://json-schema.org/draft/2020-12/schema": Ajv2020 | ||
} | ||
"http://json-schema.org/draft-07/schema": Ajv, | ||
"https://json-schema.org/draft/2020-12/schema": Ajv2020, | ||
}; | ||
const openApiSchemas = {}; | ||
function getOpenApiVersion(specification) { | ||
for (const version of openApiVersions) { | ||
const prop = specification[(version == "2.0") ? 'swagger' : 'openapi']; | ||
if (typeof prop === "string" && prop.startsWith(version)) { | ||
return version | ||
} | ||
for (const version of openApiVersions) { | ||
const prop = specification[version == "2.0" ? "swagger" : "openapi"]; | ||
if (typeof prop === "string" && prop.startsWith(version)) { | ||
return version; | ||
} | ||
return undefined | ||
} | ||
return undefined; | ||
} | ||
async function getSpecFromData(data) { | ||
if (typeof data === 'object') { | ||
return data; | ||
if (typeof data === "object") { | ||
return data; | ||
} | ||
if (typeof data === "string") { | ||
if (data.match(/\n/)) { | ||
try { | ||
return JSYaml.load(data); | ||
} catch (_) { | ||
return undefined; | ||
} | ||
} | ||
if (typeof data === 'string') { | ||
if (data.match(/\n/)) { | ||
try { | ||
return JSYaml.load(data); | ||
} | ||
catch (_) { | ||
return undefined | ||
}; | ||
} | ||
try { | ||
const fileData = await readFile(data, "utf-8"); | ||
return JSYaml.load(fileData); | ||
} catch (_) { | ||
return undefined; | ||
} | ||
try { | ||
const fileData = await readFile(data, "utf-8"); | ||
return JSYaml.load(fileData); | ||
} catch (_) { | ||
return undefined; | ||
} | ||
} | ||
} | ||
export default class Validator { | ||
constructor(ajvOptions = { strict: false, validateFormats: false }) { | ||
this.ajvOptions = ajvOptions; | ||
return this; | ||
} | ||
constructor(ajvOptions = { strict: false, validateFormats: false }) { | ||
this.ajvOptions = ajvOptions; | ||
return this; | ||
} | ||
resolveRefs(opts={}){ | ||
return resolve(this.specification || opts.specification) | ||
} | ||
resolveRefs(opts = {}) { | ||
return resolve(this.specification || opts.specification); | ||
} | ||
static supportedVersions = openApiVersions; | ||
static supportedVersions = openApiVersions; | ||
async validate(data) { | ||
const specification = await getSpecFromData(data); | ||
this.specification = specification; | ||
if (specification === undefined || specification === null) { | ||
return { | ||
valid: false, | ||
errors: "Cannot find JSON, YAML or filename in data" | ||
}; | ||
} | ||
const version = getOpenApiVersion(specification); | ||
this.version = version; | ||
if (!version) { | ||
return { | ||
valid: false, | ||
errors: "Cannot find supported swagger/openapi version in specification, version must be a string." | ||
}; | ||
} | ||
const schema = await importJSON(`./schemas/v${version}/schema.json`); | ||
const schemaVersion = schema.$schema; | ||
const AjvClass = ajvVersions[schemaVersion]; | ||
const ajv = new AjvClass(this.ajvOptions); | ||
const validate = ajv.compile(schema); | ||
const result = { | ||
valid: validate(specification) | ||
}; | ||
if (validate.errors) { | ||
result.errors = validate.errors | ||
} | ||
return result | ||
async validate(data) { | ||
const specification = await getSpecFromData(data); | ||
this.specification = specification; | ||
if (specification === undefined || specification === null) { | ||
return { | ||
valid: false, | ||
errors: "Cannot find JSON, YAML or filename in data", | ||
}; | ||
} | ||
} | ||
const version = getOpenApiVersion(specification); | ||
this.version = version; | ||
if (!version) { | ||
return { | ||
valid: false, | ||
errors: | ||
"Cannot find supported swagger/openapi version in specification, version must be a string.", | ||
}; | ||
} | ||
const schema = await importJSON(`./schemas/v${version}/schema.json`); | ||
const schemaVersion = schema.$schema; | ||
const AjvClass = ajvVersions[schemaVersion]; | ||
const ajv = new AjvClass(this.ajvOptions); | ||
const validate = ajv.compile(schema); | ||
const result = { | ||
valid: validate(specification), | ||
}; | ||
if (validate.errors) { | ||
result.errors = validate.errors; | ||
} | ||
return result; | ||
} | ||
} |
{ | ||
"name": "@seriousme/openapi-schema-validator", | ||
"version": "1.0.0", | ||
"version": "1.0.1", | ||
"description": "Validate OpenApi specifications against their JSON schema", | ||
@@ -8,4 +8,3 @@ "main": "index.js", | ||
"dependencies": { | ||
"ajv": "^8.2.0", | ||
"ajv-formats": "^2.0.2", | ||
"ajv": "^8.3.0", | ||
"js-yaml": "^4.1.0" | ||
@@ -23,3 +22,3 @@ }, | ||
"c8": "^7.7.2", | ||
"tap": "^15.0.6" | ||
"tap": "^15.0.9" | ||
}, | ||
@@ -26,0 +25,0 @@ "directories": { |
@@ -5,3 +5,3 @@ # OpenAPI schema validator | ||
[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/seriousme/openapi-schema-validator.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/seriousme/openapi-schema-validator/context:javascript) | ||
[![NPM version](https://img.shields.io/npm/v/openapi-schema-validator.svg)](https://www.npmjs.com/package/@seriousme/openapi-schema-validator) | ||
[![NPM version](https://img.shields.io/npm/v/@seriousme/openapi-schema-validator.svg)](https://www.npmjs.com/package/seriousme/openapi-schema-validator) | ||
![npm](https://img.shields.io/npm/dm/@seriousme/openapi-schema-validator) | ||
@@ -46,6 +46,19 @@ | ||
### API | ||
- `<instance>.validate(specification)` | ||
- [`new Validator(ajvOptions)`](#newValidator) | ||
- [`<instance>.validate(specification)`](#validate) | ||
- [`<instance>.version`](#version) | ||
- [`<instance>.resolveRefs(options)`](#resolveRefs) | ||
- [`Validator.supportedVersions`](#supportedVersions) | ||
Thsi function tries to validata a specification against the OpenApi schemas. `specification` can be one of: | ||
<a name="newValidator"></a> | ||
### `new Validator(ajvOptions)` | ||
The constructor returns an instance of `Validator`. | ||
By passing an ajv options object it is possible to influence the behavior of the [AJV schema validator](https://ajv.js.org/). | ||
<a name="validate"></a> | ||
### `<instance>.validate(specification)` | ||
This function tries to validata a specification against the OpenApi schemas. `specification` can be one of: | ||
- a JSON object | ||
@@ -65,3 +78,4 @@ - a JSON object encoded as string | ||
- `<instance>.version` | ||
<a name="version"></a> | ||
### `<instance>.version` | ||
@@ -71,3 +85,4 @@ If validation is succesfull this will return the openApi version found e.g. ("2.0","3.0","3.1). | ||
- `<instance>.resolveRefs(options)` | ||
<a name="resolveRefs"></a> | ||
### `<instance>.resolveRefs(options)` | ||
@@ -79,3 +94,4 @@ This function tries to resolve all internal references. External references are *not* automatically resolved so you need to inline them yourself if required. By default it will use the last specification passed to `<instance>.validate()` | ||
- `Validator.supportedVersions` | ||
<a name="supportedVersions"></a> | ||
### `Validator.supportedVersions` | ||
@@ -86,2 +102,2 @@ This static property returns the OpenApi versions supported by this package as a `Set`. If present, the result of `<instance>.version` is a member of this `Set`. | ||
# License | ||
Licensed under the [MIT license](https://opensource.org/licenses/MIT) | ||
Licensed under the [MIT license](LICENSE.txt) |
189
resolve.js
@@ -1,116 +0,121 @@ | ||
import { load, dump } from "js-yaml"; | ||
function escapeJsonPointer(str) { | ||
return str.replace(/~/g, "~0").replace(/\//g, "~1"); | ||
return str.replace(/~/g, "~0").replace(/\//g, "~1"); | ||
} | ||
function unescapeJsonPointer(str) { | ||
return str.replace(/~1/g, "/").replace(/~0/g, "~"); | ||
return str.replace(/~1/g, "/").replace(/~0/g, "~"); | ||
} | ||
const isObject = obj => (typeof obj === "object" && obj !== null); | ||
const pointerWords = new Set(["$ref", "$id", "$anchor", "$dynamicRef", "$dynamicAnchor", "$schema"]); | ||
const isObject = (obj) => typeof obj === "object" && obj !== null; | ||
const pointerWords = new Set([ | ||
"$ref", | ||
"$id", | ||
"$anchor", | ||
"$dynamicRef", | ||
"$dynamicAnchor", | ||
"$schema", | ||
]); | ||
const filtered = raw => Object.fromEntries( | ||
Object.entries(raw).filter( | ||
([key, _]) => (!pointerWords.has(key)) | ||
) | ||
); | ||
const filtered = (raw) => | ||
Object.fromEntries( | ||
Object.entries(raw).filter(([key, _]) => !pointerWords.has(key)) | ||
); | ||
function resolveUri(uri, anchors) { | ||
const [prefix, path] = uri.split("#", 2) | ||
const err = new Error(`Can't resolve ${uri}`) | ||
if (path[0] !== "/") { | ||
if (anchors[uri]) { | ||
return anchors[uri]; | ||
} | ||
throw err; | ||
const [prefix, path] = uri.split("#", 2); | ||
const err = new Error(`Can't resolve ${uri}`); | ||
if (path[0] !== "/") { | ||
if (anchors[uri]) { | ||
return anchors[uri]; | ||
} | ||
throw err; | ||
} | ||
if (!anchors[prefix]) { | ||
throw err; | ||
} | ||
const paths = path.split("/").slice(1); | ||
try { | ||
const result = paths.reduce((o, n) => o[unescapeJsonPointer(n)], anchors[prefix]); | ||
return result; | ||
} catch (_) { | ||
throw err; | ||
} | ||
if (!anchors[prefix]) { | ||
throw err; | ||
} | ||
const paths = path.split("/").slice(1); | ||
try { | ||
const result = paths.reduce( | ||
(o, n) => o[unescapeJsonPointer(n)], | ||
anchors[prefix] | ||
); | ||
return result; | ||
} catch (_) { | ||
throw err; | ||
} | ||
} | ||
export function resolve(tree) { | ||
if (!isObject(tree)) { | ||
return undefined | ||
} | ||
if (!isObject(tree)) { | ||
return undefined; | ||
} | ||
const pointers = {}; | ||
pointerWords.forEach(word => pointers[word] = []); | ||
const pointers = {}; | ||
pointerWords.forEach((word) => (pointers[word] = [])); | ||
function parse(obj, path, id) { | ||
if (!isObject(obj)) { | ||
return | ||
} | ||
if (obj.$id) { | ||
id = obj.$id; | ||
} | ||
for (const prop in obj) { | ||
if (pointerWords.has(prop)) { | ||
pointers[prop].push({ ref: obj[prop], obj, prop, path, id }); | ||
} | ||
parse(obj[prop], `${path}/${escapeJsonPointer(prop)}`, id) | ||
} | ||
function parse(obj, path, id) { | ||
if (!isObject(obj)) { | ||
return; | ||
} | ||
// find all refs | ||
parse(tree, "#", ""); | ||
if (obj.$id) { | ||
id = obj.$id; | ||
} | ||
for (const prop in obj) { | ||
if (pointerWords.has(prop)) { | ||
pointers[prop].push({ ref: obj[prop], obj, prop, path, id }); | ||
} | ||
parse(obj[prop], `${path}/${escapeJsonPointer(prop)}`, id); | ||
} | ||
} | ||
// find all refs | ||
parse(tree, "#", ""); | ||
// resolve them | ||
const anchors = { "": tree }; | ||
const dynamicAnchors = {}; | ||
pointers.$id.forEach(item => { | ||
const { ref, obj, path } = item; | ||
if (anchors[ref]) { | ||
throw new Error(`$id : '${ref}' defined more than once at ${path}`); | ||
} | ||
anchors[ref] = obj | ||
}); | ||
// resolve them | ||
const anchors = { "": tree }; | ||
const dynamicAnchors = {}; | ||
pointers.$id.forEach((item) => { | ||
const { ref, obj, path } = item; | ||
if (anchors[ref]) { | ||
throw new Error(`$id : '${ref}' defined more than once at ${path}`); | ||
} | ||
anchors[ref] = obj; | ||
}); | ||
pointers.$anchor.forEach(item => { | ||
const { ref, obj, prop, path, id } = item; | ||
const fullRef = `${id}#${ref}`; | ||
if (anchors[fullRef]) { | ||
throw new Error(`$anchor : '${ref}' defined more than once at '${path}'`); | ||
} | ||
anchors[fullRef] = obj | ||
}); | ||
pointers.$anchor.forEach((item) => { | ||
const { ref, obj, path, id } = item; | ||
const fullRef = `${id}#${ref}`; | ||
if (anchors[fullRef]) { | ||
throw new Error(`$anchor : '${ref}' defined more than once at '${path}'`); | ||
} | ||
anchors[fullRef] = obj; | ||
}); | ||
pointers.$dynamicAnchor.forEach(item => { | ||
const { ref, obj, prop, path } = item; | ||
if (dynamicAnchors[`#${ref}`]) { | ||
throw new Error(`$dynamicAnchor : '${ref}' defined more than once at '${path}'`); | ||
} | ||
dynamicAnchors[`#${ref}`] = obj; | ||
}); | ||
pointers.$dynamicAnchor.forEach((item) => { | ||
const { ref, obj, path } = item; | ||
if (dynamicAnchors[`#${ref}`]) { | ||
throw new Error( | ||
`$dynamicAnchor : '${ref}' defined more than once at '${path}'` | ||
); | ||
} | ||
dynamicAnchors[`#${ref}`] = obj; | ||
}); | ||
pointers.$ref.forEach(item => { | ||
const { ref, obj, prop, path, id } = item; | ||
delete obj[prop] | ||
const fullRef = ref[0] !== "#" ? ref : `${id}${ref}`; | ||
const res = filtered(resolveUri(fullRef, anchors)) | ||
Object.assign(obj, filtered(resolveUri(fullRef, anchors))); | ||
}); | ||
pointers.$ref.forEach((item) => { | ||
const { ref, obj, prop, id } = item; | ||
delete obj[prop]; | ||
const fullRef = ref[0] !== "#" ? ref : `${id}${ref}`; | ||
Object.assign(obj, filtered(resolveUri(fullRef, anchors))); | ||
}); | ||
pointers.$dynamicRef.forEach(item => { | ||
const { ref, obj, prop, path, id } = item; | ||
if (!dynamicAnchors[ref]){ | ||
throw new Error(`Can't resolve $dynamicAnchor : '${ref}'`); | ||
} | ||
delete obj[prop] | ||
Object.assign(obj, filtered(dynamicAnchors[ref])); | ||
}); | ||
pointers.$dynamicRef.forEach((item) => { | ||
const { ref, obj, prop } = item; | ||
if (!dynamicAnchors[ref]) { | ||
throw new Error(`Can't resolve $dynamicAnchor : '${ref}'`); | ||
} | ||
delete obj[prop]; | ||
Object.assign(obj, filtered(dynamicAnchors[ref])); | ||
}); | ||
return tree; | ||
return tree; | ||
} | ||
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
157864
2
23
5482
98
- Removedajv-formats@^2.0.2
- Removedajv-formats@2.1.1(transitive)
Updatedajv@^8.3.0