Skip to content

feat: add support of custom communication protocols #146

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 20 additions & 1 deletion src/index.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {

Expand All @@ -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 {
Expand Down Expand Up @@ -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);
}
})
});


Expand Down
114 changes: 85 additions & 29 deletions src/reference-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:");
}
Expand All @@ -107,7 +120,12 @@ export class InvalidRemoteURLError implements Error {
}
}

export default (fetch: any, fs: any) => {
interface ReferenceLoader {
canFetch(path: string): Promise<boolean>;
fetch(path: string): Promise<any>;
}

function createFileSystemReferenceLoader(fs: any) {
const fileExistsAndReadable = (f: string): Promise<boolean> => {
return new Promise((resolve) => {
return fs.access(f, fs.constants.F_OK | fs.constants.R_OK, (e: any) => { //tslint:disable-line
Expand All @@ -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<any>): 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("#", "");
Expand All @@ -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<any> => {
const resolveReference = async (ref: string, root: any, loader?: (path: string) => Promise<any>): Promise<any> => {
if (ref[0] === "#") {
return Promise.resolve(resolvePointer(ref, root));
}
Expand All @@ -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;
};
};