UNPKG

bump-cli

Version:

The Bump CLI is used to interact with your API documentation hosted on Bump.sh by using the API of developers.bump.sh

143 lines (142 loc) 6.26 kB
import debug from 'debug'; import * as jsonpath from 'jsonpathly'; import { mergician } from 'mergician'; export class Overlay { // WIP @github.com/lornajane/openapi-overlays-js // // I couldn't get the upstream lib to be imported properly due to // some issues with ESM module imports so this is method was copied // from github.com/lornajane/openapi-overlays-js and has been // adapted to make our Typescript build happy. // /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ d(formatter, ...args) { return debug(`bump-cli:core:overlay`)(formatter, ...args); } // Function signature type taken from @types/debug // Debugger(formatter: any, ...args: any[]): void; // If you make any changes here, PLEASE ALSO MAKE THEM UPSTREAM. run(spec, overlay) { // Use jsonpath.apply to do the changes if (overlay.actions && overlay.actions.length > 0) { for (const a of overlay.actions) { const action = a; if (!action.target) { process.stderr.write(`WARNING: ${this.humanName(action)} has an empty target\n`); continue; } const target = action.target; // jsonpathly's paths are strings. They represent a “Path // expression” which represents the absolute path in the objet // tree reached by our `target`. // // E.g. '$["store"]["book"][0]["price"]' const paths = jsonpath.paths(spec, target); if (paths.length === 0) { process.stderr.write(`WARNING: Action target '${target}' has no matching elements\n`); continue; } // When we execute a 'remove' action we need to be careful if // the targets are elements of an array. Because the list of // paths contains an index of each element. // // E.g. // ["servers"][0] // ["servers"][1] // ["servers"][2] // // And if we remove elements starting from the 0, the array // will be reindexed and thus the '2' index won't be valid // anymore. // // Thus, in that case we revers the list of paths if (action.remove) { paths.reverse(); } for (const path of paths) { // The 'executeAction' will mutate the passed spec object in // place. spec = this.executeAction(spec, action, path); } } } else { process.stderr.write('WARNING: No actions found in your overlay\n'); } return spec; } /* Mutates the given 'spec' object with the 'action' given and * targeting a unique 'pat ' within the spec object. We don't check * if the path is valid in the object as this is the role of the * jsonpathly lib which we used previously to extract the target * paths. */ executeAction(spec, action, path) { const explodedPath = path.split(/(?:]\[|\$\[)+/); // Remove root explodedPath.shift(); // Take last element from path (which is the thing to act // upon) let thingToActUpon = explodedPath.pop(); // The last element (e.g. '"price"]' or '0]') contains a final ']' // so we need to remove it AND we need to parse the element to // transform the string in either a string or a number thingToActUpon = thingToActUpon === undefined ? '$' : (thingToActUpon = JSON.parse(thingToActUpon.slice(0, -1))); // Reconstruct the stringified path expression targeting the parent const parentPath = explodedPath.join(']['); const parent = parentPath.length > 0 ? jsonpath.query(spec, `$[${parentPath}]`) : spec; // Do the overlay action // Is it a remove? if (Object.hasOwn(action, 'remove')) { this.d(`Executing 'remove' on target path: ${path}`); this.remove(parent, thingToActUpon); } else if (Object.hasOwn(action, 'update')) { this.d(`Executing 'update' on target path: ${path}`); spec = this.update(spec, parent, action.update, thingToActUpon); } else { process.stderr.write(`WARNING: ${this.humanName(action)} needs either a 'remove' or an 'update' property\n`); } return spec; } humanName(action) { return action.description ? `Action '${action.description}'` : 'Action'; } remove(parent, property_or_index) { if (Array.isArray(parent)) { parent.splice(property_or_index, 1); } else { delete parent[property_or_index]; } } update(spec, parent, update, property_or_index) { try { // Deep merge objects using a module (built-in spread operator is only shallow) const merger = mergician({ appendArrays: true }); if (property_or_index === '$') { // You can't actually merge an update on a root object // target with the jsonpathly lib, this is just us merging // the given update with the whole spec. spec = merger(spec, update); } else if (property_or_index !== undefined) { const targetObject = parent[property_or_index]; if (typeof targetObject === 'object' && typeof update === 'object') { parent[property_or_index] = Array.isArray(targetObject) && Array.isArray(update) ? [...targetObject, ...update] : merger(targetObject, update); } else { parent[property_or_index] = update; } } } catch (error) { process.stderr.write(`Error applying overlay: ${error.message}\n`); } return spec; } }