hapi-swagger
Version:
A swagger documentation UI generator plugin for hapi
507 lines (438 loc) • 11.2 kB
JavaScript
const Hoek = require('@hapi/hoek');
const Joi = require('joi');
const utilities = (module.exports = {});
/**
* is passed item an object
*
* @param {Object} obj
* @return {Boolean}
*/
utilities.isObject = function (obj) {
return obj !== null && obj !== undefined && typeof obj === 'object' && !Array.isArray(obj);
};
/**
* is passed item a function
*
* @param {Object} obj
* @return {Boolean}
*/
utilities.isFunction = function (obj) {
// remove `obj.constructor` test as it was always true
return !!(obj && obj.call && obj.apply);
};
/**
* is passed item a regex
*
* @param {Object} obj
* @return {Boolean}
*/
utilities.isRegex = function (obj) {
// base on https://github.com/ljharb/is-regex/
// has a couple of edge use cases for different env - hence coverage:off
/* $lab:coverage:off$ */
const regexExec = RegExp.prototype.exec;
const tryRegexExec = function tryRegexExec(value) {
try {
regexExec.call(value);
return true;
} catch (e) {
return false;
}
};
const toStr = Object.prototype.toString;
const regexClass = '[object RegExp]';
const hasToStringTag = typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol';
if (typeof obj !== 'object') {
return false;
}
return hasToStringTag ? tryRegexExec(obj) : toStr.call(obj) === regexClass;
/* $lab:coverage:on$ */
};
/**
* does an object have any of its own properties
*
* @param {Object} obj
* @return {Boolean}
*/
utilities.hasProperties = function (obj) {
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
return true;
}
}
return false;
};
/**
* deletes any property in an object that is undefined, null or an empty array
*
* @param {Object} obj
* @return {Object}
*/
utilities.deleteEmptyProperties = function (obj) {
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
// delete properties undefined values
if (obj[key] === undefined) {
delete obj[key];
}
// allow blank objects for example or default properties
if (!['default', 'example', 'security'].includes(key)) {
if (obj[key] === null) {
delete obj[key];
}
// delete array with no values
if (Array.isArray(obj[key]) && obj[key].length === 0) {
delete obj[key];
}
// delete object which does not have its own properties
if (utilities.isObject(obj[key]) && utilities.hasProperties(obj[key]) === false) {
delete obj[key];
}
}
}
}
return obj;
};
/**
* gets first item of an array
*
* @param {Array} array
* @return {Object}
*/
utilities.first = function (array) {
return Array.isArray(array) ? array[0] : undefined;
};
/**
* sort array so it has a set firstItem
*
* @param {Array<string>} array
* @param {string} firstItem
* @return {Array}
*/
utilities.sortFirstItem = function (array, firstItem) {
const input = Hoek.clone(array);
if (!firstItem) {
return input;
}
const filteredInput = input.filter((item) => item !== firstItem);
return [firstItem, ...filteredInput];
};
/**
* replace a value in an array and keep order
*
* @param {Array} inputArray
* @param {Object} current
* @param {Object} replacement
* @return {Array}
*/
utilities.replaceValue = function (inputArray, current, replacement) {
if (!inputArray || !current || !replacement) {
return Hoek.clone(inputArray);
}
const array = Hoek.clone(inputArray);
const index = array.indexOf(current);
if (index !== -1) {
array.splice(index, 1, replacement);
}
return array;
};
/**
* does an object have a key
*
* @param {Object} obj
* @param {string} findKey
* @return {Boolean}
*/
utilities.hasKey = function (obj, findKey) {
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
if (typeof obj[key] === 'object') {
if (this.hasKey(obj[key], findKey) === true) {
return true;
}
}
if (key === findKey) {
return true;
}
}
}
return false;
};
/**
* find and rename key in an object
*
* @param {Object} obj
* @param {string} findKey
* @param {string} replaceKey
* @return {Object}
*/
utilities.findAndRenameKey = function (obj, findKey, replaceKey) {
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
if (typeof obj[key] === 'object') {
this.findAndRenameKey(obj[key], findKey, replaceKey);
}
if (key === findKey) {
if (replaceKey) {
obj[replaceKey] = obj[findKey];
}
delete obj[findKey];
}
}
}
return obj;
};
/**
* remove any properties in an object that are not in the list or do not start with 'x-'
*
* @param {Object} obj
* @param {Array} listOfProps
* @return {Object}
*/
utilities.removeProps = function (obj, listOfProps) {
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
if (listOfProps.indexOf(key) === -1 && key.startsWith('x-') === false) {
delete obj[key];
}
}
}
return obj;
};
/**
* is a Joi object
*
* @param {Object} joiObj
* @return {Boolean}
*/
utilities.isJoi = function (joiObj) {
return joiObj && Joi.isSchema(joiObj);
};
/**
* does JOI object have children
*
* @param {Object} joiObj
* @return {Boolean}
*/
utilities.hasJoiChildren = function (joiObj) {
if (!utilities.isJoi(joiObj)) {
return false;
}
const byId = Hoek.reach(joiObj, '_ids._byId');
const byKey = Hoek.reach(joiObj, '_ids._byKey');
// TODO add tests to cover "byId.size > 0"
return byKey.size > 0 || byId.size > 0;
};
/**
* does JOI object have description
*
* @param {Object} joiObj
* @return {Boolean}
*/
utilities.hasJoiDescription = function (joiObj) {
if (!utilities.isJoi(joiObj)) {
return false;
}
return !!Hoek.reach(joiObj, '_flags.description');
};
/**
* checks if object has meta array
*
* @param {Object} joiObj
* @return {Boolean}
*/
utilities.hasJoiMeta = function (joiObj) {
return utilities.isJoi(joiObj) && joiObj.$_terms.metas.length > 0;
};
/**
* get meta property value from JOI object
*
* @param {Object} joiObj
* @param {string} propertyName
* @return {Object || Undefined}
*/
utilities.getJoiMetaProperty = function (joiObj, propertyName) {
// get headers added using meta function
if (utilities.isJoi(joiObj) && utilities.hasJoiMeta(joiObj)) {
const meta = joiObj.$_terms.metas;
let i = meta.length;
while (i--) {
if (meta[i][propertyName]) {
return meta[i][propertyName];
}
}
}
return undefined;
};
/**
* get label from Joi object
*
* @param {Object} joiObj
* @return {String || Null}
*/
utilities.getJoiLabel = function (joiObj) {
// old version
/* $lab:coverage:off$ */
const label = Hoek.reach(joiObj, '_settings.language.label');
if (label) {
return label;
}
/* $lab:coverage:on$ */
// Joi > 10.9
return Hoek.reach(joiObj, '_flags.label') || null;
};
/**
* returns a javascript object into JOI object
* needed to covert custom parameters objects passed in by plug-in route options
*
* @param {Object} obj
* @return {Object}
*/
utilities.toJoiObject = function (obj) {
if (!utilities.isJoi(obj) && utilities.isObject(obj)) {
return Joi.object(obj);
}
return obj;
};
/**
* get chained functions for sorting
*
* @return {Function}
*/
utilities.firstBy = (function () {
// code from https://github.com/Teun/thenBy.js
// has its own tests
/* $lab:coverage:off$ */
const makeCompareFunction = function (f, direction) {
if (typeof f !== 'function') {
const prop = f;
// make unary function
f = function (v1) {
return v1[prop];
};
}
if (f.length === 1) {
// f is a unary function mapping a single item to its sort score
const uf = f;
f = function (v1, v2) {
return uf(v1) < uf(v2) ? -1 : uf(v1) > uf(v2) ? 1 : 0;
};
}
if (direction === -1) {
return function (v1, v2) {
return -f(v1, v2);
};
}
return f;
};
/* mixin for the `thenBy` property */
const extend = function (f, d) {
f = makeCompareFunction(f, d);
f.thenBy = tb;
return f;
};
/* adds a secondary compare function to the target function (`this` context)
which is applied in case the first one returns 0 (equal)
returns a new compare function, which has a `thenBy` method as well */
const tb = function (y, d) {
const self = this;
y = makeCompareFunction(y, d);
return extend((a, b) => {
return self(a, b) || y(a, b);
});
};
return extend;
/* $lab:coverage:on$ */
})();
/**
* create id
*
* @param {string} method
* @param {string} path
* @return {string}
*/
utilities.createId = function (method, path) {
const self = this;
if (!path.includes('/')) {
return method.toLowerCase() + self.toTitleCase(path);
}
const result = path
.split('/')
.map((item) => {
// replace chars such as '{'
const updatedItem = item.replace(/[^\w\s]/gi, '');
return self.toTitleCase(updatedItem);
})
.join('');
return method.toLowerCase() + result;
};
/**
* create toTitleCase
*
* @param {string} word
* @return {string}
*/
utilities.toTitleCase = function (word) {
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
};
/**
* applies path replacements
*
* @param {string} path
* @param {Array} applyTo
* @param {Array} options
* @return {string}
*/
utilities.replaceInPath = function (path, applyTo, options) {
options.forEach((option) => {
if (applyTo.includes(option.replaceIn) || option.replaceIn === 'all') {
path = path.replace(option.pattern, option.replacement);
}
});
return path;
};
/**
* removes trailing slash `/` from a string
*
* @param {string} str
* @return {string}
*/
utilities.removeTrailingSlash = function (str) {
return str.endsWith('/') ? str.slice(0, -1) : str;
};
/**
* Assign vendor extensions: x-* to the target. This mutates target.
*
* @param {Object} target
* @param {Object} source
* @return {Object}
*/
utilities.assignVendorExtensions = function (target, source) {
if (!this.isObject(target) || !this.isObject(source)) {
return target;
}
for (const sourceProperty in source) {
if (sourceProperty.startsWith('x-') && sourceProperty.length > 2) {
// this may override existing x- properties which should not be an issue since values should be identical.
target[sourceProperty] = Hoek.reach(source, sourceProperty) || null;
}
}
return target;
};
/**
* appends a querystring to an url
*
* @param {string} inputUrl
* @param {string} qsName
* @param {string} qsValue
* @return {string}
*/
utilities.appendQueryString = function (inputUrl, qsName, qsValue) {
if (!qsName || !qsValue) {
return inputUrl;
}
const [url, queryString] = inputUrl.split('?');
const searchParams = new URLSearchParams(queryString);
searchParams.set(qsName, qsValue);
return `${url}?${searchParams.toString()}`;
};