Skip to content

Commit

Permalink
- All of the $Refs methods now accept relative or absolute paths
Browse files Browse the repository at this point in the history
- Separated all of the path-related utility methods into their own file (`paths.js`)
- Removed bundling-related code from `resolve.js` and `ref.js`
- General refactoring and code cleanup
  • Loading branch information
JamesMessinger committed Jan 4, 2016
1 parent 76b60c4 commit cac28c6
Show file tree
Hide file tree
Showing 9 changed files with 398 additions and 281 deletions.
94 changes: 90 additions & 4 deletions lib/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,82 @@ module.exports = bundle;
function bundle(parser, options) {
util.debug('Bundling $ref pointers in %s', parser._basePath);

optimize(parser.$refs);
remap(parser.$refs, options);
dereference(parser._basePath, parser.$refs, options);
}

/**
* Optimizes the {@link $Ref#referencedAt} list for each {@link $Ref} to contain as few entries
* as possible (ideally, one).
*
* @example:
* {
* first: { $ref: somefile.json#/some/part },
* second: { $ref: somefile.json#/another/part },
* third: { $ref: somefile.json },
* fourth: { $ref: somefile.json#/some/part/sub/part }
* }
*
* In this example, there are four references to the same file, but since the third reference points
* to the ENTIRE file, that's the only one we care about. The other three can just be remapped to point
* inside the third one.
*
* On the other hand, if the third reference DIDN'T exist, then the first and second would both be
* significant, since they point to different parts of the file. The fourth reference is not significant,
* since it can still be remapped to point inside the first one.
*
* @param {$Refs} $refs
*/
function optimize($refs) {
Object.keys($refs._$refs).forEach(function(key) {
var $ref = $refs._$refs[key];

// Find the first reference to this $ref
var first = $ref.referencedAt.filter(function(at) { return at.firstReference; })[0];

// Do any of the references point to the entire file?
var entireFile = $ref.referencedAt.filter(function(at) { return at.hash === '#'; });
if (entireFile.length === 1) {
// We found a single reference to the entire file. Done!
$ref.referencedAt = entireFile;
}
else if (entireFile.length > 1) {
// We found more than one reference to the entire file. Pick the first one.
if (entireFile.indexOf(first) >= 0) {
$ref.referencedAt = [first];
}
else {
$ref.referencedAt = entireFile.slice(0, 1);
}
}
else {
// There are noo references to the entire file, so optimize the list of reference points
// by eliminating any duplicate/redundant ones (e.g. "fourth" in the example above)
console.log('========================= %s BEFORE =======================', $ref.path, JSON.stringify($ref.referencedAt, null, 2));
[first].concat($ref.referencedAt).forEach(function(at) {
dedupe(at, $ref.referencedAt);
});
console.log('========================= %s AFTER =======================', $ref.path, JSON.stringify($ref.referencedAt, null, 2));
}
});
}

/**
* Removes redundant entries from the {@link $Ref#referencedAt} list.
*
* @param {object} original - The {@link $Ref#referencedAt} entry to keep
* @param {object[]} dupes - The {@link $Ref#referencedAt} list to dedupe
*/
function dedupe(original, dupes) {
for (var i = dupes.length - 1; i >= 0; i--) {
var dupe = dupes[i];
if (dupe !== original && dupe.hash.indexOf(original.hash) === 0) {
dupes.splice(i, 1);
}
}
}

/**
* Re-maps all $ref pointers in the schema, so that they are relative to the root of the schema.
*
Expand All @@ -38,7 +110,7 @@ function remap($refs, options) {
var remapped = [];

// Crawl the schema and determine the re-mapped values for all $ref pointers.
// NOTE: We don't actually APPLY the re-mappings them yet, since that can affect other re-mappings
// NOTE: We don't actually APPLY the re-mappings yet, since that can affect other re-mappings
Object.keys($refs._$refs).forEach(function(key) {
var $ref = $refs._$refs[key];
crawl($ref.value, $ref.path + '#', $refs, remapped, options);
Expand Down Expand Up @@ -72,8 +144,21 @@ function crawl(obj, path, $refs, remapped, options) {
var $refPath = url.resolve(path, value.$ref);
var pointer = $refs._resolve($refPath, options);

// Find the path from the root of the JSON schema
var hash = util.path.getHash(value.$ref);
var referencedAt = pointer.$ref.referencedAt.filter(function(at) {
return hash.indexOf(at.hash) === 0;
})[0];

console.log(
'referencedAt.pathFromRoot =', referencedAt.pathFromRoot,
'\nreferencedAt.hash =', referencedAt.hash,
'\nhash =', hash,
'\npointer.path.hash =', util.path.getHash(pointer.path)
);

// Re-map the value
var new$RefPath = pointer.$ref.pathFromRoot + util.path.getHash(pointer.path).substr(1);
var new$RefPath = referencedAt.pathFromRoot + util.path.getHash(pointer.path).substr(1);
util.debug(' new value: %s', new$RefPath);
remapped.push({
old$Ref: value,
Expand All @@ -99,8 +184,9 @@ function dereference(basePath, $refs, options) {

Object.keys($refs._$refs).forEach(function(key) {
var $ref = $refs._$refs[key];
if ($ref.pathFromRoot !== '#') {
$refs.set(basePath + $ref.pathFromRoot, $ref.value, options);

if ($ref.referencedAt.length > 0) {
$refs.set(basePath + $ref.referencedAt[0].pathFromRoot, $ref.value, options);
}
});
}
103 changes: 52 additions & 51 deletions lib/dereference.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,59 +16,42 @@ module.exports = dereference;
* @param {$RefParserOptions} options
*/
function dereference(parser, options) {
util.debug('Dereferencing $ref pointers in %s', parser._basePath);
util.debug('Dereferencing $ref pointers in %s', parser.$refs._basePath);
parser.$refs.circular = false;
crawl(parser.schema, parser._basePath, [], parser.$refs, options);
crawl(parser.schema, parser.$refs._basePath, '#', [], parser.$refs, options);
}

/**
* Recursively crawls the given value, and dereferences any JSON references.
*
* @param {*} obj - The value to crawl. If it's not an object or array, it will be ignored.
* @param {string} path - The path to use for resolving relative JSON references
* @param {string} path - The full path of `obj`, possibly with a JSON Pointer in the hash
* @param {string} pathFromRoot - The path of `obj` from the schema root
* @param {object[]} parents - An array of the parent objects that have already been dereferenced
* @param {$Refs} $refs - The resolved JSON references
* @param {$Refs} $refs
* @param {$RefParserOptions} options
* @returns {boolean} - Returns true if a circular reference was found
*/
function crawl(obj, path, parents, $refs, options) {
function crawl(obj, path, pathFromRoot, parents, $refs, options) {
var isCircular = false;

if (obj && typeof obj === 'object') {
parents.push(obj);

Object.keys(obj).forEach(function(key) {
var keyPath = Pointer.join(path, key);
var keyPathFromRoot = Pointer.join(pathFromRoot, key);
var value = obj[key];
var circular = false;

if ($Ref.isAllowed$Ref(value, options)) {
// We found a $ref, so resolve it
util.debug('Dereferencing $ref pointer "%s" at %s', value.$ref, keyPath);
var $refPath = url.resolve(path, value.$ref);
var pointer = $refs._resolve($refPath, options);

// Check for circular references
circular = pointer.circular || parents.indexOf(pointer.value) !== -1;
circular && foundCircularReference(keyPath, $refs, options);

// Dereference the JSON reference
var dereferencedValue = getDereferencedValue(value, pointer.value);

// Crawl the dereferenced value (unless it's circular)
if (!circular) {
// If the `crawl` method returns true, then dereferenced value is circular
circular = crawl(dereferencedValue, pointer.path, parents, $refs, options);
}

// Replace the JSON reference with the dereferenced value
if (!circular || options.$refs.circular === true) {
obj[key] = dereferencedValue;
}
var dereferenced = dereference$Ref(value, keyPath, keyPathFromRoot, parents, $refs, options);
circular = dereferenced.circular;
obj[key] = dereferenced.value;
}
else {
if (parents.indexOf(value) === -1) {
circular = crawl(value, keyPath, parents, $refs, options);
circular = crawl(value, keyPath, keyPathFromRoot, parents, $refs, options);
}
else {
circular = foundCircularReference(keyPath, $refs, options);
Expand All @@ -85,33 +68,51 @@ function crawl(obj, path, parents, $refs, options) {
}

/**
* Returns the dereferenced value of the given JSON reference.
* Dereferences the given JSON Reference, and then crawls the resulting value.
*
* @param {object} currentValue - the current value, which contains a JSON reference ("$ref" property)
* @param {*} resolvedValue - the resolved value, which can be any type
* @returns {*} - Returns the dereferenced value
* @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 {string} pathFromRoot - The path of `$ref` from the schema root
* @param {object[]} parents - An array of the parent objects that have already been dereferenced
* @param {$Refs} $refs
* @param {$RefParserOptions} options
* @returns {object}
*/
function getDereferencedValue(currentValue, resolvedValue) {
if (resolvedValue && typeof resolvedValue === 'object' && Object.keys(currentValue).length > 1) {
// The current value has additional properties (other than "$ref"),
// so merge the resolved value rather than completely replacing the reference
var merged = {};
Object.keys(currentValue).forEach(function(key) {
if (key !== '$ref') {
merged[key] = currentValue[key];
}
});
Object.keys(resolvedValue).forEach(function(key) {
if (!(key in merged)) {
merged[key] = resolvedValue[key];
}
});
return merged;
function dereference$Ref($ref, path, pathFromRoot, parents, $refs, options) {
util.debug('Dereferencing $ref pointer "%s" at %s', $ref.$ref, path);

var $refPath = url.resolve(path, $ref.$ref);
var pointer = $refs._resolve($refPath, options);

// Check for circular references
var directCircular = pointer.circular;
var circular = directCircular || parents.indexOf(pointer.value) !== -1;
circular && foundCircularReference(path, $refs, options);

// Dereference the JSON reference
var dereferencedValue = util.dereference($ref, pointer.value);

// Crawl the dereferenced value (unless it's circular)
if (!circular) {
// If the `crawl` method returns true, then dereferenced value is circular
circular = crawl(dereferencedValue, pointer.path, pathFromRoot, parents, $refs, options);
}
else {
// Completely replace the original reference with the resolved value
return resolvedValue;

if (circular && !directCircular && options.$refs.circular === 'ignore') {
// The user has chosen to "ignore" circular references, so don't change the value
dereferencedValue = $ref;
}

if (directCircular) {
// The pointer is a DIRECT circular reference (i.e. it references itself).
// So replace the $ref path with the absolute path from the JSON Schema root
dereferencedValue.$ref = pathFromRoot;
}

return {
circular: circular,
value: dereferencedValue
};
}

/**
Expand Down
21 changes: 6 additions & 15 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,6 @@ function $RefParser() {
* @type {$Refs}
*/
this.$refs = new $Refs();

/**
* The file path or URL of the main JSON schema file.
* This will be empty if the schema was passed as an object rather than a path.
*
* @type {string}
* @protected
*/
this._basePath = '';
}

/**
Expand Down Expand Up @@ -79,8 +70,8 @@ $RefParser.prototype.parse = function(schema, options, callback) {
if (args.schema && typeof args.schema === 'object') {
// The schema is an object, not a path/url
this.schema = args.schema;
this._basePath = '';
var $ref = new $Ref(this.$refs, this._basePath);
this.$refs._basePath = '';
var $ref = new $Ref(this.$refs, this.$refs._basePath);
$ref.setValue(this.schema, args.options);

return maybe(args.callback, Promise.resolve(this.schema));
Expand All @@ -96,14 +87,14 @@ $RefParser.prototype.parse = function(schema, options, callback) {
// Resolve the absolute path of the schema
args.schema = util.path.localPathToUrl(args.schema);
args.schema = url.resolve(util.path.cwd(), args.schema);
this._basePath = util.path.stripHash(args.schema);
this.$refs._basePath = util.path.stripHash(args.schema);

// Read the schema file/url
return read(args.schema, this.$refs, args.options)
.then(function(cached$Ref) {
var value = cached$Ref.$ref.value;
.then(function(result) {
var value = result.$ref.value;
if (!value || typeof value !== 'object' || value instanceof Buffer) {
throw ono.syntax('"%s" is not a valid JSON Schema', me._basePath);
throw ono.syntax('"%s" is not a valid JSON Schema', me.$refs._basePath);
}
else {
me.schema = value;
Expand Down
Loading

0 comments on commit cac28c6

Please sign in to comment.