openapi-enforcer
Advanced tools
Comparing version 1.10.8 to 1.11.0
@@ -10,2 +10,16 @@ # Change Log | ||
## 1.11.0 | ||
### Added | ||
- **Definition Bundler** | ||
Created a custom-built bundler that bundles and references nodes in a format that follows the OpenAPI specification. | ||
### Fixed | ||
- **Custom Ref Parser** | ||
The custom ref parser had a bug where is failed to check for null values when processing objects. That has been resolved. | ||
## 1.10.8 | ||
@@ -12,0 +26,0 @@ |
@@ -124,2 +124,18 @@ --- | ||
## Enforcer.bundle | ||
`Enforcer.bundle ( definition ) : Promise <object>` | ||
Resolves all of the `$ref` values in a definition, reorganizes `$ref` values to meet the OpenAPI specification, and returns an object that can be stringified with `JSON.stringify`. | ||
| Parameter | Description | Type | Default | | ||
| --------- | ----------- | ---- | ------- | | ||
| **definition** | A `string` for the file path to the OpenAPI definition or an `object` to bundle. | `string` or `object` | | | ||
**Parameters:** | ||
- *definition* - A `string` for the file path to the OpenAPI definition or an `object` to bundle. | ||
**Returns:** A Promise that resolves to the bundled `object`. | ||
## Enforcer.dereference | ||
@@ -126,0 +142,0 @@ |
@@ -0,0 +0,0 @@ ;(function () { |
10
index.js
@@ -92,2 +92,12 @@ /** | ||
Enforcer.bundle = function (definition) { | ||
if (Enforcer.config.useNewRefParser) { | ||
const refParser = new NewRefParser(definition); | ||
return refParser.bundle(); | ||
} else { | ||
const refParser = new OldRefParser(); | ||
return refParser.bundle(definition); | ||
} | ||
}; | ||
Enforcer.dereference = function (definition) { | ||
@@ -94,0 +104,0 @@ if (Enforcer.config.useNewRefParser) { |
{ | ||
"name": "openapi-enforcer", | ||
"version": "1.10.8", | ||
"version": "1.11.0", | ||
"description": "Library for validating, parsing, and formatting data against open api schemas.", | ||
@@ -48,13 +48,13 @@ "main": "index.js", | ||
"devDependencies": { | ||
"@gi60s/markdown-docs": "0.0.10", | ||
"@gi60s/markdown-docs": "0.1.2", | ||
"chai": "^4.2.0", | ||
"chokidar-cli": "^1.2.2", | ||
"coveralls": "^3.0.2", | ||
"chokidar-cli": "^1.2.3", | ||
"coveralls": "^3.1.0", | ||
"mocha": "^5.2.0", | ||
"nyc": "^14.1.0" | ||
"nyc": "^14.1.1" | ||
}, | ||
"dependencies": { | ||
"axios": "^0.19.2", | ||
"json-schema-ref-parser": "^6.0.1" | ||
"json-schema-ref-parser": "^6.1.0" | ||
} | ||
} |
@@ -63,5 +63,70 @@ /** | ||
RefParser.prototype.bundle = async function () { | ||
const that = map.get(this); | ||
if (that.bundled) { | ||
const { value, error, warning } = that.bundled; | ||
return new Result(value, error, warning); | ||
} | ||
const exception = new Exception('Unable to bundle definition for one or more reasons'); | ||
const [ dereferenced, error ] = await this.dereference(); | ||
const bundled = util.copy(dereferenced) | ||
if (error) { | ||
exception.push(error); | ||
} else { | ||
const map = mapNodesAndPaths(bundled, null, '', '#', []) | ||
const duplicates = Array.from(map.keys()) | ||
.map(key => { | ||
const data = map.get(key) | ||
return { node: key, refs: data } | ||
}) | ||
.filter(v => v.refs.length > 1) | ||
// get the optimal references | ||
const version = getVersion(bundled) | ||
const priorities = version === 2 | ||
? ['definitions', 'parameters', 'responses', 'securityDefinitions', 'security', 'tags', 'externalDocs'] | ||
: ['components/schemas', 'components/responses', 'components/parameters', 'components/examples', 'components/requestBodies', 'components/headers', 'components/securitySchemes', 'components/links', 'components/callbacks', 'components', 'security', 'servers', 'tags', 'externalDocs'] | ||
duplicates.forEach(dup => { | ||
const refs = dup.refs | ||
refs.sort((a, b) => { | ||
const pathA = a.path | ||
const pathB = b.path | ||
let priorityA = priorities.findIndex(p => pathA.startsWith('#/' + p)) | ||
let priorityB = priorities.findIndex(p => pathB.startsWith('#/' + p)) | ||
if (priorityA === -1) priorityA = Number.MAX_SAFE_INTEGER | ||
if (priorityB === -1) priorityB = Number.MAX_SAFE_INTEGER | ||
if (priorityA < priorityB) { | ||
return -1; | ||
} else if (priorityA > priorityB) { | ||
return 1; | ||
} else { | ||
return pathA.split('/').length < pathB.split('/').length ? -1 : 1; | ||
} | ||
}) | ||
dup.ref = refs[0] | ||
}) | ||
duplicates.forEach(dup => { | ||
const refs = dup.refs; | ||
const length = refs.length; | ||
for (let i = 1; i < length; i++) { | ||
const ref = refs[i] | ||
ref.parent[ref.key] = dup.ref.path | ||
} | ||
}) | ||
} | ||
that.bundled = new Result(bundled, exception); | ||
return that.bundled; | ||
} | ||
RefParser.prototype.dereference = async function () { | ||
const that = map.get(this); | ||
if (that.dereferenced) return that.dereferenced; | ||
if (that.dereferenced) { | ||
const { value, error, warning } = that.dereferenced | ||
return new Result(value, error, warning) | ||
} | ||
@@ -101,3 +166,4 @@ const exception = new Exception('Unable to dereference definition for one or more reasons'); | ||
const keys = Object.keys(sourceMap); | ||
for (let i = 0; i < keys.length; i++) { | ||
const length = keys.length; | ||
for (let i = 0; i < length; i++) { | ||
const key = keys[i]; | ||
@@ -153,2 +219,48 @@ if (sourceMap[key].includes(node)) return key; | ||
function mapNodesAndPaths (node, parent, key, path, chain, map = new Map()) { | ||
if (node && typeof node === 'object') { | ||
// if we're in an endless loop then exit | ||
if (chain.includes(node)) return | ||
// add to loop watching chain | ||
chain = chain.slice(); | ||
chain.push(node); | ||
const data = { | ||
key, | ||
parent, | ||
path | ||
} | ||
// store where this node resides in the tree | ||
const existing = map.get(node); | ||
if (existing) { | ||
existing.push(data); | ||
} else { | ||
map.set(node, [data]); | ||
} | ||
if (Array.isArray(node)) { | ||
node.forEach((n, i) => { | ||
mapNodesAndPaths(n, node, i, path + '/' + i, chain, map); | ||
}); | ||
} else { | ||
Object.keys(node).forEach(key => { | ||
mapNodesAndPaths(node[key], node, key, path + '/' + key, chain, map); | ||
}); | ||
} | ||
} | ||
return map; | ||
} | ||
function getVersion (spec) { | ||
if (spec) { | ||
if (spec.swagger) return 2; | ||
if (spec.openapi) { | ||
const v = spec.openapi.split('.')[0]; | ||
if (/^\d$/.test(v)) return +v; | ||
} | ||
} | ||
} | ||
async function parse (basePath, fullPath, source, value, that, map, chain, exception) { | ||
@@ -168,3 +280,3 @@ // console.log('Parse:\n B: ' + basePath + '\n S: ' + Object.keys(source)); | ||
} else if (typeof value === 'object') { | ||
} else if (value && typeof value === 'object') { | ||
if (value.hasOwnProperty('$ref')) { | ||
@@ -171,0 +283,0 @@ const infiniteLoop = chain.includes(value); |
@@ -336,2 +336,53 @@ const expect = require('chai').expect; | ||
describe('bundle', () => { | ||
it('can bundle multiple files', async function () { | ||
const parser = new RefParser(path.resolve(resourcesDir, 'Bundle1.yml')); | ||
const [ bundled ] = await parser.bundle(); | ||
expect(bundled.b.a).to.equal('#/a'); | ||
expect(bundled.c.a).to.equal('#/a'); | ||
expect(bundled.c.b).to.equal('#/b'); | ||
expect(bundled.d.c).to.equal('#/c/c'); | ||
}) | ||
// this test proves that bundling will prioritize references | ||
// to follow the OpenAPI specification | ||
it('will smartly reference duplicates', async function () { | ||
const doc = { | ||
openapi: '3.0.0', | ||
info: { title: '', version: '' }, | ||
paths: { | ||
'/': { | ||
responses: { | ||
'200': { | ||
description: '', | ||
content: { | ||
'application/json': { | ||
schema: { | ||
$ref: '#/components/schemas/x' | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
components: { | ||
schemas: { | ||
x: { | ||
$ref: '#/schemas/x' | ||
} | ||
} | ||
}, | ||
schemas: { | ||
x: { | ||
type: 'string' | ||
} | ||
} | ||
} | ||
const parser = new RefParser(doc); | ||
const [ bundled ] = await parser.bundle(); | ||
expect(bundled.components.schemas.x).to.deep.equal({ type: 'string' }) | ||
}); | ||
}) | ||
}); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
1476788
176
20084