Last active
January 13, 2022 20:33
-
-
Save marcelstoer/750739c6e3b357872f953469ac7dd7ad to your computer and use it in GitHub Desktop.
Ugly way to redefine remap() function for SwaggerParser#bundle() from APIDevTools
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ********************************************************************************************************************* | |
// 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); | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thank you so much for posting this!
Here is a way that might be considered "less invasive".
It makes
remap
a property ofbundle
, which can then (optionally) be modified.This is done as an automated source code change to
node_modules/json-schema-ref-parser/lib/bundle.js
vianpm postinstall
.Finally your
remap
implementation (or any other) can be swapped in, by running this code before parsing begins: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