Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 65 additions & 141 deletions lib/dereference.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,175 +16,99 @@ module.exports = dereference;
*/
function dereference (parser, options) {
// console.log('Dereferencing $ref pointers in %s', parser.$refs._root$Ref.path);
let dereferenced = crawl(parser.schema, parser.$refs._root$Ref.path, "#", new Set(), new Set(), new Map(), parser.$refs, options);
parser.$refs.circular = dereferenced.circular;
parser.schema = dereferenced.value;
const result = crawl(parser.schema, parser.$refs._root$Ref.path, "#", new Set(), [], new Map(), parser.$refs, options);
parser.schema = result.value;
}

const mergeRefObject = (refObject, newObject) => {
const refKeys = Object.keys(refObject);
const extraKeys = {};
if (refKeys.length > 1) {
for (let key of refKeys) {
if (key !== "$ref" && !(key in newObject)) {
extraKeys[key] = refObject[key];
}
}
return Object.assign({}, newObject, extraKeys);
}
return newObject;
};

/**
* 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 full path of `obj`, possibly with a JSON Pointer in the hash
* @param {string} pathFromRoot - The path of `obj` from the schema root
* @param {Set<object>} parents - An array of the parent objects that have already been dereferenced
* @param {Set<object>} processedObjects - An array of all the objects that have already been processed
* @param {array<string>} pathList - An array of the list of parents reference points for error handling
* @param {Map<string,object>} dereferencedCache - An map of all the dereferenced objects
* @param {$Refs} $refs
* @param {$RefParserOptions} options
* @returns {{value: object, circular: boolean}}
* @returns {object}
*/
function crawl (obj, path, pathFromRoot, parents, processedObjects, dereferencedCache, $refs, options) {
let dereferenced;
let result = {
value: obj,
circular: false
};

let isExcludedPath = options.dereference.excludedPathMatcher;

if (options.dereference.circular === "ignore" || !processedObjects.has(obj)) {
if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj) && !isExcludedPath(pathFromRoot)) {
parents.add(obj);
processedObjects.add(obj);

if ($Ref.isAllowed$Ref(obj, options)) {
dereferenced = dereference$Ref(obj, path, pathFromRoot, parents, processedObjects, dereferencedCache, $refs, options);
result.circular = dereferenced.circular;
result.value = dereferenced.value;
}
else {
for (const key of Object.keys(obj)) {
let keyPath = Pointer.join(path, key);
let keyPathFromRoot = Pointer.join(pathFromRoot, key);

if (isExcludedPath(keyPathFromRoot)) {
continue;
}

let value = obj[key];
let circular = false;

if ($Ref.isAllowed$Ref(value, options)) {
dereferenced = dereference$Ref(value, keyPath, keyPathFromRoot, parents, processedObjects, dereferencedCache, $refs, options);
circular = dereferenced.circular;
// Avoid pointless mutations; breaks frozen objects to no profit
if (obj[key] !== dereferenced.value) {
obj[key] = dereferenced.value;
}
}
else {
if (!parents.has(value)) {
dereferenced = crawl(value, keyPath, keyPathFromRoot, parents, processedObjects, dereferencedCache, $refs, options);
circular = dereferenced.circular;
// Avoid pointless mutations; breaks frozen objects to no profit
if (obj[key] !== dereferenced.value) {
obj[key] = dereferenced.value;
}
}
else {
circular = foundCircularReference(keyPath, $refs, options);
}
}

// Set the "isCircular" flag if this or any other property is circular
result.circular = result.circular || circular;
}
}

parents.delete(obj);
}
function crawl (obj, path, pathFromRoot, parents, pathList, dereferencedCache, $refs, options) {
if (!obj || Array.isArray(obj) || typeof obj !== "object" || ArrayBuffer.isView(obj) || options.dereference.excludedPathMatcher(pathFromRoot)) {
return { value: obj, circular: false };
}

return result;
}

/**
* Dereferences the given JSON Reference, and then crawls the resulting 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 {Set<object>} parents - An array of the parent objects that have already been dereferenced
* @param {Set<object>} processedObjects - An array of all the objects that have already been dereferenced
* @param {Map<string,object>} dereferencedCache - An map of all the dereferenced objects
* @param {$Refs} $refs
* @param {$RefParserOptions} options
* @returns {{value: object, circular: boolean}}
*/
function dereference$Ref ($ref, path, pathFromRoot, parents, processedObjects, dereferencedCache, $refs, options) {
// console.log('Dereferencing $ref pointer "%s" at %s', $ref.$ref, path);

let $refPath = url.resolve(path, $ref.$ref);

const cache = dereferencedCache.get($refPath);
if (cache) {
const refKeys = Object.keys($ref);
if (refKeys.length > 1) {
const extraKeys = {};
for (let key of refKeys) {
if (key !== "$ref" && !(key in cache.value)) {
extraKeys[key] = $ref[key];
}
}
return {
circular: cache.circular,
value: Object.assign({}, cache.value, extraKeys),
};
}

return cache;
if (parents.has(obj)) {
foundCircularReference(pathList.pop(), $refs, options);
return { value: obj, circular: true };
}

if ($Ref.isAllowed$Ref(obj, options)) {
const $refObject = obj;
let $refPath = url.resolve(path, $refObject.$ref);

let pointer = $refs._resolve($refPath, path, options);
const cachedPathFromRoot = dereferencedCache.get($refPath);

if (pointer === null) {
return {
circular: false,
value: null,
};
}
const pointer = $refs._resolve($refPath, path, options);
if (!pointer) {
return { value: null };
}

// Check for circular references
let directCircular = pointer.circular;
let circular = directCircular || parents.has(pointer.value);
circular && foundCircularReference(path, $refs, options);
// Dereference the JSON reference
let dereferencedValue = mergeRefObject($refObject, $Ref.dereference($refObject, pointer.value));

// Dereference the JSON reference
let dereferencedValue = $Ref.dereference($ref, pointer.value);
if (pointer.circular) {
// 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;

// Crawl the dereferenced value (unless it's circular)
if (!circular) {
// Determine if the dereferenced value is circular
let dereferenced = crawl(dereferencedValue, pointer.path, pathFromRoot, parents, processedObjects, dereferencedCache, $refs, options);
circular = dereferenced.circular;
dereferencedValue = dereferenced.value;
}
foundCircularReference(path, $refs, options);
return { value: dereferencedValue, circular: true };
}

if (circular && !directCircular && options.dereference.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;
}
// only cache if no extra properties than $ref
if (Object.keys($refObject).length === 1) {
dereferencedCache.set($refPath, pathFromRoot);
}

const result = crawl(dereferencedValue, pointer.path, pathFromRoot, new Set(parents).add(obj), pathList.concat(path), dereferencedCache, $refs, options);
if (result.circular && options.dereference.circular === "ignore") {
return {
circular: false,
value: {
...$refObject,
$circularRef: cachedPathFromRoot || $refObject.$ref
}
};
}
return result;
}

const dereferencedObject = {
circular,
value: dereferencedValue
};
let circular;
for (const key of Object.keys(obj)) {
let keyPath = Pointer.join(path, key);
let keyPathFromRoot = Pointer.join(pathFromRoot, key);

// only cache if no extra properties than $ref
if (Object.keys($ref).length === 1) {
dereferencedCache.set($refPath, dereferencedObject);
const result = crawl(obj[key], keyPath, keyPathFromRoot, new Set(parents).add(obj), pathList.concat(path), dereferencedCache, $refs, options);
circular = circular || result.circular;
obj[key] = result.value;
}

return dereferencedObject;
return { value: obj, circular };
}

/**
Expand Down
8 changes: 4 additions & 4 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,8 @@ declare class $RefParser {
declare namespace $RefParser {

export type JSONSchema = JSONSchema4 | JSONSchema6 | JSONSchema7;
export type SchemaCallback = (err: Error | null, schema?: JSONSchema) => any;
export type $RefsCallback = (err: Error | null, $refs?: $Refs) => any;
export type SchemaCallback = (err: Error | null, schema?: JSONSchema) => unknown;
export type $RefsCallback = (err: Error | null, $refs?: $Refs) => unknown;

/**
* See https://apitools.dev/json-schema-ref-parser/docs/options.html
Expand Down Expand Up @@ -290,7 +290,7 @@ declare namespace $RefParser {
*/
read(
file: FileInfo,
callback?: (error: Error | null, data: string | null) => any
callback?: (error: Error | null, data: string | null) => unknown
): string | Buffer | JSONSchema | Promise<string | Buffer | JSONSchema>;
}

Expand Down Expand Up @@ -324,7 +324,7 @@ declare namespace $RefParser {
*/
parse(
file: FileInfo,
callback?: (error: Error | null, data: string | null) => any
callback?: (error: Error | null, data: string | null) => unknown
): unknown | Promise<unknown>;
}

Expand Down
9 changes: 5 additions & 4 deletions test/specs/absolute-root/absolute-root.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,12 @@ describe("When executed in the context of root directory", () => {
expect(schema).to.deep.equal(dereferencedSchema);
// Reference equality
expect(schema.properties.name).to.equal(schema.definitions.name);

expect(schema.definitions["required string"])
.to.equal(schema.definitions.name.properties.first)
.to.equal(schema.definitions.name.properties.last)
.to.equal(schema.properties.name.properties.first)
.to.equal(schema.properties.name.properties.last);
.to.eql(schema.definitions.name.properties.first)
.to.eql(schema.definitions.name.properties.last)
.to.eql(schema.properties.name.properties.first)
.to.eql(schema.properties.name.properties.last);
// The "circular" flag should NOT be set
expect(parser.$refs.circular).to.equal(false);
});
Expand Down
6 changes: 3 additions & 3 deletions test/specs/circular-extended/circular-extended.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ describe("Schema with circular $refs that extend each other", () => {
// A ReferenceError should have been thrown
expect(err).to.be.an.instanceOf(ReferenceError);
expect(err.message).to.contain("Circular $ref pointer found at ");
expect(err.message).to.contain("specs/circular-extended/definitions/person-with-spouse.yaml#/properties/spouse");
expect(err.message).to.contain("specs/circular-extended/definitions/person-with-spouse.yaml");

// $Refs.circular should be true
expect(parser.$refs.circular).to.equal(true);
Expand Down Expand Up @@ -203,7 +203,7 @@ describe("Schema with circular $refs that extend each other", () => {
// A ReferenceError should have been thrown
expect(err).to.be.an.instanceOf(ReferenceError);
expect(err.message).to.contain("Circular $ref pointer found at ");
expect(err.message).to.contain("specs/circular-extended/definitions/child-with-parents.yaml#/properties/parents/items");
expect(err.message).to.contain("specs/circular-extended/definitions/parent-with-children.yaml");

// $Refs.circular should be true
expect(parser.$refs.circular).to.equal(true);
Expand Down Expand Up @@ -278,7 +278,7 @@ describe("Schema with circular $refs that extend each other", () => {
// A ReferenceError should have been thrown
expect(err).to.be.an.instanceOf(ReferenceError);
expect(err.message).to.contain("Circular $ref pointer found at ");
expect(err.message).to.contain("specs/circular-extended/definitions/child-with-children.yaml#/properties");
expect(err.message).to.contain("specs/circular-extended/definitions/child-with-children.yaml");

// $Refs.circular should be true
expect(parser.$refs.circular).to.equal(true);
Expand Down
Loading