Skip to content

Commit 61d77e9

Browse files
authored
Merge pull request #162 from json-schema-tools/feat/custom-protocols-alt
Feat/custom protocols alt
2 parents cc8c8e0 + 61ef640 commit 61d77e9

File tree

10 files changed

+302
-191
lines changed

10 files changed

+302
-191
lines changed

README.md

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,48 @@
1313

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

16-
Works in browser & in node (file system refs ignored in browser)
16+
Works in browser & in node (file system refs ignored in browser).
17+
18+
Easily add support for your own protocols.
1719

1820
## Getting Started
1921

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

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

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

26-
referenceResolver("#/properties/foo", { properties: { foo: 123 } }); // returns '123'
27-
referenceResolver("https://foo.com/", {}); // returns what ever json foo.com returns
28-
referenceResolver("../my-object.json", {}); // you get teh idea
32+
## Adding custom protocol handlers
2933

34+
```typescript
35+
import referenceResolver from "@json-schema-tools/reference-resolver";
36+
import JSONSchema from "@json-schema-tools/meta-schema";
37+
38+
referenceResolver.protocolHandlerMap.ipfs = (uri) => {
39+
const pretendItsFetchedFromIpfs = {
40+
title: "foo",
41+
type: "string",
42+
} as JSONSchema;
43+
return Promise.resolve(fetchedFromIpfs);
44+
};
45+
46+
referenceResolver.protocolHandlerMap["customprotocol"] = (uri) => {
47+
return Promise.resolve({
48+
type: "string",
49+
title: uri.replace("customprotocol://", ""),
50+
});
51+
};
52+
53+
referenceResolver.resolve("ipfs://80088008800880088008");
54+
referenceResolver.resolve("customprotocol://foobar");
3055
```
3156

57+
3258
### Contributing
3359

3460
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.

package-lock.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
],
3131
"devDependencies": {
3232
"@types/isomorphic-fetch": "0.0.35",
33+
"@json-schema-tools/meta-schema": "^1.5.10",
3334
"@types/jest": "^26.0.14",
3435
"@types/node": "^14.11.10",
3536
"jest": "^24.9.0",
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ProtocolHandlerMap } from "./reference-resolver";
2+
import { InvalidRemoteURLError, NonJsonRefError } from "./errors";
3+
import { JSONSchema } from "@json-schema-tools/meta-schema";
4+
import fetch from "isomorphic-fetch";
5+
6+
const fetchHandler = async (uri: string): Promise<JSONSchema> => {
7+
let schemaReq;
8+
try {
9+
schemaReq = await fetch(uri);
10+
} catch (e) {
11+
throw new InvalidRemoteURLError(uri);
12+
}
13+
14+
try {
15+
return await schemaReq.json() as JSONSchema;
16+
} catch (e) {
17+
throw new NonJsonRefError({ $ref: uri }, e.message);
18+
}
19+
};
20+
21+
export default {
22+
"https": fetchHandler,
23+
"http": fetchHandler,
24+
} as ProtocolHandlerMap;

src/errors.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/**
2+
* Error thrown when the fetched reference is not properly formatted JSON or is encoded
3+
* incorrectly
4+
*
5+
* @example
6+
* ```typescript
7+
*
8+
* import Dereferencer, { NonJsonRefError } from "@json-schema-tools/dereferencer";
9+
* const dereffer = new Dereferencer({});
10+
* try { await dereffer.resolve(); }
11+
* catch(e) {
12+
* if (e instanceof NonJsonRefError) { ... }
13+
* }
14+
* ```
15+
*
16+
*/
17+
export class NonJsonRefError implements Error {
18+
public message: string;
19+
public name: string;
20+
21+
constructor(obj: any, nonJson: string) {
22+
this.name = "NonJsonRefError";
23+
this.message = [
24+
"NonJsonRefError",
25+
`The resolved value at the reference: ${obj.$ref} was not JSON.parse 'able`,
26+
`The non-json content in question: ${nonJson}`,
27+
].join("\n");
28+
}
29+
}
30+
31+
32+
export class NotResolvableError implements Error {
33+
public message: string;
34+
public name: string;
35+
36+
constructor(ref: string) {
37+
this.name = "NotResolvableError";
38+
this.message = [
39+
"NotResolvableError",
40+
`Could not resolve the reference: ${ref}`,
41+
`No protocol handler was found, and it was not found to be an internal reference`,
42+
].join("\n");
43+
}
44+
}
45+
46+
/**
47+
* Error thrown when given an invalid file system path as a reference.
48+
*
49+
*/
50+
export class InvalidRemoteURLError implements Error {
51+
public message: string;
52+
public name: string;
53+
54+
constructor(ref: string) {
55+
this.name = "InvalidRemoteURLError";
56+
this.message = [
57+
"InvalidRemoteURLError",
58+
`The url was not resolvable: ${ref}`,
59+
].join("\n");
60+
}
61+
}
62+
63+
/**
64+
* Error thrown when given an invalid file system path as a reference.
65+
*
66+
* @example
67+
* ```typescript
68+
*
69+
* import Dereferencer, { InvalidFileSystemPathError } from "@json-schema-tools/dereferencer";
70+
* const dereffer = new Dereferencer({});
71+
* try { await dereffer.resolve(); }
72+
* catch(e) {
73+
* if (e instanceof InvalidFileSystemPathError) { ... }
74+
* }
75+
* ```
76+
*
77+
*/
78+
export class InvalidFileSystemPathError implements Error {
79+
public name: string;
80+
public message: string;
81+
82+
constructor(ref: string) {
83+
this.name = "InvalidFileSystemPathError";
84+
this.message = [
85+
"InvalidFileSystemPathError",
86+
`The path was not resolvable: ${ref}`,
87+
].join("\n");
88+
}
89+
}

src/index-web.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import buildReferenceResolver from "./reference-resolver";
21
import fetch from "isomorphic-fetch";
2+
import defaultProtocolHandlerMap from "./default-protocol-handler-map";
3+
import ReferenceResolver, { ProtocolHandlerMap } from "./reference-resolver";
34

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

src/index.test.ts

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,31 @@
1+
import { JSONSchema, JSONSchemaObject } from "@json-schema-tools/meta-schema";
2+
import { InvalidFileSystemPathError, InvalidRemoteURLError, NonJsonRefError } from "./errors";
13
import referenceResolver from "./index";
2-
import { NonJsonRefError, InvalidJsonPointerRefError, InvalidFileSystemPathError, InvalidRemoteURLError } from "./reference-resolver";
4+
import { InvalidJsonPointerRefError } from "./resolve-pointer";
35

46
describe("referenceResolver", () => {
57

68
it("simple", async () => {
7-
const resolvedRef = await referenceResolver("#/properties/foo", { properties: { foo: "boo" } });
9+
const resolvedRef = await referenceResolver.resolve("#/properties/foo", { properties: { foo: "boo" } });
810
expect(resolvedRef).toBe("boo");
911
});
1012

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

1618
it("https uri", async () => {
17-
const resolvedRef = await referenceResolver(
18-
"https://raw.githubusercontent.com/json-schema-tools/meta-schema/master/src/schema.json",
19-
{},
20-
);
19+
const uri = "https://meta.json-schema.tools";
20+
const resolvedRef = await referenceResolver.resolve(uri) as JSONSchemaObject;
21+
2122
expect(resolvedRef.title).toBe("JSONSchema");
2223
});
2324

2425
it("errors on non-json", async () => {
2526
expect.assertions(1);
2627
try {
27-
await referenceResolver("./src/test-non-json.json", {});
28+
await referenceResolver.resolve("./src/test-non-json.json");
2829
} catch (e) {
2930
expect(e).toBeInstanceOf(NonJsonRefError);
3031
}
@@ -33,7 +34,7 @@ describe("referenceResolver", () => {
3334
it("errors on bad json pointer ref", async () => {
3435
expect.assertions(1);
3536
try {
36-
await referenceResolver("#/nope", { foo: { bar: true } });
37+
await referenceResolver.resolve("#/nope", { foo: { bar: true } });
3738
} catch (e) {
3839
expect(e).toBeInstanceOf(InvalidJsonPointerRefError);
3940
}
@@ -42,7 +43,7 @@ describe("referenceResolver", () => {
4243
it("errors if file cant be found", async () => {
4344
expect.assertions(1);
4445
try {
45-
await referenceResolver("../not-real-file", {});
46+
await referenceResolver.resolve("../not-real-file");
4647
} catch (e) {
4748
expect(e).toBeInstanceOf(InvalidFileSystemPathError);
4849
}
@@ -51,28 +52,29 @@ describe("referenceResolver", () => {
5152
it("files are not relative to the src folder", async () => {
5253
expect.assertions(1);
5354
try {
54-
await referenceResolver("test-schema-1.json", {});
55+
await referenceResolver.resolve("test-schema-1.json");
5556
} catch (e) {
5657
expect(e).toBeInstanceOf(InvalidFileSystemPathError);
5758
}
5859
});
5960

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

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

7274
it("errors on urls that arent real", async () => {
7375
expect.assertions(1);
7476
try {
75-
await referenceResolver("https://not.real.at.all", {});
77+
await referenceResolver.resolve("https://not.real.at.all");
7678
} catch (e) {
7779
expect(e).toBeInstanceOf(InvalidRemoteURLError);
7880
}
@@ -81,9 +83,9 @@ describe("referenceResolver", () => {
8183
it("errors on urls that dont return json", async () => {
8284
expect.assertions(1);
8385
try {
84-
await referenceResolver("https://open-rpc.org/", {});
86+
await referenceResolver.resolve("https://open-rpc.org/");
8587
} catch (e) {
86-
expect(e).toBeInstanceOf(InvalidRemoteURLError);
88+
expect(e).toBeInstanceOf(NonJsonRefError);
8789
}
8890
});
8991
});
@@ -92,13 +94,14 @@ describe("referenceResolver", () => {
9294
describe("refs with hash fragment / internal reference component", () => {
9395
describe("files", () => {
9496
it("works in simple case", async () => {
95-
expect(await referenceResolver("./src/test-obj.json#/type", {})).toBe("string");
97+
expect(await referenceResolver.resolve("./src/test-obj.json#/type"))
98+
.toBe("string");
9699
});
97100

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

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

116119
it("errors when the json pointer is invalid", async () => {
117120
expect.assertions(1);
118121
try {
119-
await referenceResolver("https://meta.open-rpc.org/#type", {});
122+
await referenceResolver.resolve("https://meta.open-rpc.org/#type");
120123
} catch (e) {
121124
expect(e).toBeInstanceOf(InvalidJsonPointerRefError);
122125
}
@@ -125,10 +128,23 @@ describe("refs with hash fragment / internal reference component", () => {
125128
it("errors when you have 2 hash fragments in 1 ref", async () => {
126129
expect.assertions(1);
127130
try {
128-
await referenceResolver("https://meta.open-rpc.org/#properties/#openrpc", {});
131+
await referenceResolver.resolve("https://meta.open-rpc.org/#properties/#openrpc", {});
129132
} catch (e) {
130133
expect(e).toBeInstanceOf(InvalidJsonPointerRefError);
131134
}
132135
});
133136
});
134137
});
138+
139+
140+
describe("adding custom protocol handlers", () => {
141+
it("has a way to add ipfs", () => {
142+
referenceResolver.protocolHandlerMap.ipfs = () => {
143+
// pretend like we are doing ipfs things here
144+
const fetchedFromIpfs = { title: "foo", type: "string" } as JSONSchema;
145+
return Promise.resolve(fetchedFromIpfs);
146+
};
147+
148+
referenceResolver.resolve("ipfs://80088008800880088008")
149+
});
150+
});

0 commit comments

Comments
 (0)