Skip to content

Commit 4456700

Browse files
committed
Add JSON utility functions
1 parent 6677652 commit 4456700

File tree

6 files changed

+266
-7
lines changed

6 files changed

+266
-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 } 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+
/** @type (node: JsonNode) => unknown */
341+
export const 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

+175-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,31 @@
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+
} from "./index.js";
17+
18+
/**
19+
* @import {
20+
* JsonArrayNode,
21+
* JsonBooleanNode,
22+
* JsonNode,
23+
* JsonNullNode,
24+
* JsonNumberNode,
25+
* JsonObjectNode,
26+
* JsonStringNode
27+
* } from "./index.js"
28+
*/
629

730

831
describe("jsonast-util", async () => {
@@ -36,4 +59,154 @@ describe("jsonast-util", async () => {
3659
});
3760
}
3861
}
62+
63+
describe("object has property", () => {
64+
/** @type JsonNode */
65+
let subject;
66+
67+
beforeEach(() => {
68+
subject = fromJson(`{
69+
"foo": 42
70+
}`);
71+
});
72+
73+
test("true", () => {
74+
expect(jsonObjectHas("foo", subject)).to.eql(true);
75+
});
76+
77+
test("false", () => {
78+
expect(jsonObjectHas("bar", subject)).to.eql(false);
79+
});
80+
});
81+
82+
describe("typeOf", () => {
83+
test("null", () => {
84+
const subject = fromJson(`null`);
85+
if (jsonTypeOf(subject, "null")) {
86+
/** @type JsonNullNode */
87+
const _typeCheck = subject;
88+
} else {
89+
expect.fail();
90+
}
91+
});
92+
93+
test("true", () => {
94+
const subject = fromJson(`true`);
95+
if (jsonTypeOf(subject, "boolean")) {
96+
/** @type JsonBooleanNode */
97+
const _typeCheck = subject;
98+
} else {
99+
expect.fail();
100+
}
101+
});
102+
103+
test("false", () => {
104+
const subject = fromJson(`false`);
105+
if (jsonTypeOf(subject, "boolean")) {
106+
/** @type JsonBooleanNode */
107+
const _typeCheck = subject;
108+
} else {
109+
expect.fail();
110+
}
111+
});
112+
113+
test("number", () => {
114+
const subject = fromJson(`42`);
115+
if (jsonTypeOf(subject, "number")) {
116+
/** @type JsonNumberNode */
117+
const _typeCheck = subject;
118+
} else {
119+
expect.fail();
120+
}
121+
});
122+
123+
test("string", () => {
124+
const subject = fromJson(`"foo"`);
125+
if (jsonTypeOf(subject, "string")) {
126+
/** @type JsonStringNode */
127+
const _typeCheck = subject;
128+
} else {
129+
expect.fail();
130+
}
131+
});
132+
133+
test("array", () => {
134+
const subject = fromJson(`["foo", 42]`);
135+
if (jsonTypeOf(subject, "array")) {
136+
/** @type JsonArrayNode */
137+
const _typeCheck = subject;
138+
} else {
139+
expect.fail();
140+
}
141+
});
142+
143+
test("object", () => {
144+
const subject = fromJson(`{ "foo": 42 }`);
145+
if (jsonTypeOf(subject, "object")) {
146+
/** @type JsonObjectNode */
147+
const _typeCheck = subject;
148+
} else {
149+
expect.fail();
150+
}
151+
});
152+
});
153+
154+
test("iter", () => {
155+
const subject = fromJson(`[1, 2]`);
156+
157+
const generator = jsonArrayIter(subject);
158+
159+
const first = generator.next();
160+
expect(toJson(first.value)).to.equal(`1`);
161+
const second = generator.next();
162+
expect(toJson(second.value)).to.equal(`2`);
163+
expect((generator.next()).done).to.equal(true);
164+
});
165+
166+
test("keys", () => {
167+
const subject = fromJson(`{
168+
"a": 1,
169+
"b": 2
170+
}`);
171+
172+
const generator = jsonObjectKeys(subject);
173+
174+
expect(generator.next().value).to.equal("a");
175+
expect(generator.next().value).to.equal("b");
176+
expect(generator.next().done).to.equal(true);
177+
});
178+
179+
test("values", () => {
180+
const subject = fromJson(`{
181+
"a": 1,
182+
"b": 2
183+
}`);
184+
185+
const generator = jsonObjectValues(subject);
186+
187+
const first = generator.next();
188+
expect(toJson(first.value)).to.equal(`1`);
189+
const second = generator.next();
190+
expect(toJson(second.value)).to.equal(`2`);
191+
expect((generator.next()).done).to.equal(true);
192+
});
193+
194+
test("entries", () => {
195+
const subject = fromJson(`{
196+
"a": 1,
197+
"b": 2
198+
}`);
199+
200+
const generator = jsonObjectEntries(subject);
201+
202+
const a = /** @type [string, JsonNode] */ ((generator.next()).value);
203+
expect(a[0]).to.equal("a");
204+
expect(toJson(a[1])).to.equal(`1`);
205+
206+
const b = /** @type [string, JsonNode] */ ((generator.next()).value);
207+
expect(b[0]).to.equal("b");
208+
expect(toJson(b[1])).to.equal(`2`);
209+
210+
expect(generator.next().done).to.equal(true);
211+
});
39212
});

src/json/types.d.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {
2+
JsonArrayNode,
3+
JsonBooleanNode,
4+
JsonNode,
5+
JsonNullNode,
6+
JsonNumberNode,
7+
JsonObjectNode,
8+
JsonStringNode
9+
} from "./jsonast.d.ts";
10+
11+
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)
18+
);

src/json/types.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {};

0 commit comments

Comments
 (0)