Skip to content
Merged
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
36 changes: 31 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,48 @@

Takes a $ref string and a root object, and returns the referenced value.

Works in browser & in node (file system refs ignored in browser)
Works in browser & in node (file system refs ignored in browser).

Easily add support for your own protocols.

## Getting Started

`npm install @json-schema-tools/reference-resolver`

```typescript
const referenceResolver = require("@json-schema-tools/reference-resolver").default;
import refRes from "@json-schema-tools/reference-resolver";

refRes.resolve("#/properties/foo", { properties: { foo: true } }); // returns true
refRes.resolve("https://foo.com/"); // returns what ever json foo.com returns
refRef.resolve("../my-object.json"); // you get teh idea
```

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
## Adding custom protocol handlers

```typescript
import referenceResolver from "@json-schema-tools/reference-resolver";
import JSONSchema from "@json-schema-tools/meta-schema";

referenceResolver.protocolHandlerMap.ipfs = (uri) => {
const pretendItsFetchedFromIpfs = {
title: "foo",
type: "string",
} as JSONSchema;
return Promise.resolve(fetchedFromIpfs);
};

referenceResolver.protocolHandlerMap["customprotocol"] = (uri) => {
return Promise.resolve({
type: "string",
title: uri.replace("customprotocol://", ""),
});
};

referenceResolver.resolve("ipfs://80088008800880088008");
referenceResolver.resolve("customprotocol://foobar");
```


### Contributing

How to contribute, build and release are outlined in [CONTRIBUTING.md](CONTRIBUTING.md), [BUILDING.md](BUILDING.md) and [RELEASING.md](RELEASING.md) respectively. Commits in this repository follow the [CONVENTIONAL_COMMITS.md](CONVENTIONAL_COMMITS.md) specification.
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
],
"devDependencies": {
"@types/isomorphic-fetch": "0.0.35",
"@json-schema-tools/meta-schema": "^1.5.10",
"@types/jest": "^26.0.14",
"@types/node": "^14.11.10",
"jest": "^24.9.0",
Expand Down
24 changes: 24 additions & 0 deletions src/default-protocol-handler-map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ProtocolHandlerMap } from "./reference-resolver";
import { InvalidRemoteURLError, NonJsonRefError } from "./errors";
import { JSONSchema } from "@json-schema-tools/meta-schema";
import fetch from "isomorphic-fetch";

const fetchHandler = async (uri: string): Promise<JSONSchema> => {
let schemaReq;
try {
schemaReq = await fetch(uri);
} catch (e) {
throw new InvalidRemoteURLError(uri);
}

try {
return await schemaReq.json() as JSONSchema;
} catch (e) {
throw new NonJsonRefError({ $ref: uri }, e.message);
}
};

export default {
"https": fetchHandler,
"http": fetchHandler,
} as ProtocolHandlerMap;
89 changes: 89 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* Error thrown when the fetched reference is not properly formatted JSON or is encoded
* incorrectly
*
* @example
* ```typescript
*
* import Dereferencer, { NonJsonRefError } from "@json-schema-tools/dereferencer";
* const dereffer = new Dereferencer({});
* try { await dereffer.resolve(); }
* catch(e) {
* if (e instanceof NonJsonRefError) { ... }
* }
* ```
*
*/
export class NonJsonRefError implements Error {
public message: string;
public name: string;

constructor(obj: any, nonJson: string) {
this.name = "NonJsonRefError";
this.message = [
"NonJsonRefError",
`The resolved value at the reference: ${obj.$ref} was not JSON.parse 'able`,
`The non-json content in question: ${nonJson}`,
].join("\n");
}
}


export class NotResolvableError implements Error {
public message: string;
public name: string;

constructor(ref: string) {
this.name = "NotResolvableError";
this.message = [
"NotResolvableError",
`Could not resolve the reference: ${ref}`,
`No protocol handler was found, and it was not found to be an internal reference`,
].join("\n");
}
}

/**
* Error thrown when given an invalid file system path as a reference.
*
*/
export class InvalidRemoteURLError implements Error {
public message: string;
public name: string;

constructor(ref: string) {
this.name = "InvalidRemoteURLError";
this.message = [
"InvalidRemoteURLError",
`The url was not resolvable: ${ref}`,
].join("\n");
}
}

/**
* Error thrown when given an invalid file system path as a reference.
*
* @example
* ```typescript
*
* import Dereferencer, { InvalidFileSystemPathError } from "@json-schema-tools/dereferencer";
* const dereffer = new Dereferencer({});
* try { await dereffer.resolve(); }
* catch(e) {
* if (e instanceof InvalidFileSystemPathError) { ... }
* }
* ```
*
*/
export class InvalidFileSystemPathError implements Error {
public name: string;
public message: string;

constructor(ref: string) {
this.name = "InvalidFileSystemPathError";
this.message = [
"InvalidFileSystemPathError",
`The path was not resolvable: ${ref}`,
].join("\n");
}
}
14 changes: 8 additions & 6 deletions src/index-web.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import buildReferenceResolver from "./reference-resolver";
import fetch from "isomorphic-fetch";
import defaultProtocolHandlerMap from "./default-protocol-handler-map";
import ReferenceResolver, { ProtocolHandlerMap } from "./reference-resolver";

export default buildReferenceResolver(fetch, {
access: (a: any, b: any, cb: (e: Error) => any) => cb(new Error("cant resolve file refs in a browser... yet")),
readFile: (a: any, b: any, cb: () => any) => { return cb(); },
constants: { F_OK: 0, R_OK: 0 } //tslint:disable-line
});
const nodeProtocolHandlerMap: ProtocolHandlerMap = {
...defaultProtocolHandlerMap,
"file": async () => undefined
};

export default new ReferenceResolver(nodeProtocolHandlerMap);
60 changes: 38 additions & 22 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,31 @@
import { JSONSchema, JSONSchemaObject } from "@json-schema-tools/meta-schema";
import { InvalidFileSystemPathError, InvalidRemoteURLError, NonJsonRefError } from "./errors";
import referenceResolver from "./index";
import { NonJsonRefError, InvalidJsonPointerRefError, InvalidFileSystemPathError, InvalidRemoteURLError } from "./reference-resolver";
import { InvalidJsonPointerRefError } from "./resolve-pointer";

describe("referenceResolver", () => {

it("simple", async () => {
const resolvedRef = await referenceResolver("#/properties/foo", { properties: { foo: "boo" } });
const resolvedRef = await referenceResolver.resolve("#/properties/foo", { properties: { foo: "boo" } });
expect(resolvedRef).toBe("boo");
});

it("file", async () => {
const resolvedRef = await referenceResolver("./src/test-obj.json", {});
const resolvedRef = await referenceResolver.resolve("./src/test-obj.json");
expect(resolvedRef).toEqual({ type: "string" });
});

it("https uri", async () => {
const resolvedRef = await referenceResolver(
"https://raw.githubusercontent.com/json-schema-tools/meta-schema/master/src/schema.json",
{},
);
const uri = "https://meta.json-schema.tools";
const resolvedRef = await referenceResolver.resolve(uri) as JSONSchemaObject;

expect(resolvedRef.title).toBe("JSONSchema");
});

it("errors on non-json", async () => {
expect.assertions(1);
try {
await referenceResolver("./src/test-non-json.json", {});
await referenceResolver.resolve("./src/test-non-json.json");
} catch (e) {
expect(e).toBeInstanceOf(NonJsonRefError);
}
Expand All @@ -33,7 +34,7 @@ describe("referenceResolver", () => {
it("errors on bad json pointer ref", async () => {
expect.assertions(1);
try {
await referenceResolver("#/nope", { foo: { bar: true } });
await referenceResolver.resolve("#/nope", { foo: { bar: true } });
} catch (e) {
expect(e).toBeInstanceOf(InvalidJsonPointerRefError);
}
Expand All @@ -42,7 +43,7 @@ describe("referenceResolver", () => {
it("errors if file cant be found", async () => {
expect.assertions(1);
try {
await referenceResolver("../not-real-file", {});
await referenceResolver.resolve("../not-real-file");
} catch (e) {
expect(e).toBeInstanceOf(InvalidFileSystemPathError);
}
Expand All @@ -51,28 +52,29 @@ describe("referenceResolver", () => {
it("files are not relative to the src folder", async () => {
expect.assertions(1);
try {
await referenceResolver("test-schema-1.json", {});
await referenceResolver.resolve("test-schema-1.json");
} catch (e) {
expect(e).toBeInstanceOf(InvalidFileSystemPathError);
}
});

it("files are relative to the folder the script is run from (in this case, project root)", async () => {
expect.assertions(1);
const reffed = await referenceResolver("src/test-schema-1.json", {});
const reffed = await referenceResolver.resolve("src/test-schema-1.json");
expect(reffed).toBeDefined();
});

it("works with nested folders when using relative file path & no prefixing", async () => {
expect.assertions(1);
const resolved = await referenceResolver("nestedtest/test-schema-1.json", {});
const resolved = await referenceResolver
.resolve("nestedtest/test-schema-1.json") as JSONSchemaObject;
expect(resolved.$ref).toBe("./src/test-schema.json");
});

it("errors on urls that arent real", async () => {
expect.assertions(1);
try {
await referenceResolver("https://not.real.at.all", {});
await referenceResolver.resolve("https://not.real.at.all");
} catch (e) {
expect(e).toBeInstanceOf(InvalidRemoteURLError);
}
Expand All @@ -81,9 +83,9 @@ describe("referenceResolver", () => {
it("errors on urls that dont return json", async () => {
expect.assertions(1);
try {
await referenceResolver("https://open-rpc.org/", {});
await referenceResolver.resolve("https://open-rpc.org/");
} catch (e) {
expect(e).toBeInstanceOf(InvalidRemoteURLError);
expect(e).toBeInstanceOf(NonJsonRefError);
}
});
});
Expand All @@ -92,13 +94,14 @@ describe("referenceResolver", () => {
describe("refs with hash fragment / internal reference component", () => {
describe("files", () => {
it("works in simple case", async () => {
expect(await referenceResolver("./src/test-obj.json#/type", {})).toBe("string");
expect(await referenceResolver.resolve("./src/test-obj.json#/type"))
.toBe("string");
});

it("errors when the json pointer is invalid", async () => {
expect.assertions(1);
try {
await referenceResolver("./src/test-obj.json#balony", {});
await referenceResolver.resolve("./src/test-obj.json#balony");
} catch (e) {
expect(e).toBeInstanceOf(InvalidJsonPointerRefError);
}
Expand All @@ -107,16 +110,16 @@ describe("refs with hash fragment / internal reference component", () => {

describe("urls", () => {
it("works with forward slashes surrounding the hash", async () => {
expect(await referenceResolver("https://meta.open-rpc.org/#/type", {})).toBe("object");
expect(await referenceResolver.resolve("https://meta.open-rpc.org/#/type")).toBe("object");
});
it("works without slash infront of hash, but with one after", async () => {
expect(await referenceResolver("https://meta.open-rpc.org#/type", {})).toBe("object");
expect(await referenceResolver.resolve("https://meta.open-rpc.org#/type")).toBe("object");
});

it("errors when the json pointer is invalid", async () => {
expect.assertions(1);
try {
await referenceResolver("https://meta.open-rpc.org/#type", {});
await referenceResolver.resolve("https://meta.open-rpc.org/#type");
} catch (e) {
expect(e).toBeInstanceOf(InvalidJsonPointerRefError);
}
Expand All @@ -125,10 +128,23 @@ describe("refs with hash fragment / internal reference component", () => {
it("errors when you have 2 hash fragments in 1 ref", async () => {
expect.assertions(1);
try {
await referenceResolver("https://meta.open-rpc.org/#properties/#openrpc", {});
await referenceResolver.resolve("https://meta.open-rpc.org/#properties/#openrpc", {});
} catch (e) {
expect(e).toBeInstanceOf(InvalidJsonPointerRefError);
}
});
});
});


describe("adding custom protocol handlers", () => {
it("has a way to add ipfs", () => {
referenceResolver.protocolHandlerMap.ipfs = () => {
// pretend like we are doing ipfs things here
const fetchedFromIpfs = { title: "foo", type: "string" } as JSONSchema;
return Promise.resolve(fetchedFromIpfs);
};

referenceResolver.resolve("ipfs://80088008800880088008")
});
});
Loading