Skip to content

Instantly share code, notes, and snippets.

@marcelstoer
Last active January 13, 2022 20:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save marcelstoer/750739c6e3b357872f953469ac7dd7ad to your computer and use it in GitHub Desktop.
Save marcelstoer/750739c6e3b357872f953469ac7dd7ad to your computer and use it in GitHub Desktop.
Ugly way to redefine remap() function for SwaggerParser#bundle() from APIDevTools
// *********************************************************************************************************************
// I needed to redefine the remap() function invoked at
// https://github.com/APIDevTools/json-schema-ref-parser/blob/master/lib/bundle.js#L25 due to
// https://github.com/APIDevTools/swagger-parser/issues/127
//
// I'm not versed enough with JavaScript and Node.js to understand if there would be a less invasive way to have my own
// remap(). As far as I understand SwaggerParser (or JSON Schema $Ref Parser for that matter) is not built in a way that
// allows to easily extend it. The only option I found was to redefine the bundle() prototype function and to copy a lot
// of code from bundle.js.
//
// Place this file alongside the script which invokes the SwaggerParser. In your own script do:
// require("./ref-parser-bundler-overwrite");
// SwaggerParser.bundle(...);
//
// Also, in order to ensure the bundling produces a valid output I call the validator (SwaggerParser.validate()) twice.
// First on the file to-be-bundled before bundling it and then again against the bundled file.
// *********************************************************************************************************************
(function () {
"use strict";
const $RefParser = require("json-schema-ref-parser");
const normalizeArgs = require("json-schema-ref-parser/lib/normalize-args");
const $Ref = require("json-schema-ref-parser/lib/ref");
const Pointer = require("json-schema-ref-parser/lib/pointer");
const url = require("json-schema-ref-parser/lib/util/url");
const maybe = require("call-me-maybe");
// Redefines the function from json-schema-ref-parser/lib/index.js. Using the same code as upstream allows us to just
// change no behavior other than calling our own remap().
$RefParser.prototype.bundle = async function (path, schema, options, callback) {
let me = this;
let args = normalizeArgs(arguments);
try {
await this.resolve(args.path, args.schema, args.options);
// Inline the code from lib/bundle.js:bundle()
let inventory = [];
crawl(me, "schema", me.$refs._root$Ref.path + "#", "#", 0, inventory, me.$refs, args.options);
let $refMap = new Map(inventory.map(i => [i.$ref.$ref, i.value]));
remap(me, me, "schema", $refMap);
// END lib/bundle.js:bundle()
return maybe(args.callback, Promise.resolve(me.schema));
}
catch (err) {
return maybe(args.callback, Promise.reject(err));
}
};
/**
* Alternative implementation to json-schema-ref-parser/lib/bundle.js:remap().
*
* @param {object} bundle The Swagger model of the final bundle i.e. the result schema.
* @param {object} parent The parent object in which the below key is contained.
* @param {string} key parent[key] is the object to be processed by the function.
* @param {map} $refMap Maps a $ref string (internal or external) to the object being referenced. These objects are
* the ones being bundled into the result schema.
*/
function remap(bundle, parent, key, $refMap) {
let obj = key === null ? parent : parent[key];
if (obj && typeof obj === "object") {
// Determines whether the given value is a JSON reference, and whether it is allowed by the options.
if ($Ref.isAllowed$Ref(obj)) {
// 'obj' may be an external $ref object like e.g.
// {"$ref": "../../../../technical-definitions/common-types/v1/common-types-model.yaml#/parameters/Page"}
// or an internal $ref object. Since this function works recursively through all references one cannot ignore
// the internal ones (i.e. whose target object is already part of the bundle). They may be internal to an
// external file in which case its value still has to be pulled in.
const hash = obj.$ref.substring(obj.$ref.indexOf("#")); // -> #/parameters/Page
const objType = hash.substring(2, hash.indexOf("/", 2)); // -> parameters
const $refName = hash.substring(hash.lastIndexOf("/") + 1); // -> Page
if (!bundle.schema[objType]) {
bundle.schema[objType] = {};
}
// Only process the object to-be-bundled if it's not included in the bundle schema already.
if (!bundle.schema[objType][$refName]) {
// Ensure the object to-be-bundled does not contain any external references itself -> remap it recursively.
const $refObject = $refMap.get(obj.$ref);
remap(bundle, $refObject, null, $refMap);
bundle.schema[objType][$refName] = $refObject;
}
// Remap the $ref to the local path of the now bundled object.
parent[key].$ref = hash;
} else {
// Recursively iterate over all children.
for (let childKey of Object.keys(obj)) {
remap(bundle, obj, childKey, $refMap);
}
}
}
}
/** ************************************************************************/
/** EVERYTHING DOWN HERE COPIED FROM json-schema-ref-parser/lib/bundle.js. */
/** ************************************************************************/
/**
* Recursively crawls the given value, and inventories all JSON references.
*
* @param {object} parent - The object containing the value to crawl. If the value is not an object or array, it will be ignored.
* @param {string} key - The property key of `parent` to be crawled
* @param {string} path - The full path of the property being crawled, possibly with a JSON Pointer in the hash
* @param {string} pathFromRoot - The path of the property being crawled, from the schema root
* @param {object[]} inventory - An array of already-inventoried $ref pointers
* @param {$Refs} $refs
* @param {$RefParserOptions} options
*/
function crawl (parent, key, path, pathFromRoot, indirections, inventory, $refs, options) {
let obj = key === null ? parent : parent[key];
if (obj && typeof obj === "object") {
if ($Ref.isAllowed$Ref(obj)) {
inventory$Ref(parent, key, path, pathFromRoot, indirections, inventory, $refs, options);
}
else {
// Crawl the object in a specific order that's optimized for bundling.
// This is important because it determines how `pathFromRoot` gets built,
// which later determines which keys get dereferenced and which ones get remapped
let keys = Object.keys(obj)
.sort((a, b) => {
// Most people will expect references to be bundled into the the "definitions" property,
// so we always crawl that property first, if it exists.
if (a === "definitions") {
return -1;
}
else if (b === "definitions") {
return 1;
}
else {
// Otherwise, crawl the keys based on their length.
// This produces the shortest possible bundled references
return a.length - b.length;
}
});
// eslint-disable-next-line no-shadow
for (let key of keys) {
let keyPath = Pointer.join(path, key);
let keyPathFromRoot = Pointer.join(pathFromRoot, key);
let value = obj[key];
if ($Ref.isAllowed$Ref(value)) {
inventory$Ref(obj, key, path, keyPathFromRoot, indirections, inventory, $refs, options);
}
else {
crawl(obj, key, keyPath, keyPathFromRoot, indirections, inventory, $refs, options);
}
}
}
}
}
/**
* Inventories the given JSON Reference (i.e. records detailed information about it so we can
* optimize all $refs in the schema), and then crawls the resolved value.
*
* @param {object} $refParent - The object that contains a JSON Reference as one of its keys
* @param {string} $refKey - The key in `$refParent` that is a JSON Reference
* @param {string} path - The full path of the JSON Reference at `$refKey`, possibly with a JSON Pointer in the hash
* @param {string} pathFromRoot - The path of the JSON Reference at `$refKey`, from the schema root
* @param {object[]} inventory - An array of already-inventoried $ref pointers
* @param {$Refs} $refs
* @param {$RefParserOptions} options
*/
function inventory$Ref ($refParent, $refKey, path, pathFromRoot, indirections, inventory, $refs, options) {
let $ref = $refKey === null ? $refParent : $refParent[$refKey];
let $refPath = url.resolve(path, $ref.$ref);
let pointer = $refs._resolve($refPath, options);
let depth = Pointer.parse(pathFromRoot).length;
let file = url.stripHash(pointer.path);
let hash = url.getHash(pointer.path);
let external = file !== $refs._root$Ref.path;
let extended = $Ref.isExtended$Ref($ref);
indirections += pointer.indirections;
let existingEntry = findInInventory(inventory, $refParent, $refKey);
if (existingEntry) {
// This $Ref has already been inventoried, so we don't need to process it again
if (depth < existingEntry.depth || indirections < existingEntry.indirections) {
removeFromInventory(inventory, existingEntry);
}
else {
return;
}
}
inventory.push({
$ref, // The JSON Reference (e.g. {$ref: string})
parent: $refParent, // The object that contains this $ref pointer
key: $refKey, // The key in `parent` that is the $ref pointer
pathFromRoot, // The path to the $ref pointer, from the JSON Schema root
depth, // How far from the JSON Schema root is this $ref pointer?
file, // The file that the $ref pointer resolves to
hash, // The hash within `file` that the $ref pointer resolves to
value: pointer.value, // The resolved value of the $ref pointer
circular: pointer.circular, // Is this $ref pointer DIRECTLY circular? (i.e. it references itself)
extended, // Does this $ref extend its resolved value? (i.e. it has extra properties, in addition to "$ref")
external, // Does this $ref pointer point to a file other than the main JSON Schema file?
indirections, // The number of indirect references that were traversed to resolve the value
});
// Recursively crawl the resolved value
crawl(pointer.value, null, pointer.path, pathFromRoot, indirections + 1, inventory, $refs, options);
}
/**
* TODO
*/
function findInInventory (inventory, $refParent, $refKey) {
for (let i = 0; i < inventory.length; i++) {
let existingEntry = inventory[i];
if (existingEntry.parent === $refParent && existingEntry.key === $refKey) {
return existingEntry;
}
}
}
function removeFromInventory (inventory, entry) {
let index = inventory.indexOf(entry);
inventory.splice(index, 1);
}
})();
@pcafstockf
Copy link

Thank you so much for posting this!

Here is a way that might be considered "less invasive".
It makes remap a property of bundle, which can then (optionally) be modified.

function bundle (parser, options) {
	...
	bundle.remap(inventory, parser); // instead of just remap(inventory)
}
bundle.remap = remap; // default to the original behavior

This is done as an automated source code change to node_modules/json-schema-ref-parser/lib/bundle.js via npm postinstall.

"scripts": {
	"postinstall": "npm run _patchjsrp",
	"_patchjsrp//": "Patch json-schema-ref-parser.  See https://gist.github.com/marcelstoer/750739c6e3b357872f953469ac7dd7ad",
	"_patchjsrp": "replace-in-file \"/^\\s*remap\\s*\\(\\s*inventory\\s*\\)\\s*;\\s*}\\s*/gm\" \"  bundle.remap(inventory, parser);\n}\nbundle.remap = remap;\n\n\" ./node_modules/json-schema-ref-parser/lib/bundle.js --isRegex"
}

Finally your remap implementation (or any other) can be swapped in, by running this code before parsing begins:

require('json-schema-ref-parser/lib/bundle').remap = function (inventory, parser) {
	redefinedRemap(parser, parser, "schema", new Map(inventory.map(i => [i.$ref.$ref, i.value])));
};
const $Ref = require("json-schema-ref-parser/lib/ref");
function redefinedRemap(bundle, parent, key, $refMap) {
	// your remap code from above goes here
}

It's not a great solution, but it could help stay in sync with other changes / fixes that might be made to json-schema-ref-parser/lib/bundle.js

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment