Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #251 #261

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/pointer.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ Pointer.prototype.resolve = function (obj, options, pathFromRoot) {
let token = tokens[i];
if (this.value[token] === undefined || this.value[token] === null) {
this.value = null;
throw new MissingPointerError(token, decodeURI(this.originalPath));
throw new MissingPointerError(token, this.originalPath);
}
else {
this.value = this.value[token];
Expand Down
2 changes: 1 addition & 1 deletion lib/ref.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ $Ref.prototype.resolve = function (path, options, friendlyPath, pathFromRoot) {
if (err instanceof InvalidPointerError) {
// this is a special case - InvalidPointerError is thrown when dereferencing external file,
// but the issue is caused by the source file that referenced the file that undergoes dereferencing
err.source = decodeURI(stripHash(pathFromRoot));
err.source = stripHash(pathFromRoot);
}

this.addError(err);
Expand Down
146 changes: 132 additions & 14 deletions lib/resolve-external.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const Pointer = require("./pointer");
const parse = require("./parse");
const url = require("./util/url");
const { isHandledError } = require("./util/errors");

const _ = require('lodash');
module.exports = resolveExternal;

/**
Expand Down Expand Up @@ -67,7 +67,12 @@ function crawl (obj, path, $refs, options, seen) {
let value = obj[key];

if ($Ref.isExternal$Ref(value)) {
promises.push(resolve$Ref(value, keyPath, $refs, options));
if(Object.keys(value).length > 1) {
promises.push(resolveAndMerge$Ref(value, keyPath, $refs, options, seen));
}
else {
promises.push(resolve$Ref(value, keyPath, $refs, options));
}
}
else {
promises = promises.concat(crawl(value, keyPath, $refs, options, seen));
Expand All @@ -80,34 +85,147 @@ function crawl (obj, path, $refs, options, seen) {
}

/**
* Resolves the given JSON Reference, and then crawls the resulting value.
* This assigns the value to the objects property, while traversing the json path tree
*
* @param {{$ref: string}} $ref - The JSON Reference to resolve
* @param {string} path - The full path of `$ref`, possibly with a JSON Pointer in the hash
* @param {$Refs} $refs
* @param {$RefParserOptions} options
* @param object - The object we want to assign to
* @param propertyToAssign - The property object we wish to assign
* @param {string} jsonPath - The json path for this property
*/

function assignPropertyAt( object, propertyToAssign, jsonPath ) {
let property = object || this;

if(jsonPath) {
let parts = jsonPath.split( "." );
let length = parts.length;
let i;
for ( i = 0; i < length; i++ ) {
property = property[parts[i]];
}
}

Object.assign(property, propertyToAssign);

}

/**
* If object does not use Dates, functions, undefined, Infinity, RegExps, Maps, Sets, Blobs,
* FileLists, ImageDatas, sparse Arrays, Typed Arrays or other complex types within object than
* a deep clone of the input object will be returned.
*
* @param {Object} obj
* @returns {Object}
* Deep copy of the input object
*/
function deepCopy(obj) {
return JSON.parse(JSON.stringify(obj));
}

/**
* Returns JSON path in dot notation
*
* @returns {Promise}
* The promise resolves once all JSON references in the object have been resolved,
* including nested references that are contained in externally-referenced files.
* @param {hashedPath: string} hashedPath
* @returns {jsonPath: string}
*/
function hashedPathToJsonPath(hashedPath) {
let hashIndex = hashedPath.indexOf("#");
let jsonPath;
if (hashIndex >= 0) {
jsonPath = hashedPath.substr(hashIndex+2, hashedPath.length);
jsonPath = jsonPath.replace(/\//g, '.');
}

return jsonPath;
}

/**
* Customizer which concats arrays during merge without duplicating values
*
* @param {Object} objValue
* @param {Object} srcValue
* @returns
* Concatanated arrays
*/
function concatArray(objValue, srcValue) {
if (_.isArray(objValue)) {
return _.unionWith(objValue, srcValue, _.isEqual);
}
}

async function resolveAndMerge$Ref ($ref, path, $refs, options, seen) {
let resolvedPath = url.resolve(path, $ref.$ref);
let withoutHash = url.stripHash(resolvedPath);

// Do we already have this $ref?
const knownRef = $refs._$refs[withoutHash];
if (knownRef) {
// We've already parsed this $ref, so use the existing value
return Promise.resolve($ref.value);
}

// Parse the $referenced file/url
try {
const parsedRef = await parse(resolvedPath, $refs, options);

// Deep copy of parsed reference
const parsedRefCopy = deepCopy(parsedRef);

// Deep copy the $ref
const $refCopy = deepCopy($ref);

// Get rid of $ref part.
// Leave only the part which should be merged.
delete $refCopy.$ref;

const jsonPath = hashedPathToJsonPath(resolvedPath);

// Assign the part which should be merged in the right place
assignPropertyAt(parsedRefCopy, $refCopy, jsonPath);

// Deep merge parsed reference with the merged copy by concatinating arrays if they occur
const parsedMergedRef = _.mergeWith(parsedRef, parsedRefCopy, concatArray);

// Delete all keys except $ref from $ref as they are aleady merged inside parsedMergeRef
for (let key of Object.keys($ref)) {
if (key != '$ref') {
delete $ref[key];
}
}

// Crawl the parsed value
let promises = crawl(parsedMergedRef, withoutHash + "#", $refs, options, seen);

return Promise.all(promises);
}
catch (err) {
if (!options.continueOnError || !isHandledError(err)) {
throw err;
}

if ($refs._$refs[withoutHash]) {
err.source = url.stripHash(path);
err.path = url.safePointerToPath(url.getHash(path));
}

return [];
}
}
async function resolve$Ref ($ref, path, $refs, options) {
// console.log('Resolving $ref pointer "%s" at %s', $ref.$ref, path);

let resolvedPath = url.resolve(path, $ref.$ref);
let withoutHash = url.stripHash(resolvedPath);

// Do we already have this $ref?
$ref = $refs._$refs[withoutHash];
if ($ref) {
let knownRef = $refs._$refs[withoutHash];
if (knownRef) {
// We've already parsed this $ref, so use the existing value
return Promise.resolve($ref.value);
}

// Parse the $referenced file/url
try {
const result = await parse(resolvedPath, $refs, options);

// Crawl the parsed value
// console.log('Resolving $ref pointers in %s', withoutHash);
let promises = crawl(result, withoutHash + "#", $refs, options);
Expand All @@ -120,7 +238,7 @@ async function resolve$Ref ($ref, path, $refs, options) {
}

if ($refs._$refs[withoutHash]) {
err.source = decodeURI(url.stripHash(path));
err.source = url.stripHash(path);
err.path = url.safePointerToPath(url.getHash(path));
}

Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,13 @@
"@jsdevtools/ono": "^7.1.3",
"@types/json-schema": "^7.0.6",
"call-me-maybe": "^1.0.1",
"js-yaml": "^4.1.0"
"js-yaml": "^4.1.0",
"lodash": "^4.17.21"
},
"release": {
"branches": ["main"],
"branches": [
"main"
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
Expand Down