Skip to content

Commit 001ee8d

Browse files
committed
Update Hyperjump to support type narrowing
1 parent 5d11beb commit 001ee8d

File tree

5 files changed

+76
-175
lines changed

5 files changed

+76
-175
lines changed

src/hyperjump/hyperjump.js

+6-50
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ import { FileUriSchemePlugin } from "./uri-schemes/file-scheme-plugin.js";
66
import { JsonMediaTypePlugin } from "./media-types/json-media-type-plugin.js";
77
import { JrefMediaTypePlugin } from "./media-types/jref-media-type-plugin.js";
88
import { pointerGet, pointerStep } from "../jref/jref-util.js";
9+
import { jsonObjectHas, jsonObjectKeys, jsonTypeOf, jsonValue } from "../json/jsonast-util.js";
910
import { mimeMatch } from "./utilities.js";
1011

1112
/**
1213
* @import { JrefNode } from "../jref/jref-ast.js"
13-
* @import { JsonCompatible, JsonType } from "../json/jsonast.js"
14+
* @import { JsonCompatible } from "../json/jsonast.js"
1415
* @import { UriSchemePlugin } from "./uri-schemes/uri-scheme-plugin.js"
1516
* @import { DocumentNode, MediaTypePlugin } from "./media-types/media-type-plugin.js"
1617
* @import { JsonPointerError } from "../json/jsonast-util.js"
@@ -266,47 +267,9 @@ export class Hyperjump {
266267
throw new UnsupportedMediaTypeError(contentType.type, `'${contentType.type}' is not supported. Use the 'addMediaTypePlugin' function to add support for this media type.`);
267268
}
268269

269-
/** @type (node: JsonCompatible<JrefNode>) => unknown */
270-
value(node) {
271-
switch (node.jsonType) {
272-
case "object":
273-
case "array":
274-
// TODO: Handle structured values
275-
throw Error("Can't get the value of a structured value.");
276-
default:
277-
return node.value;
278-
}
279-
}
280-
281-
/** @type (node: JsonCompatible<JrefNode>) => JsonType */
282-
typeOf(node) {
283-
return node.jsonType;
284-
}
285-
286-
/** @type (key: string, node: JsonCompatible<JrefNode>) => boolean */
287-
has(key, node) {
288-
if (node.jsonType === "object") {
289-
for (const property of node.children) {
290-
if (property.children[0].value === key) {
291-
return true;
292-
}
293-
}
294-
}
295-
296-
return false;
297-
}
298-
299-
/** @type (node: JsonCompatible<JrefNode>) => number */
300-
length(node) {
301-
switch (node.jsonType) {
302-
case "array":
303-
return node.children.length;
304-
case "string":
305-
return node.value.length;
306-
default:
307-
throw Error("Can't get the length of a value that is not an array or a string.");
308-
}
309-
}
270+
value = jsonValue;
271+
typeOf = jsonTypeOf;
272+
has = jsonObjectHas;
310273

311274
/**
312275
* This is like indexing into an object or array. It will follow any
@@ -332,14 +295,7 @@ export class Hyperjump {
332295
}
333296
}
334297

335-
/** @type (node: JsonCompatible<JrefNode>) => Generator<string, undefined, string> */
336-
* keys(node) {
337-
if (node.jsonType === "object") {
338-
for (const propertyNode of node.children) {
339-
yield propertyNode.children[0].value;
340-
}
341-
}
342-
}
298+
keys = jsonObjectKeys;
343299

344300
/**
345301
* Iterate over the values of an object. It will follow any references it

src/hyperjump/node-functions.test.js

+47-28
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,7 @@ describe("JSON Browser", () => {
5050
});
5151
});
5252

53-
test("length of array", async () => {
54-
const jref = `[42]`;
55-
56-
mockAgent.get(testDomain)
57-
.intercept({ method: "GET", path: "/foo" })
58-
.reply(200, jref, { headers: { "content-type": "application/reference+json" } });
59-
60-
const hyperjump = new Hyperjump();
61-
const subject = await hyperjump.get(`${testDomain}/foo`);
62-
63-
expect(hyperjump.length(subject)).to.eql(1);
64-
});
65-
66-
describe("typeOf", () => {
53+
describe("typeOf/value and type narrowing", () => {
6754
const hyperjump = new Hyperjump();
6855

6956
beforeEach(() => {
@@ -83,38 +70,70 @@ describe("JSON Browser", () => {
8370
});
8471

8572
test("null", async () => {
86-
const subject = await hyperjump.get(`${testDomain}/foo#/null`);
87-
expect(hyperjump.typeOf(subject)).to.eql("null");
73+
const node = await hyperjump.get(`${testDomain}/foo#/null`);
74+
if (hyperjump.typeOf(node, "null")) {
75+
/** @type null */
76+
const subject = hyperjump.value(node);
77+
expect(subject).to.equal(null);
78+
} else {
79+
expect.fail();
80+
}
8881
});
8982

9083
test("true", async () => {
91-
const subject = await hyperjump.get(`${testDomain}/foo#/true`);
92-
expect(hyperjump.typeOf(subject)).to.eql("boolean");
84+
const node = await hyperjump.get(`${testDomain}/foo#/true`);
85+
if (hyperjump.typeOf(node, "boolean")) {
86+
/** @type boolean */
87+
const subject = hyperjump.value(node);
88+
expect(subject).to.equal(true);
89+
} else {
90+
expect.fail();
91+
}
9392
});
9493

9594
test("false", async () => {
96-
const subject = await hyperjump.get(`${testDomain}/foo#/false`);
97-
expect(hyperjump.typeOf(subject)).to.eql("boolean");
95+
const node = await hyperjump.get(`${testDomain}/foo#/false`);
96+
if (hyperjump.typeOf(node, "boolean")) {
97+
/** @type boolean */
98+
const subject = hyperjump.value(node);
99+
expect(subject).to.equal(false);
100+
} else {
101+
expect.fail();
102+
}
98103
});
99104

100105
test("number", async () => {
101-
const subject = await hyperjump.get(`${testDomain}/foo#/number`);
102-
expect(hyperjump.typeOf(subject)).to.eql("number");
106+
const node = await hyperjump.get(`${testDomain}/foo#/number`);
107+
if (hyperjump.typeOf(node, "number")) {
108+
/** @type number */
109+
const subject = hyperjump.value(node);
110+
expect(subject).to.equal(42);
111+
} else {
112+
expect.fail();
113+
}
103114
});
104115

105116
test("string", async () => {
106-
const subject = await hyperjump.get(`${testDomain}/foo#/string`);
107-
expect(hyperjump.typeOf(subject)).to.eql("string");
117+
const node = await hyperjump.get(`${testDomain}/foo#/string`);
118+
if (hyperjump.typeOf(node, "string")) {
119+
/** @type string */
120+
const subject = hyperjump.value(node);
121+
expect(subject).to.equal("foo");
122+
} else {
123+
expect.fail();
124+
}
108125
});
109126

110127
test("array", async () => {
111-
const subject = await hyperjump.get(`${testDomain}/foo#/array`);
112-
expect(hyperjump.typeOf(subject)).to.eql("array");
128+
const node = await hyperjump.get(`${testDomain}/foo#/array`);
129+
expect(hyperjump.typeOf(node, "array")).to.equal(true);
130+
expect(() => hyperjump.value(node)).to.throw();
113131
});
114132

115133
test("object", async () => {
116-
const subject = await hyperjump.get(`${testDomain}/foo#/object`);
117-
expect(hyperjump.typeOf(subject)).to.eql("object");
134+
const node = await hyperjump.get(`${testDomain}/foo#/object`);
135+
expect(hyperjump.typeOf(node, "object")).to.equal(true);
136+
expect(() => hyperjump.value(node)).to.throw();
118137
});
119138
});
120139
});

src/json/jsonast-util.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ export const jsonTypeOf = /** @type JsonTypeOf */ ((node, type) => {
354354
return node.jsonType === type;
355355
});
356356

357-
/** @type (key: string, node: JsonNode) => boolean */
357+
/** @type (key: string, node: JsonCompatible<any>) => boolean */
358358
export const jsonObjectHas = (key, node) => {
359359
if (node.jsonType === "object") {
360360
for (const property of node.children) {
@@ -367,7 +367,7 @@ export const jsonObjectHas = (key, node) => {
367367
return false;
368368
};
369369

370-
/** @type (node: JsonNode) => Generator<JsonNode, void, unknown> */
370+
/** @type <A>(node: JsonCompatible<A>) => Generator<A, void, unknown> */
371371
export const jsonArrayIter = function* (node) {
372372
if (node.jsonType === "array") {
373373
for (const itemNode of node.children) {
@@ -376,7 +376,7 @@ export const jsonArrayIter = function* (node) {
376376
}
377377
};
378378

379-
/** @type (node: JsonNode) => Generator<string, undefined, string> */
379+
/** @type (node: JsonCompatible<any>) => Generator<string, undefined, string> */
380380
export const jsonObjectKeys = function* (node) {
381381
if (node.jsonType === "object") {
382382
for (const propertyNode of node.children) {
@@ -385,7 +385,7 @@ export const jsonObjectKeys = function* (node) {
385385
}
386386
};
387387

388-
/** @type (node: JsonNode) => Generator<JsonNode, void, unknown> */
388+
/** @type <A>(node: JsonCompatible<A>) => Generator<A, void, unknown> */
389389
export const jsonObjectValues = function* (node) {
390390
if (node.jsonType === "object") {
391391
for (const propertyNode of node.children) {
@@ -394,7 +394,7 @@ export const jsonObjectValues = function* (node) {
394394
}
395395
};
396396

397-
/** @type (node: JsonNode) => Generator<[string, JsonNode], void, unknown> */
397+
/** @type <A>(node: JsonCompatible<A>) => Generator<[string, A], void, unknown> */
398398
export const jsonObjectEntries = function* (node) {
399399
if (node.jsonType === "object") {
400400
for (const propertyNode of node.children) {

src/json/jsonast-util.test.js

+3-78
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ describe("jsonast-util", async () => {
6161
}
6262
}
6363

64-
describe("value", () => {
64+
describe("type/value and type narrowing", () => {
6565
test("null", () => {
6666
const node = fromJson(`null`);
6767
if (jsonTypeOf(node, "null")) {
@@ -108,18 +108,15 @@ describe("jsonast-util", async () => {
108108

109109
test("array", () => {
110110
const node = fromJson(`["foo", 42]`);
111+
expect(jsonTypeOf(node, "array")).to.equal(true);
111112
expect(() => jsonValue(node)).to.throw();
112113
});
113114

114115
test("object", () => {
115116
const node = fromJson(`{ "foo": 42 }`);
117+
expect(jsonTypeOf(node, "object")).to.equal(true);
116118
expect(() => jsonValue(node)).to.throw();
117119
});
118-
119-
test("unknown", () => {
120-
const node = fromJson(`42`);
121-
expect(jsonValue(node)).to.equal(42);
122-
});
123120
});
124121

125122
describe("object has property", () => {
@@ -141,78 +138,6 @@ describe("jsonast-util", async () => {
141138
});
142139
});
143140

144-
describe("typeOf", () => {
145-
test("null", () => {
146-
const subject = fromJson(`null`);
147-
if (jsonTypeOf(subject, "null")) {
148-
/** @type JsonNullNode */
149-
const _typeCheck = subject;
150-
} else {
151-
expect.fail();
152-
}
153-
});
154-
155-
test("true", () => {
156-
const subject = fromJson(`true`);
157-
if (jsonTypeOf(subject, "boolean")) {
158-
/** @type JsonBooleanNode */
159-
const _typeCheck = subject;
160-
} else {
161-
expect.fail();
162-
}
163-
});
164-
165-
test("false", () => {
166-
const subject = fromJson(`false`);
167-
if (jsonTypeOf(subject, "boolean")) {
168-
/** @type JsonBooleanNode */
169-
const _typeCheck = subject;
170-
} else {
171-
expect.fail();
172-
}
173-
});
174-
175-
test("number", () => {
176-
const subject = fromJson(`42`);
177-
if (jsonTypeOf(subject, "number")) {
178-
/** @type JsonNumberNode */
179-
const _typeCheck = subject;
180-
} else {
181-
expect.fail();
182-
}
183-
});
184-
185-
test("string", () => {
186-
const subject = fromJson(`"foo"`);
187-
if (jsonTypeOf(subject, "string")) {
188-
/** @type JsonStringNode */
189-
const _typeCheck = subject;
190-
} else {
191-
expect.fail();
192-
}
193-
});
194-
195-
test("array", () => {
196-
const subject = fromJson(`["foo", 42]`);
197-
if (jsonTypeOf(subject, "array")) {
198-
/** @type JsonArrayNode */
199-
const _typeCheck = subject;
200-
} else {
201-
expect.fail();
202-
}
203-
});
204-
205-
test("object", () => {
206-
const subject = fromJson(`{ "foo": 42 }`);
207-
if (jsonTypeOf(subject, "object")) {
208-
/** @type JsonObjectNode */
209-
const _typeCheck = subject;
210-
} else {
211-
expect.fail();
212-
}
213-
});
214-
});
215-
216141
test("iter", () => {
217142
const subject = fromJson(`[1, 2]`);
218143

src/json/types.d.ts

+15-14
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,29 @@
11
import {
22
JsonArrayNode,
33
JsonBooleanNode,
4-
JsonNode,
4+
JsonCompatible,
55
JsonNullNode,
66
JsonNumberNode,
77
JsonObjectNode,
88
JsonStringNode
99
} from "./jsonast.d.ts";
1010

11+
1112
export type JsonTypeOf = (
12-
((node: JsonNode, type: "null") => node is JsonNullNode) &
13-
((node: JsonNode, type: "boolean") => node is JsonBooleanNode) &
14-
((node: JsonNode, type: "number") => node is JsonNumberNode) &
15-
((node: JsonNode, type: "string") => node is JsonStringNode) &
16-
((node: JsonNode, type: "array") => node is JsonArrayNode) &
17-
((node: JsonNode, type: "object") => node is JsonObjectNode)
13+
(<A>(node: JsonCompatible<A>, type: "null") => node is JsonNullNode) &
14+
(<A>(node: JsonCompatible<A>, type: "boolean") => node is JsonBooleanNode) &
15+
(<A>(node: JsonCompatible<A>, type: "number") => node is JsonNumberNode) &
16+
(<A>(node: JsonCompatible<A>, type: "string") => node is JsonStringNode) &
17+
(<A>(node: JsonCompatible<A>, type: "array") => node is JsonArrayNode<A>) &
18+
(<A>(node: JsonCompatible<A>, type: "object") => node is JsonObjectNode<A>)
1819
);
1920

2021
export type JsonValue = (
21-
((node: JsonNullNode) => null) &
22-
((node: JsonBooleanNode) => boolean) &
23-
((node: JsonNumberNode) => number) &
24-
((node: JsonStringNode) => string) &
25-
((node: JsonArrayNode) => unknown[]) &
26-
((node: JsonObjectNode) => Record<string, unknown>) &
27-
((node: JsonNode) => unknown)
22+
(<_A>(node: JsonNullNode) => null) &
23+
(<_A>(node: JsonBooleanNode) => boolean) &
24+
(<_A>(node: JsonNumberNode) => number) &
25+
(<_A>(node: JsonStringNode) => string) &
26+
(<A>(node: JsonArrayNode<A>) => unknown[]) &
27+
(<A>(node: JsonObjectNode<A>) => Record<string, unknown>) &
28+
(<A>(node: JsonCompatible<A>) => unknown)
2829
);

0 commit comments

Comments
 (0)