@procedures/json 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/LICENSE +21 -0
- package/README.md +104 -2
- package/dist/.tsbuildinfo +1 -0
- package/dist/diff.d.ts +18 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +66 -0
- package/dist/diff.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/merge.d.ts +14 -0
- package/dist/merge.d.ts.map +1 -0
- package/dist/merge.js +34 -0
- package/dist/merge.js.map +1 -0
- package/dist/module.d.ts +21 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +192 -0
- package/dist/module.js.map +1 -0
- package/dist/patch.d.ts +23 -0
- package/dist/patch.d.ts.map +1 -0
- package/dist/patch.js +137 -0
- package/dist/patch.js.map +1 -0
- package/dist/path.d.ts +38 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +157 -0
- package/dist/path.js.map +1 -0
- package/dist/pointer.d.ts +22 -0
- package/dist/pointer.d.ts.map +1 -0
- package/dist/pointer.js +70 -0
- package/dist/pointer.js.map +1 -0
- package/dist/procedures/json/diff.d.ts +12 -0
- package/dist/procedures/json/diff.d.ts.map +1 -0
- package/dist/procedures/json/diff.js +17 -0
- package/dist/procedures/json/diff.js.map +1 -0
- package/dist/procedures/json/index.d.ts +6 -0
- package/dist/procedures/json/index.d.ts.map +1 -0
- package/dist/procedures/json/index.js +6 -0
- package/dist/procedures/json/index.js.map +1 -0
- package/dist/procedures/json/merge.d.ts +12 -0
- package/dist/procedures/json/merge.d.ts.map +1 -0
- package/dist/procedures/json/merge.js +17 -0
- package/dist/procedures/json/merge.js.map +1 -0
- package/dist/procedures/json/patch.d.ts +12 -0
- package/dist/procedures/json/patch.d.ts.map +1 -0
- package/dist/procedures/json/patch.js +20 -0
- package/dist/procedures/json/patch.js.map +1 -0
- package/dist/procedures/json/query.d.ts +12 -0
- package/dist/procedures/json/query.d.ts.map +1 -0
- package/dist/procedures/json/query.js +28 -0
- package/dist/procedures/json/query.js.map +1 -0
- package/dist/procedures/json/validate.d.ts +12 -0
- package/dist/procedures/json/validate.d.ts.map +1 -0
- package/dist/procedures/json/validate.js +18 -0
- package/dist/procedures/json/validate.js.map +1 -0
- package/dist/procedures/yaml/index.d.ts +3 -0
- package/dist/procedures/yaml/index.d.ts.map +1 -0
- package/dist/procedures/yaml/index.js +3 -0
- package/dist/procedures/yaml/index.js.map +1 -0
- package/dist/procedures/yaml/parse.d.ts +12 -0
- package/dist/procedures/yaml/parse.d.ts.map +1 -0
- package/dist/procedures/yaml/parse.js +14 -0
- package/dist/procedures/yaml/parse.js.map +1 -0
- package/dist/procedures/yaml/stringify.d.ts +11 -0
- package/dist/procedures/yaml/stringify.d.ts.map +1 -0
- package/dist/procedures/yaml/stringify.js +15 -0
- package/dist/procedures/yaml/stringify.js.map +1 -0
- package/dist/register.d.ts +6 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +8 -0
- package/dist/register.js.map +1 -0
- package/dist/schema.d.ts +38 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +92 -0
- package/dist/schema.js.map +1 -0
- package/dist/types.d.ts +90 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/value.d.ts +61 -0
- package/dist/value.d.ts.map +1 -0
- package/dist/value.js +124 -0
- package/dist/value.js.map +1 -0
- package/dist/yaml.d.ts +35 -0
- package/dist/yaml.d.ts.map +1 -0
- package/dist/yaml.js +111 -0
- package/dist/yaml.js.map +1 -0
- package/package.json +81 -3
- package/src/diff.ts +58 -0
- package/src/index.ts +14 -0
- package/src/merge.ts +34 -0
- package/src/module.ts +236 -0
- package/src/patch.ts +132 -0
- package/src/path.ts +155 -0
- package/src/pointer.ts +70 -0
- package/src/procedures/json/diff.ts +19 -0
- package/src/procedures/json/index.ts +5 -0
- package/src/procedures/json/merge.ts +19 -0
- package/src/procedures/json/patch.ts +21 -0
- package/src/procedures/json/query.ts +29 -0
- package/src/procedures/json/validate.ts +20 -0
- package/src/procedures/yaml/index.ts +2 -0
- package/src/procedures/yaml/parse.ts +16 -0
- package/src/procedures/yaml/stringify.ts +17 -0
- package/src/register.ts +10 -0
- package/src/schema.ts +104 -0
- package/src/types.ts +127 -0
- package/src/value.ts +143 -0
- package/src/yaml.ts +128 -0
package/src/path.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A simple path query: a small part of JSONPath, without filters and scripts.
|
|
3
|
+
*
|
|
4
|
+
* | Part | Meaning |
|
|
5
|
+
* |---|---|
|
|
6
|
+
* | `$` | The whole value. It is optional at the start. |
|
|
7
|
+
* | `.name` or `name` | The member `name` of an object. A name has letters, digits, `_`, `$` and `-`. |
|
|
8
|
+
* | `["a.b"]` or `['a.b']` | The member with any key. `\` escapes a quote or a backslash. |
|
|
9
|
+
* | `[2]` | The item 2 of an array. `[-1]` is the last item. |
|
|
10
|
+
* | `*` or `[*]` | Each member of an object, or each item of an array. |
|
|
11
|
+
* | `..name`, `..*`, `..[0]` | The same step on the value and on each value in it, at each depth. |
|
|
12
|
+
*
|
|
13
|
+
* A path that names `__proto__`, `constructor` or `prototype` is refused (roadmap R6).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { JsonError, PROTOTYPE_KEYS, hasOwn, type JsonValue } from "./value.js";
|
|
17
|
+
|
|
18
|
+
/** One step of a path. */
|
|
19
|
+
export type PathStep =
|
|
20
|
+
| { readonly kind: "key"; readonly key: string; readonly deep: boolean }
|
|
21
|
+
| { readonly kind: "index"; readonly index: number; readonly deep: boolean }
|
|
22
|
+
| { readonly kind: "wildcard"; readonly deep: boolean };
|
|
23
|
+
|
|
24
|
+
/** One match of a path: its JSON Pointer tokens and its value. */
|
|
25
|
+
export interface PathMatch {
|
|
26
|
+
readonly tokens: readonly (string | number)[];
|
|
27
|
+
readonly value: JsonValue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const NAME_CHAR = /[A-Za-z0-9_$\-\u0080-]/;
|
|
31
|
+
|
|
32
|
+
/** This function parses a path query. A bad path throws `INVALID_POINTER`. */
|
|
33
|
+
export function parsePath(path: string): PathStep[] {
|
|
34
|
+
const steps: PathStep[] = [];
|
|
35
|
+
const fail = (at: number, message: string): never => {
|
|
36
|
+
throw new JsonError("INVALID_POINTER", `The path ${JSON.stringify(path)} is not valid at character ${at + 1}: ${message}`);
|
|
37
|
+
};
|
|
38
|
+
const key = (name: string, deep: boolean, at: number): PathStep => {
|
|
39
|
+
if (PROTOTYPE_KEYS.has(name)) {
|
|
40
|
+
throw new JsonError("PROTOTYPE_KEY", `The path ${JSON.stringify(path)} has the key "${name}" at character ${at + 1}, which reaches a prototype`);
|
|
41
|
+
}
|
|
42
|
+
return { kind: "key", key: name, deep };
|
|
43
|
+
};
|
|
44
|
+
let i = 0;
|
|
45
|
+
if (path.startsWith("$")) i = 1;
|
|
46
|
+
let first = true;
|
|
47
|
+
while (i < path.length) {
|
|
48
|
+
let deep = false;
|
|
49
|
+
const start = i;
|
|
50
|
+
if (path.startsWith("..", i)) {
|
|
51
|
+
deep = true;
|
|
52
|
+
i += 2;
|
|
53
|
+
} else if (path[i] === ".") {
|
|
54
|
+
i += 1;
|
|
55
|
+
} else if (path[i] !== "[" && !(first && i === 0)) {
|
|
56
|
+
fail(i, 'expected ".", ".." or "["');
|
|
57
|
+
}
|
|
58
|
+
first = false;
|
|
59
|
+
const char = path[i];
|
|
60
|
+
if (char === undefined) fail(i, "the path ends after a dot");
|
|
61
|
+
if (char === "*") {
|
|
62
|
+
steps.push({ kind: "wildcard", deep });
|
|
63
|
+
i += 1;
|
|
64
|
+
} else if (char === "[") {
|
|
65
|
+
if ((path[start] === "." && !deep)) fail(i, 'a "[" does not come after a single "."');
|
|
66
|
+
const close = (from: number): void => {
|
|
67
|
+
if (path[from] !== "]") fail(from, 'expected "]"');
|
|
68
|
+
};
|
|
69
|
+
const inner = path[i + 1];
|
|
70
|
+
if (inner === "*") {
|
|
71
|
+
close(i + 2);
|
|
72
|
+
steps.push({ kind: "wildcard", deep });
|
|
73
|
+
i += 3;
|
|
74
|
+
} else if (inner === '"' || inner === "'") {
|
|
75
|
+
let j = i + 2;
|
|
76
|
+
let name = "";
|
|
77
|
+
while (j < path.length && path[j] !== inner) {
|
|
78
|
+
if (path[j] === "\\") {
|
|
79
|
+
j += 1;
|
|
80
|
+
if (j >= path.length) break;
|
|
81
|
+
}
|
|
82
|
+
name += path[j];
|
|
83
|
+
j += 1;
|
|
84
|
+
}
|
|
85
|
+
if (j >= path.length) fail(i, "the quoted key has no end");
|
|
86
|
+
close(j + 1);
|
|
87
|
+
steps.push(key(name, deep, i));
|
|
88
|
+
i = j + 2;
|
|
89
|
+
} else {
|
|
90
|
+
const match = /^-?(0|[1-9][0-9]*)/.exec(path.slice(i + 1));
|
|
91
|
+
if (!match) fail(i + 1, "expected an index, a quoted key or *");
|
|
92
|
+
close(i + 1 + match![0].length);
|
|
93
|
+
steps.push({ kind: "index", index: Number(match![0]), deep });
|
|
94
|
+
i += match![0].length + 2;
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
let j = i;
|
|
98
|
+
while (j < path.length && NAME_CHAR.test(path[j]!)) j += 1;
|
|
99
|
+
if (j === i) fail(i, "expected a name, * or [");
|
|
100
|
+
steps.push(key(path.slice(i, j), deep, i));
|
|
101
|
+
i = j;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return steps;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The children of a value, in document order: members of an object, or items of an array. */
|
|
108
|
+
function children(match: PathMatch): PathMatch[] {
|
|
109
|
+
const { value, tokens } = match;
|
|
110
|
+
if (Array.isArray(value)) return value.map((item, index) => ({ tokens: [...tokens, index], value: item }));
|
|
111
|
+
if (value !== null && typeof value === "object") return Object.keys(value).map((name) => ({ tokens: [...tokens, name], value: value[name]! }));
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The value and each value in it, at each depth, in document order. */
|
|
116
|
+
function descendants(match: PathMatch): PathMatch[] {
|
|
117
|
+
const out: PathMatch[] = [];
|
|
118
|
+
const visit = (current: PathMatch): void => {
|
|
119
|
+
out.push(current);
|
|
120
|
+
for (const child of children(current)) visit(child);
|
|
121
|
+
};
|
|
122
|
+
visit(match);
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function stepOn(step: PathStep, match: PathMatch): PathMatch[] {
|
|
127
|
+
const { value, tokens } = match;
|
|
128
|
+
switch (step.kind) {
|
|
129
|
+
case "wildcard":
|
|
130
|
+
return children(match);
|
|
131
|
+
case "key":
|
|
132
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && hasOwn(value, step.key)
|
|
133
|
+
? [{ tokens: [...tokens, step.key], value: value[step.key]! }]
|
|
134
|
+
: [];
|
|
135
|
+
case "index": {
|
|
136
|
+
if (!Array.isArray(value)) return [];
|
|
137
|
+
const index = step.index < 0 ? value.length + step.index : step.index;
|
|
138
|
+
return index >= 0 && index < value.length ? [{ tokens: [...tokens, index], value: value[index]! }] : [];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** This function gives the matches of a path in a value, in document order. */
|
|
144
|
+
export function queryPath(value: JsonValue, path: string): PathMatch[] {
|
|
145
|
+
let matches: PathMatch[] = [{ tokens: [], value }];
|
|
146
|
+
for (const step of parsePath(path)) {
|
|
147
|
+
const next: PathMatch[] = [];
|
|
148
|
+
for (const match of matches) {
|
|
149
|
+
const sources = step.deep ? descendants(match) : [match];
|
|
150
|
+
for (const source of sources) next.push(...stepOn(step, source));
|
|
151
|
+
}
|
|
152
|
+
matches = next;
|
|
153
|
+
}
|
|
154
|
+
return matches;
|
|
155
|
+
}
|
package/src/pointer.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Pointer (RFC 6901): the parse, and the value at a pointer.
|
|
3
|
+
*
|
|
4
|
+
* A pointer is `""` (the whole value) or a list of reference tokens, each after a `/`. In a token,
|
|
5
|
+
* `~1` is `/` and `~0` is `~`. A pointer in the URI fragment form (`#/a/b`) is accepted too. A
|
|
6
|
+
* pointer that names `__proto__`, `constructor` or `prototype` is refused (roadmap R6).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { hasOwn, JsonError, PROTOTYPE_KEYS, type JsonValue } from "./value.js";
|
|
10
|
+
|
|
11
|
+
/** This function gives the reference tokens of a pointer. A bad pointer throws `INVALID_POINTER`. */
|
|
12
|
+
export function parsePointer(pointer: string, label = "pointer"): string[] {
|
|
13
|
+
let text = pointer;
|
|
14
|
+
let fragment = false;
|
|
15
|
+
if (text.startsWith("#")) {
|
|
16
|
+
fragment = true;
|
|
17
|
+
text = text.slice(1);
|
|
18
|
+
}
|
|
19
|
+
if (text === "") return [];
|
|
20
|
+
if (!text.startsWith("/")) {
|
|
21
|
+
throw new JsonError("INVALID_POINTER", `The ${label} ${JSON.stringify(pointer)} is not a JSON Pointer: it must be "" or start with "/"`);
|
|
22
|
+
}
|
|
23
|
+
return text
|
|
24
|
+
.slice(1)
|
|
25
|
+
.split("/")
|
|
26
|
+
.map((raw) => {
|
|
27
|
+
let token = raw;
|
|
28
|
+
if (fragment) {
|
|
29
|
+
try {
|
|
30
|
+
token = decodeURIComponent(raw);
|
|
31
|
+
} catch {
|
|
32
|
+
throw new JsonError("INVALID_POINTER", `The ${label} ${JSON.stringify(pointer)} has a bad percent escape`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (/~[^01]|~$/.test(token)) {
|
|
36
|
+
throw new JsonError("INVALID_POINTER", `The ${label} ${JSON.stringify(pointer)} has a "~" that is not "~0" or "~1"`);
|
|
37
|
+
}
|
|
38
|
+
token = token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
39
|
+
if (PROTOTYPE_KEYS.has(token)) {
|
|
40
|
+
throw new JsonError("PROTOTYPE_KEY", `The ${label} ${JSON.stringify(pointer)} has the token "${token}", which reaches a prototype`);
|
|
41
|
+
}
|
|
42
|
+
return token;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The index of an array token (`0`, or a digit 1 to 9 and more digits), or undefined. */
|
|
47
|
+
export function arrayIndex(token: string): number | undefined {
|
|
48
|
+
return /^(0|[1-9][0-9]*)$/.test(token) ? Number(token) : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The result of a look-up: found, with the value, or not found. */
|
|
52
|
+
export type Lookup = { readonly found: true; readonly value: JsonValue } | { readonly found: false };
|
|
53
|
+
|
|
54
|
+
/** This function gives the value at the tokens of a pointer. Only own keys of an object count. */
|
|
55
|
+
export function lookup(value: JsonValue, tokens: readonly string[]): Lookup {
|
|
56
|
+
let current: JsonValue = value;
|
|
57
|
+
for (const token of tokens) {
|
|
58
|
+
if (Array.isArray(current)) {
|
|
59
|
+
const index = arrayIndex(token);
|
|
60
|
+
if (index === undefined || index >= current.length) return { found: false };
|
|
61
|
+
current = current[index]!;
|
|
62
|
+
} else if (current !== null && typeof current === "object") {
|
|
63
|
+
if (!hasOwn(current, token)) return { found: false };
|
|
64
|
+
current = current[token]!;
|
|
65
|
+
} else {
|
|
66
|
+
return { found: false };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { found: true, value: current };
|
|
70
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* json.diff procedure
|
|
3
|
+
*
|
|
4
|
+
* Give the RFC 6902 JSON Patch that changes one JSON value into another. `json.patch` with this
|
|
5
|
+
* patch on `from` gives a value equal to `to`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { diffJson } from "../../diff.js";
|
|
9
|
+
import { assertJson } from "../../value.js";
|
|
10
|
+
import type { JsonDiffInput, JsonDiffOutput } from "../../types.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Give the RFC 6902 JSON Patch that changes one JSON value into another
|
|
14
|
+
*/
|
|
15
|
+
export async function jsonDiff(input: JsonDiffInput): Promise<JsonDiffOutput> {
|
|
16
|
+
assertJson(input.from, "json.diff: from");
|
|
17
|
+
assertJson(input.to, "json.diff: to");
|
|
18
|
+
return { patch: diffJson(input.from, input.to) };
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* json.merge procedure
|
|
3
|
+
*
|
|
4
|
+
* Apply an RFC 7386 JSON merge patch to a JSON value. A member with the value null removes its
|
|
5
|
+
* key. A patch that is not an object replaces the value.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mergePatch } from "../../merge.js";
|
|
9
|
+
import { assertJson } from "../../value.js";
|
|
10
|
+
import type { JsonMergeInput, JsonMergeOutput } from "../../types.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Apply an RFC 7386 JSON merge patch to a JSON value
|
|
14
|
+
*/
|
|
15
|
+
export async function jsonMerge(input: JsonMergeInput): Promise<JsonMergeOutput> {
|
|
16
|
+
assertJson(input.target, "json.merge: target");
|
|
17
|
+
assertJson(input.patch, "json.merge: patch");
|
|
18
|
+
return { value: mergePatch(input.target, input.patch) };
|
|
19
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* json.patch procedure
|
|
3
|
+
*
|
|
4
|
+
* Apply an RFC 6902 JSON Patch to a JSON value. All operations apply or none. A failed `test`
|
|
5
|
+
* operation is an error with the code `TEST_FAILED`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { applyPatch } from "../../patch.js";
|
|
9
|
+
import { assertJson } from "../../value.js";
|
|
10
|
+
import type { JsonPatchInput, JsonPatchOutput } from "../../types.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Apply an RFC 6902 JSON Patch to a JSON value
|
|
14
|
+
*/
|
|
15
|
+
export async function jsonPatch(input: JsonPatchInput): Promise<JsonPatchOutput> {
|
|
16
|
+
assertJson(input.value, "json.patch: value");
|
|
17
|
+
input.patch.forEach((operation, index) => {
|
|
18
|
+
if (operation.value !== undefined) assertJson(operation.value, `json.patch: the value of operation ${index}`);
|
|
19
|
+
});
|
|
20
|
+
return { value: applyPatch(input.value, input.patch) };
|
|
21
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* json.query procedure
|
|
3
|
+
*
|
|
4
|
+
* Find the values at a JSON Pointer or a path in a JSON value. A pointer gives one match or none.
|
|
5
|
+
* A path query can give more matches, in document order. Each match has its JSON Pointer.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { queryPath } from "../../path.js";
|
|
9
|
+
import { lookup, parsePointer } from "../../pointer.js";
|
|
10
|
+
import { assertJson, formatPointer, JsonError } from "../../value.js";
|
|
11
|
+
import type { JsonQueryInput, JsonQueryOutput } from "../../types.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Find the values at a JSON Pointer or a path in a JSON value
|
|
15
|
+
*/
|
|
16
|
+
export async function jsonQuery(input: JsonQueryInput): Promise<JsonQueryOutput> {
|
|
17
|
+
assertJson(input.value, "json.query: value");
|
|
18
|
+
if ((input.pointer === undefined) === (input.path === undefined)) {
|
|
19
|
+
throw new JsonError("INVALID_POINTER", "json.query: give one of pointer and path");
|
|
20
|
+
}
|
|
21
|
+
if (input.pointer !== undefined) {
|
|
22
|
+
const tokens = parsePointer(input.pointer);
|
|
23
|
+
const found = lookup(input.value, tokens);
|
|
24
|
+
if (!found.found) return { found: false, matches: [] };
|
|
25
|
+
return { found: true, value: found.value, matches: [{ pointer: formatPointer(tokens), value: found.value }] };
|
|
26
|
+
}
|
|
27
|
+
const matches = queryPath(input.value, input.path!).map((match) => ({ pointer: formatPointer(match.tokens), value: match.value }));
|
|
28
|
+
return matches.length === 0 ? { found: false, matches } : { found: true, value: matches[0]!.value, matches };
|
|
29
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* json.validate procedure
|
|
3
|
+
*
|
|
4
|
+
* Examine a JSON value against a JSON Schema. The check is a subset of JSON Schema (see
|
|
5
|
+
* `schema.ts`): `unchecked` lists the keywords of the schema that the check does not examine.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { validateJson } from "../../schema.js";
|
|
9
|
+
import { assertJson } from "../../value.js";
|
|
10
|
+
import type { JsonValidateInput, JsonValidateOutput } from "../../types.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Examine a JSON value against a JSON Schema
|
|
14
|
+
*/
|
|
15
|
+
export async function jsonValidate(input: JsonValidateInput): Promise<JsonValidateOutput> {
|
|
16
|
+
assertJson(input.value, "json.validate: value");
|
|
17
|
+
assertJson(input.schema, "json.validate: schema");
|
|
18
|
+
const result = validateJson(input.value, input.schema);
|
|
19
|
+
return { valid: result.valid, errors: [...result.errors], unchecked: [...result.unchecked] };
|
|
20
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* yaml.parse procedure
|
|
3
|
+
*
|
|
4
|
+
* Parse YAML text into a JSON value. With `all`, the text can have more than one document, and
|
|
5
|
+
* the value is the list of documents.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { parseYaml } from "../../yaml.js";
|
|
9
|
+
import type { YamlParseInput, YamlParseOutput } from "../../types.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse YAML text into a JSON value
|
|
13
|
+
*/
|
|
14
|
+
export async function yamlParse(input: YamlParseInput): Promise<YamlParseOutput> {
|
|
15
|
+
return { value: parseYaml(input.text, { all: input.all }) };
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* yaml.stringify procedure
|
|
3
|
+
*
|
|
4
|
+
* Write a JSON value as YAML text. `yaml.parse` of the text gives a value equal to the input.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { assertJson } from "../../value.js";
|
|
8
|
+
import { stringifyYaml } from "../../yaml.js";
|
|
9
|
+
import type { YamlStringifyInput, YamlStringifyOutput } from "../../types.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Write a JSON value as YAML text
|
|
13
|
+
*/
|
|
14
|
+
export async function yamlStringify(input: YamlStringifyInput): Promise<YamlStringifyOutput> {
|
|
15
|
+
assertJson(input.value, "yaml.stringify: value");
|
|
16
|
+
return { text: stringifyYaml(input.value, { indent: input.indent, lineWidth: input.lineWidth, sortKeys: input.sortKeys }) };
|
|
17
|
+
}
|
package/src/register.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Importing this file registers the procedures of @procedures/json in the global registry:
|
|
3
|
+
* `import "@procedures/json/register"`. The package root registers nothing.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { registerJsonProcedures } from "./module.js";
|
|
7
|
+
|
|
8
|
+
export * from "./module.js";
|
|
9
|
+
|
|
10
|
+
registerJsonProcedures();
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The check of a JSON value against a JSON Schema (`json.validate`).
|
|
3
|
+
*
|
|
4
|
+
* The check uses the validator of `@procedures/compiler` (`checkShape`), the validator that
|
|
5
|
+
* `program.check` uses for the inputs of a program. It is a subset of JSON Schema:
|
|
6
|
+
*
|
|
7
|
+
* - Examined: `type`, `enum`, `const`, `$ref` to the same schema (`#/...`), `allOf`, `anyOf`,
|
|
8
|
+
* `oneOf` (as `anyOf`), `properties`, `required`, `additionalProperties`, `patternProperties`,
|
|
9
|
+
* `items`, `prefixItems`, `additionalItems`, `minItems`, `maxItems`, `minLength`, `maxLength`,
|
|
10
|
+
* `pattern`, `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum` and `multipleOf`.
|
|
11
|
+
* - Annotations without a check, as the standard tells: `format`, `title`, `description`,
|
|
12
|
+
* `default`, `examples` and the other annotations.
|
|
13
|
+
* - Not examined: `not`, `if`, `then`, `else`, `uniqueItems`, `contains`, `minProperties`,
|
|
14
|
+
* `maxProperties`, `propertyNames`, `dependentRequired`, `dependentSchemas`, `unevaluated*`, a
|
|
15
|
+
* `$ref` to another document, and the others of `NOT_EXAMINED`. The output lists them in
|
|
16
|
+
* `unchecked`, so a caller knows that the verdict is not complete.
|
|
17
|
+
*
|
|
18
|
+
* `minLength` and `maxLength` count UTF-16 code units, not code points.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { checkShape } from "@procedures/compiler";
|
|
22
|
+
import { isPlainObject, JsonError, type JsonValue } from "./value.js";
|
|
23
|
+
|
|
24
|
+
/** One problem of a value. */
|
|
25
|
+
export interface ValidationIssue {
|
|
26
|
+
/** The JSON Pointer of the part of the value. */
|
|
27
|
+
readonly pointer: string;
|
|
28
|
+
readonly message: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The verdict of a check. */
|
|
32
|
+
export interface ValidationResult {
|
|
33
|
+
readonly valid: boolean;
|
|
34
|
+
readonly errors: readonly ValidationIssue[];
|
|
35
|
+
/** The keywords of the schema that the check does not examine, or examines only in part. */
|
|
36
|
+
readonly unchecked: readonly string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The assertion keywords of JSON Schema that the subset does not examine. */
|
|
40
|
+
export const NOT_EXAMINED: readonly string[] = [
|
|
41
|
+
"not",
|
|
42
|
+
"if",
|
|
43
|
+
"then",
|
|
44
|
+
"else",
|
|
45
|
+
"uniqueItems",
|
|
46
|
+
"contains",
|
|
47
|
+
"minContains",
|
|
48
|
+
"maxContains",
|
|
49
|
+
"minProperties",
|
|
50
|
+
"maxProperties",
|
|
51
|
+
"propertyNames",
|
|
52
|
+
"dependentRequired",
|
|
53
|
+
"dependentSchemas",
|
|
54
|
+
"dependencies",
|
|
55
|
+
"unevaluatedItems",
|
|
56
|
+
"unevaluatedProperties",
|
|
57
|
+
"$dynamicRef",
|
|
58
|
+
"$recursiveRef",
|
|
59
|
+
"nullable",
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
/** The keywords whose value is one schema. */
|
|
63
|
+
const ONE_SCHEMA = ["additionalProperties", "additionalItems", "items", "not", "if", "then", "else", "contains", "propertyNames", "unevaluatedItems", "unevaluatedProperties"];
|
|
64
|
+
/** The keywords whose value is a list of schemas. */
|
|
65
|
+
const SCHEMA_LIST = ["allOf", "anyOf", "oneOf", "prefixItems", "items"];
|
|
66
|
+
/** The keywords whose value is a map of schemas. */
|
|
67
|
+
const SCHEMA_MAP = ["properties", "patternProperties", "$defs", "definitions", "dependentSchemas"];
|
|
68
|
+
|
|
69
|
+
/** The keywords of a schema and its subschemas that the check does not examine fully. */
|
|
70
|
+
function uncheckedKeywords(schema: unknown): string[] {
|
|
71
|
+
const found = new Set<string>();
|
|
72
|
+
const seen = new Set<object>();
|
|
73
|
+
const visit = (node: unknown): void => {
|
|
74
|
+
if (!isPlainObject(node) || seen.has(node)) return;
|
|
75
|
+
seen.add(node);
|
|
76
|
+
for (const keyword of Object.keys(node)) {
|
|
77
|
+
if (NOT_EXAMINED.includes(keyword)) found.add(keyword);
|
|
78
|
+
}
|
|
79
|
+
if (node["oneOf"] !== undefined) found.add("oneOf (examined as anyOf)");
|
|
80
|
+
const ref = node["$ref"];
|
|
81
|
+
if (typeof ref === "string" && !(ref === "#" || ref.startsWith("#/"))) found.add(`$ref ${ref}`);
|
|
82
|
+
for (const keyword of ONE_SCHEMA) visit(node[keyword]);
|
|
83
|
+
for (const keyword of SCHEMA_LIST) {
|
|
84
|
+
const list = node[keyword];
|
|
85
|
+
if (Array.isArray(list)) list.forEach(visit);
|
|
86
|
+
}
|
|
87
|
+
for (const keyword of SCHEMA_MAP) {
|
|
88
|
+
const map = node[keyword];
|
|
89
|
+
if (isPlainObject(map)) Object.values(map).forEach(visit);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
visit(schema);
|
|
93
|
+
return [...found].sort();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** This function examines a JSON value against a JSON Schema. A schema that is not an object or a boolean throws. */
|
|
97
|
+
export function validateJson(value: JsonValue, schema: unknown): ValidationResult {
|
|
98
|
+
if (typeof schema !== "boolean" && !isPlainObject(schema)) {
|
|
99
|
+
throw new JsonError("INVALID_SCHEMA", "json.validate: the schema is not an object or a boolean");
|
|
100
|
+
}
|
|
101
|
+
const issues = checkShape({ kind: "known", value, pointer: "" }, schema);
|
|
102
|
+
const errors = issues.map((issue) => ({ pointer: issue.pointer, message: issue.message }));
|
|
103
|
+
return { valid: errors.length === 0, errors, unchecked: uncheckedKeywords(schema) };
|
|
104
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions of the procedures of this package
|
|
3
|
+
*
|
|
4
|
+
* mark procedure new adds the types of each new procedure here. The zod schemas are in
|
|
5
|
+
* `module.ts`: an exported schema needs a written type (isolatedDeclarations).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PatchOperation } from "./patch.js";
|
|
9
|
+
|
|
10
|
+
export type { JsonValue } from "./value.js";
|
|
11
|
+
export type { PatchOperation } from "./patch.js";
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// json.query Types - Find the values at a JSON Pointer or a path in a JSON value
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
export interface JsonQueryInput {
|
|
18
|
+
/** The JSON value to search. */
|
|
19
|
+
value: unknown;
|
|
20
|
+
/** A JSON Pointer (RFC 6901): `/items/0/name`. Give `pointer` or `path`. */
|
|
21
|
+
pointer?: string | undefined;
|
|
22
|
+
/** A path query: `items[*].name`, `$..id`. Give `pointer` or `path`. */
|
|
23
|
+
path?: string | undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface JsonQueryOutput {
|
|
27
|
+
/** True when one or more values match. */
|
|
28
|
+
found: boolean;
|
|
29
|
+
/** The value of the first match. */
|
|
30
|
+
value?: unknown;
|
|
31
|
+
/** Each match, in document order: its JSON Pointer and its value. */
|
|
32
|
+
matches: Array<{ pointer: string; value?: unknown }>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// json.patch Types - Apply an RFC 6902 JSON Patch to a JSON value
|
|
37
|
+
// =============================================================================
|
|
38
|
+
|
|
39
|
+
export interface JsonPatchInput {
|
|
40
|
+
value: unknown;
|
|
41
|
+
/** The operations, in order. */
|
|
42
|
+
patch: PatchOperation[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface JsonPatchOutput {
|
|
46
|
+
/** The value after the patch. */
|
|
47
|
+
value?: unknown;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// =============================================================================
|
|
51
|
+
// json.diff Types - Give the RFC 6902 JSON Patch that changes one JSON value into another
|
|
52
|
+
// =============================================================================
|
|
53
|
+
|
|
54
|
+
export interface JsonDiffInput {
|
|
55
|
+
from: unknown;
|
|
56
|
+
to: unknown;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface JsonDiffOutput {
|
|
60
|
+
/** The operations that change `from` into `to`. Equal values give an empty list. */
|
|
61
|
+
patch: PatchOperation[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// =============================================================================
|
|
65
|
+
// json.merge Types - Apply an RFC 7386 JSON merge patch to a JSON value
|
|
66
|
+
// =============================================================================
|
|
67
|
+
|
|
68
|
+
export interface JsonMergeInput {
|
|
69
|
+
/** The value to change. */
|
|
70
|
+
target: unknown;
|
|
71
|
+
/** The merge patch: an object of changes, where null removes a key. */
|
|
72
|
+
patch: unknown;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface JsonMergeOutput {
|
|
76
|
+
value?: unknown;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// =============================================================================
|
|
80
|
+
// json.validate Types - Examine a JSON value against a JSON Schema
|
|
81
|
+
// =============================================================================
|
|
82
|
+
|
|
83
|
+
export interface JsonValidateInput {
|
|
84
|
+
value: unknown;
|
|
85
|
+
/** The JSON Schema: an object or a boolean. */
|
|
86
|
+
schema: unknown;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface JsonValidateOutput {
|
|
90
|
+
valid: boolean;
|
|
91
|
+
/** Each problem: the JSON Pointer of the part of the value, and a message. */
|
|
92
|
+
errors: Array<{ pointer: string; message: string }>;
|
|
93
|
+
/** The keywords of the schema that the check does not examine. Empty: the verdict is complete. */
|
|
94
|
+
unchecked: string[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// =============================================================================
|
|
98
|
+
// yaml.parse Types - Parse YAML text into a JSON value
|
|
99
|
+
// =============================================================================
|
|
100
|
+
|
|
101
|
+
export interface YamlParseInput {
|
|
102
|
+
text: string;
|
|
103
|
+
/** True: the text can have more than one document, and `value` is the list of documents. */
|
|
104
|
+
all?: boolean | undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface YamlParseOutput {
|
|
108
|
+
value?: unknown;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// =============================================================================
|
|
112
|
+
// yaml.stringify Types - Write a JSON value as YAML text
|
|
113
|
+
// =============================================================================
|
|
114
|
+
|
|
115
|
+
export interface YamlStringifyInput {
|
|
116
|
+
value: unknown;
|
|
117
|
+
/** The spaces of one level of indentation: 1 to 8. The default is 2. */
|
|
118
|
+
indent?: number | undefined;
|
|
119
|
+
/** The longest line before a fold. 0: no fold. The default is 80. */
|
|
120
|
+
lineWidth?: number | undefined;
|
|
121
|
+
/** True: the keys of each map in alphabetical order. */
|
|
122
|
+
sortKeys?: boolean | undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface YamlStringifyOutput {
|
|
126
|
+
text: string;
|
|
127
|
+
}
|