Skip to content

Commit

Permalink
feat: add resolveInlineRefWithLocation util (#106)
Browse files Browse the repository at this point in the history
  • Loading branch information
P0lip authored Mar 29, 2022
1 parent 91cd1c3 commit 1b68c5c
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 16 deletions.
16 changes: 15 additions & 1 deletion src/__tests__/resolveInlineRef.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { resolveInlineRef } from '../resolveInlineRef';
import { resolveInlineRef, resolveInlineRefWithLocation } from '../resolveInlineRef';

describe('resolveInlineRef', () => {
test('should follow refs', () => {
Expand All @@ -20,6 +20,7 @@ describe('resolveInlineRef', () => {
};

expect(resolveInlineRef(doc, '#/a')).toEqual('woo!');
expect(resolveInlineRefWithLocation(doc, '#/a')).toHaveProperty('location', ['d', '0', 'foo']);
});

test('should follow refs #2', () => {
Expand All @@ -42,6 +43,7 @@ describe('resolveInlineRef', () => {
};

expect(resolveInlineRef(doc, '#/a')).toEqual('woo!');
expect(resolveInlineRefWithLocation(doc, '#/a')).toHaveProperty('location', ['e']);
});

test('should handle direct circular refs', () => {
Expand All @@ -57,6 +59,7 @@ describe('resolveInlineRef', () => {
expect(resolveInlineRef(doc, '#/a')).toEqual({
$ref: '#/a',
});
expect(resolveInlineRefWithLocation(doc, '#/a')).toHaveProperty('location', ['b']);
});

test('should handle direct circular refs #2', () => {
Expand All @@ -83,6 +86,7 @@ describe('resolveInlineRef', () => {
expect(resolveInlineRef(doc, '#/a')).toEqual({
$ref: '#/a',
});
expect(resolveInlineRefWithLocation(doc, '#/a')).toHaveProperty('location', ['e']);
});

test('given external reference, should throw', () => {
Expand Down Expand Up @@ -172,16 +176,26 @@ describe('resolveInlineRef', () => {
description: 'Apparently Tom likes bears',
});

expect(resolveInlineRefWithLocation(doc, '#/properties/caves/contains')).toHaveProperty('location', [
'$defs',
'Cave',
]);

expect(resolveInlineRef(doc, '#/properties/greatestBear')).toStrictEqual({
type: 'string',
description: 'The greatest bear!',
summary: "Tom's favorite bear",
});
expect(resolveInlineRefWithLocation(doc, '#/properties/greatestBear')).toHaveProperty('location', [
'$defs',
'Bear',
]);

expect(resolveInlineRef(doc, '#/properties/bestBear')).toStrictEqual({
type: 'string',
summary: 'The best bear!',
});
expect(resolveInlineRefWithLocation(doc, '#/properties/bestBear')).toHaveProperty('location', ['$defs', 'Bear']);
});
});
});
55 changes: 40 additions & 15 deletions src/resolveInlineRef.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,81 @@
import { Dictionary } from '@stoplight/types';
import { Dictionary, JsonPath } from '@stoplight/types';
import { extractSourceFromRef } from './extractSourceFromRef';
import { isPlainObject } from './isPlainObject';
import { pointerToPath } from './pointerToPath';

function _resolveInlineRef(document: Dictionary<unknown>, ref: string, seen: unknown[]): unknown {
function _resolveInlineRef(
document: Dictionary<unknown>,
ref: string,
seen: unknown[],
prevLocation?: JsonPath,
): { location: JsonPath; value: unknown } {
const source = extractSourceFromRef(ref);
if (source !== null) {
throw new ReferenceError('Cannot resolve external references');
}

const path = pointerToPath(ref);
let location = path;
let value: unknown = document;
for (const segment of path) {
for (const [i, segment] of path.entries()) {
if ((!isPlainObject(value) && !Array.isArray(value)) || !(segment in value)) {
throw new ReferenceError(`Could not resolve '${ref}'`);
}

value = value[segment];

if (isPlainObject(value) && '$ref' in value) {
if (typeof value.$ref !== 'string') {
throw new TypeError('$ref should be a string');
}

if (seen.includes(value)) {
// circular, let's stop
return seen[seen.length - 1];
return {
location: prevLocation || location,
value: seen[seen.length - 1],
};
}

seen.push(value);

if (typeof value.$ref !== 'string') {
throw new TypeError('$ref should be a string');
}

value = _resolveInlineRef(document, value.$ref, seen);
({ value, location } = _resolveInlineRef(document, value.$ref, seen, location));
location.push(...path.slice(i + 1));
}
}

if (seen.length === 0) {
return value;
return {
location,
value,
};
}

const sourceDocument = seen[seen.length - 1];

if (isPlainObject(sourceDocument) && ('summary' in sourceDocument || 'description' in sourceDocument)) {
return {
...value,
...('description' in sourceDocument ? { description: sourceDocument.description } : null),
...('summary' in sourceDocument ? { summary: sourceDocument.summary } : null),
location,
value: {
...value,
...('description' in sourceDocument ? { description: sourceDocument.description } : null),
...('summary' in sourceDocument ? { summary: sourceDocument.summary } : null),
},
};
}

return value;
return {
location,
value,
};
}

export function resolveInlineRef(document: Dictionary<unknown>, ref: string): unknown {
return _resolveInlineRef(document, ref, []).value;
}

export function resolveInlineRefWithLocation(
document: Dictionary<unknown>,
ref: string,
): { location: JsonPath; value: unknown } {
return _resolveInlineRef(document, ref, []);
}

0 comments on commit 1b68c5c

Please sign in to comment.