Skip to content

Commit cac28c6

Browse files
- All of the $Refs methods now accept relative or absolute paths
- 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
1 parent 76b60c4 commit cac28c6

File tree

9 files changed

+398
-281
lines changed

9 files changed

+398
-281
lines changed

lib/bundle.js

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,82 @@ module.exports = bundle;
2424
function bundle(parser, options) {
2525
util.debug('Bundling $ref pointers in %s', parser._basePath);
2626

27+
optimize(parser.$refs);
2728
remap(parser.$refs, options);
2829
dereference(parser._basePath, parser.$refs, options);
2930
}
3031

32+
/**
33+
* Optimizes the {@link $Ref#referencedAt} list for each {@link $Ref} to contain as few entries
34+
* as possible (ideally, one).
35+
*
36+
* @example:
37+
* {
38+
* first: { $ref: somefile.json#/some/part },
39+
* second: { $ref: somefile.json#/another/part },
40+
* third: { $ref: somefile.json },
41+
* fourth: { $ref: somefile.json#/some/part/sub/part }
42+
* }
43+
*
44+
* In this example, there are four references to the same file, but since the third reference points
45+
* to the ENTIRE file, that's the only one we care about. The other three can just be remapped to point
46+
* inside the third one.
47+
*
48+
* On the other hand, if the third reference DIDN'T exist, then the first and second would both be
49+
* significant, since they point to different parts of the file. The fourth reference is not significant,
50+
* since it can still be remapped to point inside the first one.
51+
*
52+
* @param {$Refs} $refs
53+
*/
54+
function optimize($refs) {
55+
Object.keys($refs._$refs).forEach(function(key) {
56+
var $ref = $refs._$refs[key];
57+
58+
// Find the first reference to this $ref
59+
var first = $ref.referencedAt.filter(function(at) { return at.firstReference; })[0];
60+
61+
// Do any of the references point to the entire file?
62+
var entireFile = $ref.referencedAt.filter(function(at) { return at.hash === '#'; });
63+
if (entireFile.length === 1) {
64+
// We found a single reference to the entire file. Done!
65+
$ref.referencedAt = entireFile;
66+
}
67+
else if (entireFile.length > 1) {
68+
// We found more than one reference to the entire file. Pick the first one.
69+
if (entireFile.indexOf(first) >= 0) {
70+
$ref.referencedAt = [first];
71+
}
72+
else {
73+
$ref.referencedAt = entireFile.slice(0, 1);
74+
}
75+
}
76+
else {
77+
// There are noo references to the entire file, so optimize the list of reference points
78+
// by eliminating any duplicate/redundant ones (e.g. "fourth" in the example above)
79+
console.log('========================= %s BEFORE =======================', $ref.path, JSON.stringify($ref.referencedAt, null, 2));
80+
[first].concat($ref.referencedAt).forEach(function(at) {
81+
dedupe(at, $ref.referencedAt);
82+
});
83+
console.log('========================= %s AFTER =======================', $ref.path, JSON.stringify($ref.referencedAt, null, 2));
84+
}
85+
});
86+
}
87+
88+
/**
89+
* Removes redundant entries from the {@link $Ref#referencedAt} list.
90+
*
91+
* @param {object} original - The {@link $Ref#referencedAt} entry to keep
92+
* @param {object[]} dupes - The {@link $Ref#referencedAt} list to dedupe
93+
*/
94+
function dedupe(original, dupes) {
95+
for (var i = dupes.length - 1; i >= 0; i--) {
96+
var dupe = dupes[i];
97+
if (dupe !== original && dupe.hash.indexOf(original.hash) === 0) {
98+
dupes.splice(i, 1);
99+
}
100+
}
101+
}
102+
31103
/**
32104
* Re-maps all $ref pointers in the schema, so that they are relative to the root of the schema.
33105
*
@@ -38,7 +110,7 @@ function remap($refs, options) {
38110
var remapped = [];
39111

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

147+
// Find the path from the root of the JSON schema
148+
var hash = util.path.getHash(value.$ref);
149+
var referencedAt = pointer.$ref.referencedAt.filter(function(at) {
150+
return hash.indexOf(at.hash) === 0;
151+
})[0];
152+
153+
console.log(
154+
'referencedAt.pathFromRoot =', referencedAt.pathFromRoot,
155+
'\nreferencedAt.hash =', referencedAt.hash,
156+
'\nhash =', hash,
157+
'\npointer.path.hash =', util.path.getHash(pointer.path)
158+
);
159+
75160
// Re-map the value
76-
var new$RefPath = pointer.$ref.pathFromRoot + util.path.getHash(pointer.path).substr(1);
161+
var new$RefPath = referencedAt.pathFromRoot + util.path.getHash(pointer.path).substr(1);
77162
util.debug(' new value: %s', new$RefPath);
78163
remapped.push({
79164
old$Ref: value,
@@ -99,8 +184,9 @@ function dereference(basePath, $refs, options) {
99184

100185
Object.keys($refs._$refs).forEach(function(key) {
101186
var $ref = $refs._$refs[key];
102-
if ($ref.pathFromRoot !== '#') {
103-
$refs.set(basePath + $ref.pathFromRoot, $ref.value, options);
187+
188+
if ($ref.referencedAt.length > 0) {
189+
$refs.set(basePath + $ref.referencedAt[0].pathFromRoot, $ref.value, options);
104190
}
105191
});
106192
}

lib/dereference.js

Lines changed: 52 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -16,59 +16,42 @@ module.exports = dereference;
1616
* @param {$RefParserOptions} options
1717
*/
1818
function dereference(parser, options) {
19-
util.debug('Dereferencing $ref pointers in %s', parser._basePath);
19+
util.debug('Dereferencing $ref pointers in %s', parser.$refs._basePath);
2020
parser.$refs.circular = false;
21-
crawl(parser.schema, parser._basePath, [], parser.$refs, options);
21+
crawl(parser.schema, parser.$refs._basePath, '#', [], parser.$refs, options);
2222
}
2323

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

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

4041
Object.keys(obj).forEach(function(key) {
4142
var keyPath = Pointer.join(path, key);
43+
var keyPathFromRoot = Pointer.join(pathFromRoot, key);
4244
var value = obj[key];
4345
var circular = false;
4446

4547
if ($Ref.isAllowed$Ref(value, options)) {
46-
// We found a $ref, so resolve it
47-
util.debug('Dereferencing $ref pointer "%s" at %s', value.$ref, keyPath);
48-
var $refPath = url.resolve(path, value.$ref);
49-
var pointer = $refs._resolve($refPath, options);
50-
51-
// Check for circular references
52-
circular = pointer.circular || parents.indexOf(pointer.value) !== -1;
53-
circular && foundCircularReference(keyPath, $refs, options);
54-
55-
// Dereference the JSON reference
56-
var dereferencedValue = getDereferencedValue(value, pointer.value);
57-
58-
// Crawl the dereferenced value (unless it's circular)
59-
if (!circular) {
60-
// If the `crawl` method returns true, then dereferenced value is circular
61-
circular = crawl(dereferencedValue, pointer.path, parents, $refs, options);
62-
}
63-
64-
// Replace the JSON reference with the dereferenced value
65-
if (!circular || options.$refs.circular === true) {
66-
obj[key] = dereferencedValue;
67-
}
48+
var dereferenced = dereference$Ref(value, keyPath, keyPathFromRoot, parents, $refs, options);
49+
circular = dereferenced.circular;
50+
obj[key] = dereferenced.value;
6851
}
6952
else {
7053
if (parents.indexOf(value) === -1) {
71-
circular = crawl(value, keyPath, parents, $refs, options);
54+
circular = crawl(value, keyPath, keyPathFromRoot, parents, $refs, options);
7255
}
7356
else {
7457
circular = foundCircularReference(keyPath, $refs, options);
@@ -85,33 +68,51 @@ function crawl(obj, path, parents, $refs, options) {
8568
}
8669

8770
/**
88-
* Returns the dereferenced value of the given JSON reference.
71+
* Dereferences the given JSON Reference, and then crawls the resulting value.
8972
*
90-
* @param {object} currentValue - the current value, which contains a JSON reference ("$ref" property)
91-
* @param {*} resolvedValue - the resolved value, which can be any type
92-
* @returns {*} - Returns the dereferenced value
73+
* @param {{$ref: string}} $ref - The JSON Reference to resolve
74+
* @param {string} path - The full path of `$ref`, possibly with a JSON Pointer in the hash
75+
* @param {string} pathFromRoot - The path of `$ref` from the schema root
76+
* @param {object[]} parents - An array of the parent objects that have already been dereferenced
77+
* @param {$Refs} $refs
78+
* @param {$RefParserOptions} options
79+
* @returns {object}
9380
*/
94-
function getDereferencedValue(currentValue, resolvedValue) {
95-
if (resolvedValue && typeof resolvedValue === 'object' && Object.keys(currentValue).length > 1) {
96-
// The current value has additional properties (other than "$ref"),
97-
// so merge the resolved value rather than completely replacing the reference
98-
var merged = {};
99-
Object.keys(currentValue).forEach(function(key) {
100-
if (key !== '$ref') {
101-
merged[key] = currentValue[key];
102-
}
103-
});
104-
Object.keys(resolvedValue).forEach(function(key) {
105-
if (!(key in merged)) {
106-
merged[key] = resolvedValue[key];
107-
}
108-
});
109-
return merged;
81+
function dereference$Ref($ref, path, pathFromRoot, parents, $refs, options) {
82+
util.debug('Dereferencing $ref pointer "%s" at %s', $ref.$ref, path);
83+
84+
var $refPath = url.resolve(path, $ref.$ref);
85+
var pointer = $refs._resolve($refPath, options);
86+
87+
// Check for circular references
88+
var directCircular = pointer.circular;
89+
var circular = directCircular || parents.indexOf(pointer.value) !== -1;
90+
circular && foundCircularReference(path, $refs, options);
91+
92+
// Dereference the JSON reference
93+
var dereferencedValue = util.dereference($ref, pointer.value);
94+
95+
// Crawl the dereferenced value (unless it's circular)
96+
if (!circular) {
97+
// If the `crawl` method returns true, then dereferenced value is circular
98+
circular = crawl(dereferencedValue, pointer.path, pathFromRoot, parents, $refs, options);
11099
}
111-
else {
112-
// Completely replace the original reference with the resolved value
113-
return resolvedValue;
100+
101+
if (circular && !directCircular && options.$refs.circular === 'ignore') {
102+
// The user has chosen to "ignore" circular references, so don't change the value
103+
dereferencedValue = $ref;
104+
}
105+
106+
if (directCircular) {
107+
// The pointer is a DIRECT circular reference (i.e. it references itself).
108+
// So replace the $ref path with the absolute path from the JSON Schema root
109+
dereferencedValue.$ref = pathFromRoot;
114110
}
111+
112+
return {
113+
circular: circular,
114+
value: dereferencedValue
115+
};
115116
}
116117

117118
/**

lib/index.js

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,6 @@ function $RefParser() {
3737
* @type {$Refs}
3838
*/
3939
this.$refs = new $Refs();
40-
41-
/**
42-
* The file path or URL of the main JSON schema file.
43-
* This will be empty if the schema was passed as an object rather than a path.
44-
*
45-
* @type {string}
46-
* @protected
47-
*/
48-
this._basePath = '';
4940
}
5041

5142
/**
@@ -79,8 +70,8 @@ $RefParser.prototype.parse = function(schema, options, callback) {
7970
if (args.schema && typeof args.schema === 'object') {
8071
// The schema is an object, not a path/url
8172
this.schema = args.schema;
82-
this._basePath = '';
83-
var $ref = new $Ref(this.$refs, this._basePath);
73+
this.$refs._basePath = '';
74+
var $ref = new $Ref(this.$refs, this.$refs._basePath);
8475
$ref.setValue(this.schema, args.options);
8576

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

10192
// Read the schema file/url
10293
return read(args.schema, this.$refs, args.options)
103-
.then(function(cached$Ref) {
104-
var value = cached$Ref.$ref.value;
94+
.then(function(result) {
95+
var value = result.$ref.value;
10596
if (!value || typeof value !== 'object' || value instanceof Buffer) {
106-
throw ono.syntax('"%s" is not a valid JSON Schema', me._basePath);
97+
throw ono.syntax('"%s" is not a valid JSON Schema', me.$refs._basePath);
10798
}
10899
else {
109100
me.schema = value;

0 commit comments

Comments
 (0)