swagger-client
Version:
SwaggerJS - a collection of interfaces for OAI specs
365 lines (351 loc) • 11.5 kB
JavaScript
import lib from './lib/index.js';
import refs from './lib/refs.js';
import allOf from './lib/all-of.js';
import parameters from './lib/parameters.js';
import properties from './lib/properties.js';
import ContextTree from './lib/context-tree.js';
import { TRAVERSE_LIMIT } from '../../constants.js';
const PLUGIN_DISPATCH_LIMIT = 100;
const noop = () => {};
class SpecMap {
static getPluginName(plugin) {
return plugin.pluginName;
}
static getPatchesOfType(patches, fn) {
return patches.filter(fn);
}
constructor(opts) {
Object.assign(this, {
spec: '',
debugLevel: 'info',
plugins: [],
pluginHistory: {},
errors: [],
mutations: [],
promisedPatches: [],
state: {},
patches: [],
context: {},
contextTree: new ContextTree(),
showDebug: false,
allPatches: [],
// only populated if showDebug is true
pluginProp: 'specMap',
libMethods: Object.assign(Object.create(this), lib, {
getInstance: () => this
}),
allowMetaPatches: false
}, opts);
// Lib methods bound
this.get = this._get.bind(this); // eslint-disable-line no-underscore-dangle
this.getContext = this._getContext.bind(this); // eslint-disable-line no-underscore-dangle
this.hasRun = this._hasRun.bind(this); // eslint-disable-line no-underscore-dangle
this.wrappedPlugins = this.plugins.map(this.wrapPlugin.bind(this)).filter(lib.isFunction);
// Initial patch(s)
this.patches.push(lib.add([], this.spec));
this.patches.push(lib.context([], this.context));
this.updatePatches(this.patches);
}
debug(level, ...args) {
if (this.debugLevel === level) {
console.log(...args); // eslint-disable-line no-console
}
}
verbose(header, ...args) {
if (this.debugLevel === 'verbose') {
console.log(`[${header}] `, ...args); // eslint-disable-line no-console
}
}
wrapPlugin(plugin, name) {
const {
pathDiscriminator
} = this;
let ctx = null;
let fn;
if (plugin[this.pluginProp]) {
ctx = plugin;
fn = plugin[this.pluginProp];
} else if (lib.isFunction(plugin)) {
fn = plugin;
} else if (lib.isObject(plugin)) {
fn = createKeyBasedPlugin(plugin);
}
return Object.assign(fn.bind(ctx), {
pluginName: plugin.name || name,
isGenerator: lib.isGenerator(fn)
});
// Expected plugin interface: {key: string, plugin: fn*}
// This traverses depth-first and immediately applies yielded patches.
// This strategy should work well for most plugins (including the built-ins).
// We might consider making this (traversing & application) configurable later.
function createKeyBasedPlugin(pluginObj) {
const isSubPath = (path, tested) => {
if (!Array.isArray(path)) {
return true;
}
return path.every((val, i) => val === tested[i]);
};
return function* generator(patches, specmap) {
const refCache = {};
// eslint-disable-next-line no-restricted-syntax
for (const [i, patch] of patches.filter(lib.isAdditiveMutation).entries()) {
if (i < TRAVERSE_LIMIT) {
yield* traverse(patch.value, patch.path, patch);
} else {
return;
}
}
function* traverse(obj, path, patch) {
if (!lib.isObject(obj)) {
if (pluginObj.key === path[path.length - 1]) {
yield pluginObj.plugin(obj, pluginObj.key, path, specmap);
}
} else {
const parentIndex = path.length - 1;
const parent = path[parentIndex];
const indexOfFirstProperties = path.indexOf('properties');
const isRootProperties = parent === 'properties' && parentIndex === indexOfFirstProperties;
const traversed = specmap.allowMetaPatches && refCache[obj.$$ref];
// eslint-disable-next-line no-restricted-syntax
for (const key of Object.keys(obj)) {
const val = obj[key];
const updatedPath = path.concat(key);
const isObj = lib.isObject(val);
const objRef = obj.$$ref;
if (!traversed) {
if (isObj) {
// Only store the ref if it exists
if (specmap.allowMetaPatches && objRef && isSubPath(pathDiscriminator, updatedPath)) {
refCache[objRef] = true;
}
yield* traverse(val, updatedPath, patch);
}
}
if (!isRootProperties && key === pluginObj.key) {
const isWithinPathDiscriminator = isSubPath(pathDiscriminator, path);
if (!pathDiscriminator || isWithinPathDiscriminator) {
yield pluginObj.plugin(val, key, updatedPath, specmap, patch);
}
}
}
}
}
};
}
}
nextPlugin() {
return this.wrappedPlugins.find(plugin => {
const mutations = this.getMutationsForPlugin(plugin);
return mutations.length > 0;
});
}
nextPromisedPatch() {
if (this.promisedPatches.length > 0) {
return Promise.race(this.promisedPatches.map(patch => patch.value));
}
return undefined;
}
getPluginHistory(plugin) {
const name = this.constructor.getPluginName(plugin);
return this.pluginHistory[name] || [];
}
getPluginRunCount(plugin) {
return this.getPluginHistory(plugin).length;
}
getPluginHistoryTip(plugin) {
const history = this.getPluginHistory(plugin);
const val = history && history[history.length - 1];
return val || {};
}
getPluginMutationIndex(plugin) {
const mi = this.getPluginHistoryTip(plugin).mutationIndex;
return typeof mi !== 'number' ? -1 : mi;
}
updatePluginHistory(plugin, val) {
const name = this.constructor.getPluginName(plugin);
this.pluginHistory[name] = this.pluginHistory[name] || [];
this.pluginHistory[name].push(val);
}
updatePatches(patches) {
lib.normalizeArray(patches).forEach(patch => {
if (patch instanceof Error) {
this.errors.push(patch);
return;
}
try {
if (!lib.isObject(patch)) {
this.debug('updatePatches', 'Got a non-object patch', patch);
return;
}
if (this.showDebug) {
this.allPatches.push(patch);
}
if (lib.isPromise(patch.value)) {
this.promisedPatches.push(patch);
this.promisedPatchThen(patch);
return;
}
if (lib.isContextPatch(patch)) {
this.setContext(patch.path, patch.value);
return;
}
if (lib.isMutation(patch)) {
this.updateMutations(patch);
}
} catch (e) {
console.error(e); // eslint-disable-line no-console
this.errors.push(e);
}
});
}
updateMutations(patch) {
if (typeof patch.value === 'object' && !Array.isArray(patch.value) && this.allowMetaPatches) {
patch.value = {
...patch.value
};
}
const result = lib.applyPatch(this.state, patch, {
allowMetaPatches: this.allowMetaPatches
});
if (result) {
this.mutations.push(patch);
this.state = result;
}
}
removePromisedPatch(patch) {
const index = this.promisedPatches.indexOf(patch);
if (index < 0) {
this.debug("Tried to remove a promisedPatch that isn't there!");
return;
}
this.promisedPatches.splice(index, 1);
}
promisedPatchThen(patch) {
patch.value = patch.value.then(val => {
const promisedPatch = {
...patch,
value: val
};
this.removePromisedPatch(patch);
this.updatePatches(promisedPatch);
}).catch(e => {
this.removePromisedPatch(patch);
this.updatePatches(e);
});
return patch.value;
}
getMutations(from, to) {
from = from || 0;
if (typeof to !== 'number') {
to = this.mutations.length;
}
return this.mutations.slice(from, to);
}
getCurrentMutations() {
return this.getMutationsForPlugin(this.getCurrentPlugin());
}
getMutationsForPlugin(plugin) {
const tip = this.getPluginMutationIndex(plugin);
return this.getMutations(tip + 1);
}
getCurrentPlugin() {
return this.currentPlugin;
}
getLib() {
return this.libMethods;
}
// eslint-disable-next-line no-underscore-dangle
_get(path) {
return lib.getIn(this.state, path);
}
// eslint-disable-next-line no-underscore-dangle
_getContext(path) {
return this.contextTree.get(path);
}
setContext(path, value) {
return this.contextTree.set(path, value);
}
// eslint-disable-next-line no-underscore-dangle
_hasRun(count) {
const times = this.getPluginRunCount(this.getCurrentPlugin());
return times > (count || 0);
}
dispatch() {
const that = this;
const plugin = this.nextPlugin();
if (!plugin) {
const nextPromise = this.nextPromisedPatch();
if (nextPromise) {
return nextPromise.then(() => this.dispatch()).catch(() => this.dispatch());
}
// We're done!
const result = {
spec: this.state,
errors: this.errors
};
if (this.showDebug) {
result.patches = this.allPatches;
}
return Promise.resolve(result);
}
// Makes sure plugin isn't running an endless loop
that.pluginCount = that.pluginCount || new WeakMap();
that.pluginCount.set(plugin, (that.pluginCount.get(plugin) || 0) + 1);
if (that.pluginCount[plugin] > PLUGIN_DISPATCH_LIMIT) {
return Promise.resolve({
spec: that.state,
errors: that.errors.concat(new Error(`We've reached a hard limit of ${PLUGIN_DISPATCH_LIMIT} plugin runs`))
});
}
// A different plugin runs, wait for all promises to resolve, then retry
if (plugin !== this.currentPlugin && this.promisedPatches.length) {
const promises = this.promisedPatches.map(p => p.value);
// Waits for all to settle instead of Promise.all which stops on rejection
return Promise.all(promises.map(promise => promise.then(noop, noop))).then(() => this.dispatch());
}
// Ok, run the plugin
return executePlugin();
function executePlugin() {
that.currentPlugin = plugin;
const mutations = that.getCurrentMutations();
const lastMutationIndex = that.mutations.length - 1;
try {
if (plugin.isGenerator) {
// eslint-disable-next-line no-restricted-syntax
for (const yieldedPatches of plugin(mutations, that.getLib())) {
updatePatches(yieldedPatches);
}
} else {
const newPatches = plugin(mutations, that.getLib());
updatePatches(newPatches);
}
} catch (e) {
console.error(e); // eslint-disable-line no-console
updatePatches([Object.assign(Object.create(e), {
plugin
})]);
} finally {
that.updatePluginHistory(plugin, {
mutationIndex: lastMutationIndex
});
}
return that.dispatch();
}
function updatePatches(patches) {
if (patches) {
patches = lib.fullyNormalizeArray(patches);
that.updatePatches(patches, plugin);
}
}
}
}
export default function mapSpec(opts) {
return new SpecMap(opts).dispatch();
}
const plugins = {
refs,
allOf,
parameters,
properties
};
export { SpecMap, plugins };