Skip to content

Commit 5d11beb

Browse files
committed
Add JSON utility functions
1 parent 6677652 commit 5d11beb

File tree

7 files changed

+339
-7
lines changed

7 files changed

+339
-7
lines changed

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@hyperjump/browser",
3-
"version": "2.0.0",
3+
"version": "2.0.0-dev",
44
"description": "A client for working with JSON Reference (`$ref`) documents",
55
"keywords": [
66
"$ref",
@@ -19,7 +19,7 @@
1919
"type": "module",
2020
"exports": {
2121
".": "./src/hyperjump/index.js",
22-
"./rejson": "./src/json/index.js",
22+
"./json": "./src/json/index.js",
2323
"./jref": "./src/jref/index.js"
2424
},
2525
"bin": {

src/hyperjump/node-functions.test.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,11 @@ describe("JSON Browser", () => {
4242
});
4343

4444
test("true", () => {
45-
expect(node && hyperjump.has("foo", node)).to.eql(true);
45+
expect(hyperjump.has("foo", node)).to.eql(true);
4646
});
4747

4848
test("false", () => {
49-
expect(node && hyperjump.has("bar", node)).to.eql(false);
49+
expect(hyperjump.has("bar", node)).to.eql(false);
5050
});
5151
});
5252

@@ -63,7 +63,7 @@ describe("JSON Browser", () => {
6363
expect(hyperjump.length(subject)).to.eql(1);
6464
});
6565

66-
describe("object has property", () => {
66+
describe("typeOf", () => {
6767
const hyperjump = new Hyperjump();
6868

6969
beforeEach(() => {

src/json/jsonast-util.js

+67
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { JsonLexer } from "./json-lexer.js";
1515
* JsonPropertyNode,
1616
* JsonStringNode
1717
* } from "./jsonast.js"
18+
* @import { JsonTypeOf, JsonValue } from "./types.js"
1819
*/
1920

2021

@@ -336,6 +337,72 @@ const unescapePointerSegment = (segment) => segment.toString().replace(/~1/g, "/
336337
/** @type (segment: string) => string */
337338
const escapePointerSegment = (segment) => segment.toString().replace(/~/g, "~0").replace(/\//g, "~1");
338339

340+
// eslint-disable-next-line @stylistic/no-extra-parens
341+
export const jsonValue = /** @type JsonValue */ ((node) => {
342+
switch (node.jsonType) {
343+
case "object":
344+
case "array":
345+
// TODO: Handle structured values
346+
throw Error("Can't get the value of a structured value.");
347+
default:
348+
return node.value;
349+
}
350+
});
351+
352+
// eslint-disable-next-line @stylistic/no-extra-parens
353+
export const jsonTypeOf = /** @type JsonTypeOf */ ((node, type) => {
354+
return node.jsonType === type;
355+
});
356+
357+
/** @type (key: string, node: JsonNode) => boolean */
358+
export const jsonObjectHas = (key, node) => {
359+
if (node.jsonType === "object") {
360+
for (const property of node.children) {
361+
if (property.children[0].value === key) {
362+
return true;
363+
}
364+
}
365+
}
366+
367+
return false;
368+
};
369+
370+
/** @type (node: JsonNode) => Generator<JsonNode, void, unknown> */
371+
export const jsonArrayIter = function* (node) {
372+
if (node.jsonType === "array") {
373+
for (const itemNode of node.children) {
374+
yield itemNode;
375+
}
376+
}
377+
};
378+
379+
/** @type (node: JsonNode) => Generator<string, undefined, string> */
380+
export const jsonObjectKeys = function* (node) {
381+
if (node.jsonType === "object") {
382+
for (const propertyNode of node.children) {
383+
yield propertyNode.children[0].value;
384+
}
385+
}
386+
};
387+
388+
/** @type (node: JsonNode) => Generator<JsonNode, void, unknown> */
389+
export const jsonObjectValues = function* (node) {
390+
if (node.jsonType === "object") {
391+
for (const propertyNode of node.children) {
392+
yield propertyNode.children[1];
393+
}
394+
}
395+
};
396+
397+
/** @type (node: JsonNode) => Generator<[string, JsonNode], void, unknown> */
398+
export const jsonObjectEntries = function* (node) {
399+
if (node.jsonType === "object") {
400+
for (const propertyNode of node.children) {
401+
yield [propertyNode.children[0].value, propertyNode.children[1]];
402+
}
403+
}
404+
};
405+
339406
export class JsonPointerError extends Error {
340407
/**
341408
* @param {string} [message]

src/json/jsonast-util.test.js

+237-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,32 @@
11
import { readdir, readFile } from "node:fs/promises";
22
import { resolve } from "node:path";
3-
import { describe, test, expect } from "vitest";
3+
import { describe, test, expect, beforeEach } from "vitest";
44
import { VFileMessage } from "vfile-message";
5-
import { rejson, rejsonStringify } from "./index.js";
5+
import {
6+
fromJson,
7+
toJson,
8+
rejson,
9+
rejsonStringify,
10+
jsonObjectHas,
11+
jsonTypeOf,
12+
jsonArrayIter,
13+
jsonObjectKeys,
14+
jsonObjectValues,
15+
jsonObjectEntries,
16+
jsonValue
17+
} from "./index.js";
18+
19+
/**
20+
* @import {
21+
* JsonArrayNode,
22+
* JsonBooleanNode,
23+
* JsonNode,
24+
* JsonNullNode,
25+
* JsonNumberNode,
26+
* JsonObjectNode,
27+
* JsonStringNode
28+
* } from "./index.js"
29+
*/
630

731

832
describe("jsonast-util", async () => {
@@ -36,4 +60,215 @@ describe("jsonast-util", async () => {
3660
});
3761
}
3862
}
63+
64+
describe("value", () => {
65+
test("null", () => {
66+
const node = fromJson(`null`);
67+
if (jsonTypeOf(node, "null")) {
68+
/** @type null */
69+
const subject = jsonValue(node);
70+
expect(subject).to.equal(null);
71+
} else {
72+
expect.fail();
73+
}
74+
});
75+
76+
test("boolean", () => {
77+
const node = fromJson(`true`);
78+
if (jsonTypeOf(node, "boolean")) {
79+
/** @type boolean */
80+
const subject = jsonValue(node);
81+
expect(subject).to.equal(true);
82+
} else {
83+
expect.fail();
84+
}
85+
});
86+
87+
test("number", () => {
88+
const node = fromJson(`42`);
89+
if (jsonTypeOf(node, "number")) {
90+
/** @type number */
91+
const subject = jsonValue(node);
92+
expect(subject).to.equal(42);
93+
} else {
94+
expect.fail();
95+
}
96+
});
97+
98+
test("string", () => {
99+
const node = fromJson(`"foo"`);
100+
if (jsonTypeOf(node, "string")) {
101+
/** @type string */
102+
const subject = jsonValue(node);
103+
expect(subject).to.equal("foo");
104+
} else {
105+
expect.fail();
106+
}
107+
});
108+
109+
test("array", () => {
110+
const node = fromJson(`["foo", 42]`);
111+
expect(() => jsonValue(node)).to.throw();
112+
});
113+
114+
test("object", () => {
115+
const node = fromJson(`{ "foo": 42 }`);
116+
expect(() => jsonValue(node)).to.throw();
117+
});
118+
119+
test("unknown", () => {
120+
const node = fromJson(`42`);
121+
expect(jsonValue(node)).to.equal(42);
122+
});
123+
});
124+
125+
describe("object has property", () => {
126+
/** @type JsonNode */
127+
let subject;
128+
129+
beforeEach(() => {
130+
subject = fromJson(`{
131+
"foo": 42
132+
}`);
133+
});
134+
135+
test("true", () => {
136+
expect(jsonObjectHas("foo", subject)).to.equal(true);
137+
});
138+
139+
test("false", () => {
140+
expect(jsonObjectHas("bar", subject)).to.equal(false);
141+
});
142+
});
143+
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+
216+
test("iter", () => {
217+
const subject = fromJson(`[1, 2]`);
218+
219+
const generator = jsonArrayIter(subject);
220+
221+
const first = generator.next();
222+
expect(toJson(first.value)).to.equal(`1`);
223+
const second = generator.next();
224+
expect(toJson(second.value)).to.equal(`2`);
225+
expect((generator.next()).done).to.equal(true);
226+
});
227+
228+
test("keys", () => {
229+
const subject = fromJson(`{
230+
"a": 1,
231+
"b": 2
232+
}`);
233+
234+
const generator = jsonObjectKeys(subject);
235+
236+
expect(generator.next().value).to.equal("a");
237+
expect(generator.next().value).to.equal("b");
238+
expect(generator.next().done).to.equal(true);
239+
});
240+
241+
test("values", () => {
242+
const subject = fromJson(`{
243+
"a": 1,
244+
"b": 2
245+
}`);
246+
247+
const generator = jsonObjectValues(subject);
248+
249+
const first = generator.next();
250+
expect(toJson(first.value)).to.equal(`1`);
251+
const second = generator.next();
252+
expect(toJson(second.value)).to.equal(`2`);
253+
expect((generator.next()).done).to.equal(true);
254+
});
255+
256+
test("entries", () => {
257+
const subject = fromJson(`{
258+
"a": 1,
259+
"b": 2
260+
}`);
261+
262+
const generator = jsonObjectEntries(subject);
263+
264+
const a = /** @type [string, JsonNode] */ ((generator.next()).value);
265+
expect(a[0]).to.equal("a");
266+
expect(toJson(a[1])).to.equal(`1`);
267+
268+
const b = /** @type [string, JsonNode] */ ((generator.next()).value);
269+
expect(b[0]).to.equal("b");
270+
expect(toJson(b[1])).to.equal(`2`);
271+
272+
expect(generator.next().done).to.equal(true);
273+
});
39274
});

0 commit comments

Comments
 (0)