diff --git a/README.md b/README.md index 82ccfcf..5eb931d 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ const referenceResolver = require("@json-schema-tools/reference-resolver").defau referenceResolver("#/properties/foo", { properties: { foo: 123 } }); // returns '123' referenceResolver("https://foo.com/", {}); // returns what ever json foo.com returns referenceResolver("../my-object.json", {}); // you get teh idea - +referenceResolver("custom-reference", {}, (ref) => { /* some logic to retrieve reference JSON */ }); // returns resolved reference using customLoader to load json definitions ``` ### Contributing diff --git a/src/index.test.ts b/src/index.test.ts index fac48ce..f482e22 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,5 +1,5 @@ import referenceResolver from "./index"; -import { NonJsonRefError, InvalidJsonPointerRefError, InvalidFileSystemPathError, InvalidRemoteURLError } from "./reference-resolver"; +import { NonJsonRefError, InvalidJsonPointerRefError, InvalidFileSystemPathError, InvalidRemoteURLError, CustomLoaderError } from "./reference-resolver"; describe("referenceResolver", () => { @@ -21,6 +21,16 @@ describe("referenceResolver", () => { expect(resolvedRef.title).toBe("JSONSchema"); }); + it("custom loader", async () => { + const schema = { title: 'custom protocol' }; + const resolvedRef = await referenceResolver( + "custom-reference", + {}, + () => Promise.resolve(schema) + ); + expect(resolvedRef).toStrictEqual(schema); + }); + it("errors on non-json", async () => { expect.assertions(1); try { @@ -86,6 +96,15 @@ describe("referenceResolver", () => { expect(e).toBeInstanceOf(InvalidRemoteURLError); } }); + + it("errors if custom loader fails load", async () => { + expect.assertions(1); + try { + await referenceResolver("custom-reference", {}, () => Promise.reject()); + } catch (e) { + expect(e).toBeInstanceOf(CustomLoaderError); + } + }) }); diff --git a/src/reference-resolver.ts b/src/reference-resolver.ts index 7107f6a..140e62c 100644 --- a/src/reference-resolver.ts +++ b/src/reference-resolver.ts @@ -86,6 +86,19 @@ export class InvalidFileSystemPathError implements Error { } } +export class CustomLoaderError implements Error { + public name: string; + public message: string; + + constructor(ref: string, public innerError: any) { + this.name = "CusromLoaderError"; + this.message = [ + "CusromLoaderError", + `Provided custom loader did not load: ${ref}`, + ].join("\n"); + } +} + const isUrlLike = (s: string) => { return s.includes("://") || s.includes("localhost:"); } @@ -107,7 +120,12 @@ export class InvalidRemoteURLError implements Error { } } -export default (fetch: any, fs: any) => { +interface ReferenceLoader { + canFetch(path: string): Promise; + fetch(path: string): Promise; +} + +function createFileSystemReferenceLoader(fs: any) { const fileExistsAndReadable = (f: string): Promise => { return new Promise((resolve) => { return fs.access(f, fs.constants.F_OK | fs.constants.R_OK, (e: any) => { //tslint:disable-line @@ -121,6 +139,62 @@ export default (fetch: any, fs: any) => { return new Promise((resolve) => fs.readFile(f, "utf8", (err: any, data: any) => resolve(data))); }; + return { + canFetch: fileExistsAndReadable, + fetch: async (path: string) => { + const fileContents = await readFile(path); + + try { + return JSON.parse(fileContents); + } catch (e) { + throw new NonJsonRefError({ $ref: path }, fileContents); + } + } + }; +} + +function createHttpReferenceLoader(fetch: any) { + return { + canFetch: (path: string) => Promise.resolve(isUrlLike(path)), + fetch: async (path: string) => { + let fetchedContent; + try { + fetchedContent = await fetch(path); + return await fetchedContent.json(); + } catch (e) { + throw new InvalidRemoteURLError(path); + } + } + }; +} + +function createCustomReferenceLoader(loader: (path: string) => Promise): ReferenceLoader { + return { + canFetch: async () => true, + fetch: async (path) => { + try { + return await loader(path); + } catch (e) { + throw new CustomLoaderError(path, e); + } + } + } +} + +async function loadReference(path: string, ...loaders: ReferenceLoader[]) { + for (const loader of loaders) { + if (await loader.canFetch(path)) { + return await loader.fetch(path); + } + } + + throw new InvalidFileSystemPathError(path); +} + +export default (fetch: any, fs: any) => { + const fileSystemReferenceLoader = createFileSystemReferenceLoader(fs); + const httpReferenceLoader = createHttpReferenceLoader(fetch); + const resolvePointer = (ref: string, root: any): any => { try { const withoutHash = ref.replace("#", ""); @@ -135,7 +209,7 @@ export default (fetch: any, fs: any) => { * Given a $ref string, it will return the underlying pointed-to value. * For remote references, the root object is not used. */ - const resolveReference = async (ref: string, root: any): Promise => { + const resolveReference = async (ref: string, root: any, loader?: (path: string) => Promise): Promise => { if (ref[0] === "#") { return Promise.resolve(resolvePointer(ref, root)); } @@ -147,38 +221,20 @@ export default (fetch: any, fs: any) => { } const hashlessRef = hashFragmentSplit[0]; - - if (await fileExistsAndReadable(hashlessRef) === true) { - const fileContents = await readFile(hashlessRef); - let reffedSchema; - try { - reffedSchema = JSON.parse(fileContents); - } catch (e) { - throw new NonJsonRefError({ $ref: ref }, fileContents); - } - - if (hashFragment) { - reffedSchema = resolvePointer(hashFragment, reffedSchema); - } - - return reffedSchema; - } else if (isUrlLike(ref) === false) { - throw new InvalidFileSystemPathError(ref); - } - - let result; - try { - result = await fetch(hashlessRef).then((r: any) => r.json()); - } catch (e) { - throw new InvalidRemoteURLError(ref); + let reffedSchema; + if (loader) { + const customRefernceLoader = createCustomReferenceLoader(loader); + reffedSchema = await loadReference(hashlessRef, customRefernceLoader); + } else { + reffedSchema = await loadReference(hashlessRef, fileSystemReferenceLoader, httpReferenceLoader); } if (hashFragment) { - result = resolvePointer(hashFragment, result); + reffedSchema = resolvePointer(hashFragment, reffedSchema); } - return result; + return reffedSchema; }; return resolveReference; -}; +}; \ No newline at end of file