Skip to content

Commit

Permalink
Add sync version of resolveRef
Browse files Browse the repository at this point in the history
  • Loading branch information
redneckz committed Jul 27, 2023
1 parent a0ecc34 commit 88bcbc7
Show file tree
Hide file tree
Showing 11 changed files with 99 additions and 84 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"editor.formatOnSave": true
"editor.formatOnSave": true,
"java.saveActions.organizeImports": false
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@redneckz/json-ref",
"version": "0.0.4",
"version": "0.0.5",
"license": "MIT",
"author": {
"name": "redneckz",
Expand Down
2 changes: 1 addition & 1 deletion src/URIResolver.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import type { JSONNode } from './JSONNode';

export type URIResolver = (uri: string) => Promise<JSONNode> | JSONNode;
export type URIResolver<R extends Promise<JSONNode> | JSONNode = Promise<JSONNode>> = (uri: string) => R;
8 changes: 8 additions & 0 deletions src/collectRef.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { type JSONNode } from './JSONNode';
import { visitRef } from './visitRef';

export const collectRef = (json: JSONNode): string[] => {
const refs: string[] = [];
visitRef(json, _ => refs.push(_));
return refs;
};
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export { resolveRef } from './resolveRef';
export { resolveRef as resolveRefSync } from './resolveRef.sync';
export { visitRef, type RefVisitor } from './visitRef';
export { collectRef } from './collectRef';

export { resolveJPointer } from './resolveJPointer';

Expand Down
29 changes: 29 additions & 0 deletions src/mapJSONNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {
isJSONArray,
isJSONRecord,
isJSONRef,
type JSONNode,
type JSONRecord,
type JSONRef,
type JSONScalar
} from './JSONNode';

export const mapJSONNode = <R extends JSONNode | Promise<JSONNode> | void>(
json: JSONNode,
handlers: {
ref: (json: JSONRef) => R;
record: (json: JSONRecord) => R;
array: (json: JSONNode[]) => R;
scalar?: (json: JSONScalar | null) => R;
}
): R => {
if (isJSONRef(json)) {
return handlers.ref(json);
} else if (isJSONRecord(json)) {
return handlers.record(json);
} else if (isJSONArray(json)) {
return handlers.array(json);
} else {
return handlers.scalar ? handlers.scalar(json) : (json as R);
}
};
2 changes: 2 additions & 0 deletions src/mergeRecords.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const mergeRecords = <K extends string, V, R extends Record<K, V>>(entries: R[]): R =>
entries.reduce((acc, _) => Object.assign(acc, _), {} as R);
50 changes: 12 additions & 38 deletions src/resolveRef.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,66 +2,46 @@ import { resolveRef } from './resolveRef';

describe('resolveRef', () => {
it('should substitute JSON addressed by URI in place of "$ref" declared inside object', async () => {
const constResolver = () => ({ some: { remote: 'json' } });
const constResolver = async () => ({ some: { remote: 'json' } });

const result = await resolveRef(
{
foo: {
bar: { $ref: 'http://some-remote.json' },
baz: [123]
}
foo: { bar: { $ref: 'http://some-remote.json' }, baz: [123] }
},
constResolver
);

expect(result).toEqual({
foo: {
bar: { some: { remote: 'json' } },
baz: [123]
}
foo: { bar: { some: { remote: 'json' } }, baz: [123] }
});
});

it('should substitute JSON addressed by URI in place of "$ref" declared inside list', async () => {
const constResolver = () => ({ some: { remote: 'json' } });
const constResolver = async () => ({ some: { remote: 'json' } });

const result = await resolveRef(
{
foo: {
bar: 0,
baz: [123, { $ref: 'http://some-remote.json' }, 456]
}
foo: { bar: 0, baz: [123, { $ref: 'http://some-remote.json' }, 456] }
},
constResolver
);

expect(result).toEqual({
foo: {
bar: 0,
baz: [123, { some: { remote: 'json' } }, 456]
}
foo: { bar: 0, baz: [123, { some: { remote: 'json' } }, 456] }
});
});

it('should substitute JSON addressed by URI in place of "$ref" with regard to URI fragment', async () => {
const constResolver = () => ({ some: { remote: 'json' } });
const constResolver = async () => ({ some: { remote: 'json' } });

const result = await resolveRef(
{
foo: {
bar: { $ref: 'http://some-remote.json#/some/remote' },
baz: [123]
}
foo: { bar: { $ref: 'http://some-remote.json#/some/remote' }, baz: [123] }
},
constResolver
);

expect(result).toEqual({
foo: {
bar: 'json',
baz: [123]
}
});
expect(result).toEqual({ foo: { bar: 'json', baz: [123] } });
});

it('should "ask" URIResolver to resolve URI for each "$ref"', async () => {
Expand All @@ -84,24 +64,18 @@ describe('resolveRef', () => {
});

it('should recursively process "$ref" fields of fragments returned by URIresolver', async () => {
const constResolver = (uri: string) =>
const constResolver = async (uri: string) =>
uri === '#some/remote/json' ? { some: { remote: { json: { $ref: '#/foo' } } } } : 'foo';

const result = await resolveRef(
{
foo: {
bar: { $ref: '#some/remote/json' },
baz: [123]
}
foo: { bar: { $ref: '#some/remote/json' }, baz: [123] }
},
constResolver
);

expect(result).toEqual({
foo: {
bar: 'foo',
baz: [123]
}
foo: { bar: 'foo', baz: [123] }
});
});
});
15 changes: 15 additions & 0 deletions src/resolveRef.sync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { type JSONNode, type JSONRecord } from './JSONNode';
import type { URIResolver } from './URIResolver';
import { mapJSONNode } from './mapJSONNode';
import { mergeRecords } from './mergeRecords';
import { resolveJPointer } from './resolveJPointer';

export const resolveRef = (json: JSONNode, resolver: URIResolver<JSONNode>): JSONNode =>
mapJSONNode(json, {
ref: ({ $ref, ...rest }) => resolveRef($ref ? resolveJPointer(resolver($ref), $ref) : rest, resolver),
record: record =>
mergeRecords(
Object.entries(record).map(([key, value]) => ({ [key]: resolveRef(value, resolver) } as JSONRecord))
),
array: list => list.map(_ => resolveRef(_, resolver))
});
43 changes: 16 additions & 27 deletions src/resolveRef.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,19 @@
import { type JSONNode, type JSONRecord, type JSONRef, isJSONArray, isJSONRecord, isJSONRef } from './JSONNode';
import { type JSONNode, type JSONRecord } from './JSONNode';
import type { URIResolver } from './URIResolver';
import { mapJSONNode } from './mapJSONNode';
import { mergeRecords } from './mergeRecords';
import { resolveJPointer } from './resolveJPointer';

export const resolveRef = async (json: JSONNode, resolver: URIResolver): Promise<JSONNode> => {
if (isJSONRef(json)) {
return mapJSONRef(json, resolver);
} else if (isJSONRecord(json)) {
return mapJSONRecord(json, resolver);
} else if (isJSONArray(json)) {
return mapJSONArray(json, resolver);
} else {
return json;
}
};

const mapJSONArray = async (list: JSONNode[], resolver: URIResolver): Promise<JSONNode[]> =>
Promise.all(list.map(_ => resolveRef(_, resolver)));

const mapJSONRecord = async (record: JSONRecord, resolver: URIResolver): Promise<JSONRecord> =>
(
await Promise.all(
Object.entries(record).map(async ([key, value]) => ({
[key]: await resolveRef(value, resolver)
}))
)
).reduce((acc, _) => Object.assign(acc, _), {});

const mapJSONRef = async ({ $ref, ...rest }: JSONRef, resolver: URIResolver): Promise<JSONNode> =>
resolveRef($ref ? resolveJPointer(await resolver($ref), $ref) : rest, resolver);
export const resolveRef = async (json: JSONNode, resolver: URIResolver): Promise<JSONNode> =>
mapJSONNode(json, {
ref: async ({ $ref, ...rest }) => resolveRef($ref ? resolveJPointer(await resolver($ref), $ref) : rest, resolver),
record: async record =>
mergeRecords(
await Promise.all(
Object.entries(record).map(
async ([key, value]) => ({ [key]: await resolveRef(value, resolver) } as JSONRecord)
)
)
),
array: async list => Promise.all(list.map(_ => resolveRef(_, resolver)))
});
27 changes: 11 additions & 16 deletions src/visitRef.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
import { type JSONNode, isJSONArray, isJSONRecord, isJSONRef, type JSONRecord } from './JSONNode';
import { type JSONNode } from './JSONNode';
import { mapJSONNode } from './mapJSONNode';

type JSONNodePath = (string | number)[];
export type RefVisitor = (ref: string, path: JSONNodePath) => void;

export const visitRef = (json: JSONNode, visitor: RefVisitor, path: JSONNodePath = []): void => {
if (isJSONRef(json)) {
visitor(json.$ref, path);
} else if (isJSONRecord(json)) {
visitJSONRecord(json, visitor, path);
} else if (isJSONArray(json)) {
visitJSONArray(json, visitor, path);
}
};

const visitJSONRecord = (record: JSONRecord, visitor: RefVisitor, path: JSONNodePath): void => {
for (const key in record) visitRef(record[key], visitor, [...path, key]);
};

const visitJSONArray = (list: JSONNode[], visitor: RefVisitor, path: JSONNodePath): void => {
for (let i = 0; i < list.length; i++) visitRef(list[i], visitor, [...path, i]);
mapJSONNode(json, {
ref: json => visitor(json.$ref, path),
record: record => {
for (const key in record) visitRef(record[key], visitor, [...path, key]);
},
array: list => {
for (let i = 0; i < list.length; i++) visitRef(list[i], visitor, [...path, i]);
}
});
};

0 comments on commit 88bcbc7

Please sign in to comment.