@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.
Files changed (110) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/LICENSE +21 -0
  3. package/README.md +104 -2
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/diff.d.ts +18 -0
  6. package/dist/diff.d.ts.map +1 -0
  7. package/dist/diff.js +66 -0
  8. package/dist/diff.js.map +1 -0
  9. package/dist/index.d.ts +13 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +15 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/merge.d.ts +14 -0
  14. package/dist/merge.d.ts.map +1 -0
  15. package/dist/merge.js +34 -0
  16. package/dist/merge.js.map +1 -0
  17. package/dist/module.d.ts +21 -0
  18. package/dist/module.d.ts.map +1 -0
  19. package/dist/module.js +192 -0
  20. package/dist/module.js.map +1 -0
  21. package/dist/patch.d.ts +23 -0
  22. package/dist/patch.d.ts.map +1 -0
  23. package/dist/patch.js +137 -0
  24. package/dist/patch.js.map +1 -0
  25. package/dist/path.d.ts +38 -0
  26. package/dist/path.d.ts.map +1 -0
  27. package/dist/path.js +157 -0
  28. package/dist/path.js.map +1 -0
  29. package/dist/pointer.d.ts +22 -0
  30. package/dist/pointer.d.ts.map +1 -0
  31. package/dist/pointer.js +70 -0
  32. package/dist/pointer.js.map +1 -0
  33. package/dist/procedures/json/diff.d.ts +12 -0
  34. package/dist/procedures/json/diff.d.ts.map +1 -0
  35. package/dist/procedures/json/diff.js +17 -0
  36. package/dist/procedures/json/diff.js.map +1 -0
  37. package/dist/procedures/json/index.d.ts +6 -0
  38. package/dist/procedures/json/index.d.ts.map +1 -0
  39. package/dist/procedures/json/index.js +6 -0
  40. package/dist/procedures/json/index.js.map +1 -0
  41. package/dist/procedures/json/merge.d.ts +12 -0
  42. package/dist/procedures/json/merge.d.ts.map +1 -0
  43. package/dist/procedures/json/merge.js +17 -0
  44. package/dist/procedures/json/merge.js.map +1 -0
  45. package/dist/procedures/json/patch.d.ts +12 -0
  46. package/dist/procedures/json/patch.d.ts.map +1 -0
  47. package/dist/procedures/json/patch.js +20 -0
  48. package/dist/procedures/json/patch.js.map +1 -0
  49. package/dist/procedures/json/query.d.ts +12 -0
  50. package/dist/procedures/json/query.d.ts.map +1 -0
  51. package/dist/procedures/json/query.js +28 -0
  52. package/dist/procedures/json/query.js.map +1 -0
  53. package/dist/procedures/json/validate.d.ts +12 -0
  54. package/dist/procedures/json/validate.d.ts.map +1 -0
  55. package/dist/procedures/json/validate.js +18 -0
  56. package/dist/procedures/json/validate.js.map +1 -0
  57. package/dist/procedures/yaml/index.d.ts +3 -0
  58. package/dist/procedures/yaml/index.d.ts.map +1 -0
  59. package/dist/procedures/yaml/index.js +3 -0
  60. package/dist/procedures/yaml/index.js.map +1 -0
  61. package/dist/procedures/yaml/parse.d.ts +12 -0
  62. package/dist/procedures/yaml/parse.d.ts.map +1 -0
  63. package/dist/procedures/yaml/parse.js +14 -0
  64. package/dist/procedures/yaml/parse.js.map +1 -0
  65. package/dist/procedures/yaml/stringify.d.ts +11 -0
  66. package/dist/procedures/yaml/stringify.d.ts.map +1 -0
  67. package/dist/procedures/yaml/stringify.js +15 -0
  68. package/dist/procedures/yaml/stringify.js.map +1 -0
  69. package/dist/register.d.ts +6 -0
  70. package/dist/register.d.ts.map +1 -0
  71. package/dist/register.js +8 -0
  72. package/dist/register.js.map +1 -0
  73. package/dist/schema.d.ts +38 -0
  74. package/dist/schema.d.ts.map +1 -0
  75. package/dist/schema.js +92 -0
  76. package/dist/schema.js.map +1 -0
  77. package/dist/types.d.ts +90 -0
  78. package/dist/types.d.ts.map +1 -0
  79. package/dist/types.js +8 -0
  80. package/dist/types.js.map +1 -0
  81. package/dist/value.d.ts +61 -0
  82. package/dist/value.d.ts.map +1 -0
  83. package/dist/value.js +124 -0
  84. package/dist/value.js.map +1 -0
  85. package/dist/yaml.d.ts +35 -0
  86. package/dist/yaml.d.ts.map +1 -0
  87. package/dist/yaml.js +111 -0
  88. package/dist/yaml.js.map +1 -0
  89. package/package.json +81 -3
  90. package/src/diff.ts +58 -0
  91. package/src/index.ts +14 -0
  92. package/src/merge.ts +34 -0
  93. package/src/module.ts +236 -0
  94. package/src/patch.ts +132 -0
  95. package/src/path.ts +155 -0
  96. package/src/pointer.ts +70 -0
  97. package/src/procedures/json/diff.ts +19 -0
  98. package/src/procedures/json/index.ts +5 -0
  99. package/src/procedures/json/merge.ts +19 -0
  100. package/src/procedures/json/patch.ts +21 -0
  101. package/src/procedures/json/query.ts +29 -0
  102. package/src/procedures/json/validate.ts +20 -0
  103. package/src/procedures/yaml/index.ts +2 -0
  104. package/src/procedures/yaml/parse.ts +16 -0
  105. package/src/procedures/yaml/stringify.ts +17 -0
  106. package/src/register.ts +10 -0
  107. package/src/schema.ts +104 -0
  108. package/src/types.ts +127 -0
  109. package/src/value.ts +143 -0
  110. package/src/yaml.ts +128 -0
package/package.json CHANGED
@@ -1,6 +1,84 @@
1
1
  {
2
+ "$schema": "https://json.schemastore.org/package",
2
3
  "name": "@procedures/json",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
4
+ "version": "0.3.0",
5
+ "description": "Pure JSON and YAML procedures: a query with JSON Pointer or a path, JSON Patch, a diff, a merge patch, a JSON Schema check, and YAML to JSON and back.",
6
+ "keywords": [
7
+ "procedures",
8
+ "json",
9
+ "yaml",
10
+ "json-pointer",
11
+ "json-patch",
12
+ "merge-patch",
13
+ "json-schema",
14
+ "diff"
15
+ ],
16
+ "homepage": "https://mark1russell7.github.io/client/",
17
+ "bugs": {
18
+ "url": "https://github.com/mark1russell7/client/issues"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/mark1russell7/client.git",
23
+ "directory": "packages/json"
24
+ },
25
+ "license": "MIT",
26
+ "author": "Mark Russell (https://github.com/mark1russell7)",
27
+ "type": "module",
28
+ "main": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js"
34
+ },
35
+ "./module": {
36
+ "types": "./dist/module.d.ts",
37
+ "import": "./dist/module.js"
38
+ },
39
+ "./register": {
40
+ "types": "./dist/register.d.ts",
41
+ "import": "./dist/register.js"
42
+ }
43
+ },
44
+ "files": [
45
+ "dist",
46
+ "src",
47
+ "!src/**/*.test.ts",
48
+ "!src/**/*.test.tsx",
49
+ "!src/**/__tests__",
50
+ "CHANGELOG.md"
51
+ ],
52
+ "sideEffects": [
53
+ "./dist/register.js"
54
+ ],
55
+ "devDependencies": {
56
+ "@mark1russell7/cue": "^1.0.0",
57
+ "fast-check": "^4.10.2",
58
+ "typescript": "^7.0.2",
59
+ "vitest": "^5.0.3"
60
+ },
61
+ "engines": {
62
+ "node": ">=22"
63
+ },
64
+ "publishConfig": {
65
+ "access": "public",
66
+ "provenance": true
67
+ },
68
+ "client": {
69
+ "procedures": "./dist/register.js"
70
+ },
71
+ "dependencies": {
72
+ "@procedures/client": "0.3.0",
73
+ "@procedures/compiler": "0.3.0",
74
+ "yaml": "^2.9.1",
75
+ "zod": "^4.6.5"
76
+ },
77
+ "scripts": {
78
+ "build": "tsc -b",
79
+ "typecheck": "tsc --noEmit",
80
+ "clean": "rm -rf dist .tsbuildinfo",
81
+ "test": "vitest run",
82
+ "test:watch": "vitest"
83
+ }
6
84
  }
package/src/diff.ts ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The diff of two JSON values: a JSON Patch (RFC 6902) that changes the first value into the
3
+ * second value. `applyPatch(a, diffJson(a, b))` gives a value equal to `b`.
4
+ *
5
+ * - Two objects: a `remove` for each key that only the first has, a diff of each shared key, and
6
+ * an `add` for each key that only the second has.
7
+ * - Two arrays: the items of the shared start and end stay. The items between them get a diff
8
+ * for each shared position, then a `remove` or an `add` for each other item.
9
+ * - Other values: one `replace`.
10
+ *
11
+ * The patch never names `__proto__`, `constructor` or `prototype`, because `applyPatch` refuses
12
+ * them. When a difference is under such a key, the patch replaces the object that holds the key.
13
+ */
14
+
15
+ import type { PatchOperation } from "./patch.js";
16
+ import { cloneJson, formatPointer, hasOwn, jsonEqual, PROTOTYPE_KEYS, type JsonValue } from "./value.js";
17
+
18
+ function diffAt(a: JsonValue, b: JsonValue, tokens: (string | number)[], out: PatchOperation[]): void {
19
+ if (jsonEqual(a, b)) return;
20
+ const path = formatPointer(tokens);
21
+ if (Array.isArray(a) && Array.isArray(b)) {
22
+ diffArrays(a, b, tokens, out);
23
+ return;
24
+ }
25
+ if (a !== null && b !== null && typeof a === "object" && typeof b === "object" && !Array.isArray(a) && !Array.isArray(b)) {
26
+ const keysA = Object.keys(a);
27
+ const keysB = Object.keys(b);
28
+ const named = [...keysA, ...keysB].some((key) => PROTOTYPE_KEYS.has(key) && !(hasOwn(a, key) && hasOwn(b, key) && jsonEqual(a[key]!, b[key]!)));
29
+ if (!named) {
30
+ for (const key of keysA) if (!hasOwn(b, key)) out.push({ op: "remove", path: formatPointer([...tokens, key]) });
31
+ for (const key of keysA) if (hasOwn(b, key)) diffAt(a[key]!, b[key]!, [...tokens, key], out);
32
+ for (const key of keysB) if (!hasOwn(a, key)) out.push({ op: "add", path: formatPointer([...tokens, key]), value: cloneJson(b[key]!) });
33
+ return;
34
+ }
35
+ }
36
+ out.push({ op: "replace", path, value: cloneJson(b) });
37
+ }
38
+
39
+ function diffArrays(a: JsonValue[], b: JsonValue[], tokens: (string | number)[], out: PatchOperation[]): void {
40
+ let start = 0;
41
+ while (start < a.length && start < b.length && jsonEqual(a[start]!, b[start]!)) start++;
42
+ let end = 0;
43
+ while (end < a.length - start && end < b.length - start && jsonEqual(a[a.length - 1 - end]!, b[b.length - 1 - end]!)) end++;
44
+ const middleA = a.length - start - end;
45
+ const middleB = b.length - start - end;
46
+ const shared = Math.min(middleA, middleB);
47
+ for (let i = 0; i < shared; i++) diffAt(a[start + i]!, b[start + i]!, [...tokens, start + i], out);
48
+ // The items after the shared positions: remove from the last one, so each index stays valid
49
+ for (let i = middleA - 1; i >= shared; i--) out.push({ op: "remove", path: formatPointer([...tokens, start + i]) });
50
+ for (let i = shared; i < middleB; i++) out.push({ op: "add", path: formatPointer([...tokens, start + i]), value: cloneJson(b[start + i]!) });
51
+ }
52
+
53
+ /** This function gives a JSON Patch that changes `a` into `b`. Equal values give an empty patch. */
54
+ export function diffJson(a: JsonValue, b: JsonValue): PatchOperation[] {
55
+ const out: PatchOperation[] = [];
56
+ diffAt(a, b, [], out);
57
+ return out;
58
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ // The root registers nothing: import "<package>/register", or give the module to createRegistry()
2
+ export * from "./module.js";
3
+ export * from "./types.js";
4
+ export { jsonQuery, jsonPatch, jsonDiff, jsonMerge, jsonValidate } from "./procedures/json/index.js";
5
+ export { yamlParse, yamlStringify } from "./procedures/yaml/index.js";
6
+ // The functions behind the procedures, for a program in the same process
7
+ export { assertJson, cloneJson, jsonEqual, JsonError, PROTOTYPE_KEYS, formatPointer, escapeToken, type JsonErrorCode } from "./value.js";
8
+ export { parsePointer, lookup, arrayIndex, type Lookup } from "./pointer.js";
9
+ export { parsePath, queryPath, type PathMatch, type PathStep } from "./path.js";
10
+ export { applyPatch } from "./patch.js";
11
+ export { diffJson } from "./diff.js";
12
+ export { mergePatch } from "./merge.js";
13
+ export { validateJson, NOT_EXAMINED, type ValidationIssue, type ValidationResult } from "./schema.js";
14
+ export { parseYaml, stringifyYaml, type YamlParseOptions, type YamlStringifyOptions } from "./yaml.js";
package/src/merge.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * JSON Merge Patch (RFC 7386): a patch is a JSON value of the same form as the target.
3
+ *
4
+ * - A patch that is an object changes the target member by member. A member with the value null
5
+ * removes the key. Another member merges into the key.
6
+ * - A patch that is not an object replaces the target.
7
+ *
8
+ * The merge applies to a copy of the target. A key `__proto__`, `constructor` or `prototype` in
9
+ * an object of the patch is refused (roadmap R6).
10
+ */
11
+
12
+ import { cloneJson, formatPointer, hasOwn, isPlainObject, JsonError, PROTOTYPE_KEYS, put, type JsonValue } from "./value.js";
13
+
14
+ function mergeAt(target: JsonValue | undefined, patch: JsonValue, tokens: string[]): JsonValue {
15
+ if (!isPlainObject(patch)) return cloneJson(patch);
16
+ const result: Record<string, JsonValue> = isPlainObject(target) ? cloneJson(target as Record<string, JsonValue>) : {};
17
+ for (const key of Object.keys(patch)) {
18
+ if (PROTOTYPE_KEYS.has(key)) {
19
+ throw new JsonError("PROTOTYPE_KEY", `The merge patch has the key "${key}" at "${formatPointer([...tokens, key])}", which reaches a prototype`);
20
+ }
21
+ const value = patch[key] as JsonValue;
22
+ if (value === null) {
23
+ delete result[key];
24
+ } else {
25
+ put(result, key, mergeAt(hasOwn(result, key) ? result[key] : undefined, value, [...tokens, key]));
26
+ }
27
+ }
28
+ return result;
29
+ }
30
+
31
+ /** This function applies a JSON merge patch to a copy of the target and gives the result. */
32
+ export function mergePatch(target: JsonValue, patch: JsonValue): JsonValue {
33
+ return mergeAt(target, patch, []);
34
+ }
package/src/module.ts ADDED
@@ -0,0 +1,236 @@
1
+ /**
2
+ * The procedures of @procedures/json as data. Importing this file registers nothing.
3
+ *
4
+ * Scaffold a procedure with: mark procedure new <name> --path packages/json
5
+ *
6
+ * Each procedure is pure (`PURE_EFFECTS`): its output comes from its input, and it reads and
7
+ * changes nothing else (roadmap R39).
8
+ */
9
+
10
+ import { z } from "zod";
11
+ import { defineModule, registerModules, PROCEDURE_REGISTRY, PURE_EFFECTS, type ProcedureModule, type ProcedureRegistry, createProcedure, zodAdapter } from "@procedures/client";
12
+ import { jsonQuery } from "./procedures/json/query.js";
13
+ import { jsonPatch } from "./procedures/json/patch.js";
14
+ import { jsonDiff } from "./procedures/json/diff.js";
15
+ import { jsonMerge } from "./procedures/json/merge.js";
16
+ import { jsonValidate } from "./procedures/json/validate.js";
17
+ import { yamlParse } from "./procedures/yaml/parse.js";
18
+ import { yamlStringify } from "./procedures/yaml/stringify.js";
19
+ import type {
20
+ JsonDiffInput,
21
+ JsonDiffOutput,
22
+ JsonMergeInput,
23
+ JsonMergeOutput,
24
+ JsonPatchInput,
25
+ JsonPatchOutput,
26
+ JsonQueryInput,
27
+ JsonQueryOutput,
28
+ JsonValidateInput,
29
+ JsonValidateOutput,
30
+ YamlParseInput,
31
+ YamlParseOutput,
32
+ YamlStringifyInput,
33
+ YamlStringifyOutput,
34
+ } from "./types.js";
35
+
36
+ // =============================================================================
37
+ // Schemas
38
+ // =============================================================================
39
+
40
+ /** A JSON value. The handler examines it, so a key `__proto__` of the data reaches the check. */
41
+ const json = (description: string) => z.unknown().describe(description);
42
+
43
+ const operation = z.object({
44
+ op: z.enum(["add", "remove", "replace", "move", "copy", "test"]).describe("The operation"),
45
+ path: z.string().describe("The JSON Pointer of the location"),
46
+ from: z.string().optional().describe("The JSON Pointer of the source of move and copy"),
47
+ value: json("The value of add, replace and test").optional(),
48
+ });
49
+
50
+ const JsonQueryInputSchema = z.object({
51
+ value: json("The JSON value to search"),
52
+ pointer: z.string().optional().describe("A JSON Pointer (RFC 6901), for example /items/0/name. Give pointer or path"),
53
+ path: z.string().optional().describe("A path query, for example items[*].name or $..id. Give pointer or path"),
54
+ });
55
+
56
+ const JsonQueryOutputSchema = z.object({
57
+ found: z.boolean().describe("True when one or more values match"),
58
+ value: json("The value of the first match").optional(),
59
+ matches: z.array(z.object({ pointer: z.string().describe("The JSON Pointer of the match"), value: json("The value of the match") })).describe("Each match, in document order"),
60
+ });
61
+
62
+ const JsonPatchInputSchema = z.object({
63
+ value: json("The JSON value to change"),
64
+ patch: z.array(operation).describe("The operations of the JSON Patch (RFC 6902), in order"),
65
+ });
66
+
67
+ const JsonPatchOutputSchema = z.object({ value: json("The value after the patch") });
68
+
69
+ const JsonDiffInputSchema = z.object({
70
+ from: json("The first JSON value"),
71
+ to: json("The second JSON value"),
72
+ });
73
+
74
+ const JsonDiffOutputSchema = z.object({
75
+ patch: z.array(operation).describe("The JSON Patch (RFC 6902) that changes from into to"),
76
+ });
77
+
78
+ const JsonMergeInputSchema = z.object({
79
+ target: json("The JSON value to change"),
80
+ patch: json("The merge patch (RFC 7386): an object of changes, where null removes a key"),
81
+ });
82
+
83
+ const JsonMergeOutputSchema = z.object({ value: json("The value after the merge") });
84
+
85
+ const JsonValidateInputSchema = z.object({
86
+ value: json("The JSON value to examine"),
87
+ schema: json("The JSON Schema: an object or a boolean"),
88
+ });
89
+
90
+ const JsonValidateOutputSchema = z.object({
91
+ valid: z.boolean().describe("True when the check found no problem"),
92
+ errors: z.array(z.object({ pointer: z.string().describe("The JSON Pointer of the part"), message: z.string().describe("The problem") })).describe("Each problem"),
93
+ unchecked: z.array(z.string()).describe("The keywords of the schema that the check does not examine. Empty: the verdict is complete"),
94
+ });
95
+
96
+ const YamlParseInputSchema = z.object({
97
+ text: z.string().describe("The YAML text"),
98
+ all: z.boolean().optional().describe("True: the text can have more than one document, and value is the list of documents"),
99
+ });
100
+
101
+ const YamlParseOutputSchema = z.object({ value: json("The JSON value of the text") });
102
+
103
+ const YamlStringifyInputSchema = z.object({
104
+ value: json("The JSON value to write"),
105
+ indent: z.number().int().min(1).max(8).optional().describe("The spaces of one level of indentation (default: 2)"),
106
+ lineWidth: z.number().int().min(0).optional().describe("The longest line before a fold. 0: no fold (default: 80)"),
107
+ sortKeys: z.boolean().optional().describe("True: the keys of each map in alphabetical order"),
108
+ });
109
+
110
+ const YamlStringifyOutputSchema = z.object({ text: z.string().describe("The YAML text") });
111
+
112
+ // =============================================================================
113
+ // Procedures
114
+ // =============================================================================
115
+
116
+ const jsonQueryProcedure = createProcedure()
117
+ .path(["json", "query"])
118
+ .input(zodAdapter<JsonQueryInput>(JsonQueryInputSchema))
119
+ .output(zodAdapter<JsonQueryOutput>(JsonQueryOutputSchema))
120
+ .meta({
121
+ description: "Find the values at a JSON Pointer or a path in a JSON value",
122
+ args: [],
123
+ shorts: { pointer: "p" },
124
+ output: "json",
125
+ effects: PURE_EFFECTS,
126
+ })
127
+ .handler(async (input: JsonQueryInput): Promise<JsonQueryOutput> => jsonQuery(input))
128
+ .build();
129
+
130
+ const jsonPatchProcedure = createProcedure()
131
+ .path(["json", "patch"])
132
+ .input(zodAdapter<JsonPatchInput>(JsonPatchInputSchema))
133
+ .output(zodAdapter<JsonPatchOutput>(JsonPatchOutputSchema))
134
+ .meta({
135
+ description: "Apply an RFC 6902 JSON Patch to a JSON value",
136
+ args: [],
137
+ shorts: {},
138
+ output: "json",
139
+ effects: PURE_EFFECTS,
140
+ })
141
+ .handler(async (input: JsonPatchInput): Promise<JsonPatchOutput> => jsonPatch(input))
142
+ .build();
143
+
144
+ const jsonDiffProcedure = createProcedure()
145
+ .path(["json", "diff"])
146
+ .input(zodAdapter<JsonDiffInput>(JsonDiffInputSchema))
147
+ .output(zodAdapter<JsonDiffOutput>(JsonDiffOutputSchema))
148
+ .meta({
149
+ description: "Give the RFC 6902 JSON Patch that changes one JSON value into another",
150
+ args: [],
151
+ shorts: {},
152
+ output: "json",
153
+ effects: PURE_EFFECTS,
154
+ })
155
+ .handler(async (input: JsonDiffInput): Promise<JsonDiffOutput> => jsonDiff(input))
156
+ .build();
157
+
158
+ const jsonMergeProcedure = createProcedure()
159
+ .path(["json", "merge"])
160
+ .input(zodAdapter<JsonMergeInput>(JsonMergeInputSchema))
161
+ .output(zodAdapter<JsonMergeOutput>(JsonMergeOutputSchema))
162
+ .meta({
163
+ description: "Apply an RFC 7386 JSON merge patch to a JSON value",
164
+ args: [],
165
+ shorts: {},
166
+ output: "json",
167
+ effects: PURE_EFFECTS,
168
+ })
169
+ .handler(async (input: JsonMergeInput): Promise<JsonMergeOutput> => jsonMerge(input))
170
+ .build();
171
+
172
+ const jsonValidateProcedure = createProcedure()
173
+ .path(["json", "validate"])
174
+ .input(zodAdapter<JsonValidateInput>(JsonValidateInputSchema))
175
+ .output(zodAdapter<JsonValidateOutput>(JsonValidateOutputSchema))
176
+ .meta({
177
+ description: "Examine a JSON value against a JSON Schema",
178
+ args: [],
179
+ shorts: {},
180
+ output: "json",
181
+ effects: PURE_EFFECTS,
182
+ })
183
+ .handler(async (input: JsonValidateInput): Promise<JsonValidateOutput> => jsonValidate(input))
184
+ .build();
185
+
186
+ const yamlParseProcedure = createProcedure()
187
+ .path(["yaml", "parse"])
188
+ .input(zodAdapter<YamlParseInput>(YamlParseInputSchema))
189
+ .output(zodAdapter<YamlParseOutput>(YamlParseOutputSchema))
190
+ .meta({
191
+ description: "Parse YAML text into a JSON value",
192
+ args: ["text"],
193
+ shorts: { all: "a" },
194
+ output: "json",
195
+ effects: PURE_EFFECTS,
196
+ })
197
+ .handler(async (input: YamlParseInput): Promise<YamlParseOutput> => yamlParse(input))
198
+ .build();
199
+
200
+ const yamlStringifyProcedure = createProcedure()
201
+ .path(["yaml", "stringify"])
202
+ .input(zodAdapter<YamlStringifyInput>(YamlStringifyInputSchema))
203
+ .output(zodAdapter<YamlStringifyOutput>(YamlStringifyOutputSchema))
204
+ .meta({
205
+ description: "Write a JSON value as YAML text",
206
+ args: [],
207
+ shorts: { indent: "i", sortKeys: "s" },
208
+ output: "json",
209
+ effects: PURE_EFFECTS,
210
+ })
211
+ .handler(async (input: YamlStringifyInput): Promise<YamlStringifyOutput> => yamlStringify(input))
212
+ .build();
213
+
214
+ /**
215
+ * The module of this package, for `createRegistry()`: its procedures as data. Importing this
216
+ * file registers nothing.
217
+ */
218
+ export const jsonModule: ProcedureModule = defineModule("@procedures/json", [
219
+ jsonQueryProcedure,
220
+ jsonPatchProcedure,
221
+ jsonDiffProcedure,
222
+ jsonMergeProcedure,
223
+ jsonValidateProcedure,
224
+ yamlParseProcedure,
225
+ yamlStringifyProcedure,
226
+ ]);
227
+
228
+ export default jsonModule;
229
+
230
+ /**
231
+ * This function registers the procedures of @procedures/json in a registry. The default is the
232
+ * global registry.
233
+ */
234
+ export function registerJsonProcedures(registry: ProcedureRegistry = PROCEDURE_REGISTRY): void {
235
+ registerModules(registry, [jsonModule]);
236
+ }
package/src/patch.ts ADDED
@@ -0,0 +1,132 @@
1
+ /**
2
+ * JSON Patch (RFC 6902): apply a list of operations to a JSON value.
3
+ *
4
+ * The operations are `add`, `remove`, `replace`, `move`, `copy` and `test`. The patch applies to
5
+ * a copy of the value, and all operations apply or none: the first failure throws, and the input
6
+ * value does not change. A `path` or a `from` that names `__proto__`, `constructor` or
7
+ * `prototype` is refused (roadmap R6).
8
+ */
9
+
10
+ import { arrayIndex, lookup, parsePointer } from "./pointer.js";
11
+ import { cloneJson, formatPointer, isPlainObject, jsonEqual, JsonError, put, typeName, type JsonValue } from "./value.js";
12
+
13
+ /** One operation of a JSON Patch. */
14
+ export interface PatchOperation {
15
+ op: "add" | "remove" | "replace" | "move" | "copy" | "test";
16
+ path: string;
17
+ from?: string | undefined;
18
+ value?: unknown;
19
+ }
20
+
21
+ /** The parent of a location: the container and the last token. */
22
+ function parentOf(document: JsonValue, tokens: readonly string[], fail: (message: string) => never): { container: JsonValue[] | Record<string, JsonValue>; token: string } {
23
+ const parentTokens = tokens.slice(0, -1);
24
+ const found = lookup(document, parentTokens);
25
+ if (!found.found) fail(`the location ${formatPointer(parentTokens)} does not exist`);
26
+ const container = (found as { value: JsonValue }).value;
27
+ if (container === null || typeof container !== "object") fail(`the location ${formatPointer(parentTokens)} is ${typeName(container)}, not an object or an array`);
28
+ return { container: container as JsonValue[] | Record<string, JsonValue>, token: tokens[tokens.length - 1]! };
29
+ }
30
+
31
+ function add(document: JsonValue, tokens: readonly string[], value: JsonValue, fail: (message: string) => never): JsonValue {
32
+ if (tokens.length === 0) return value;
33
+ const { container, token } = parentOf(document, tokens, fail);
34
+ if (Array.isArray(container)) {
35
+ if (token === "-") {
36
+ container.push(value);
37
+ return document;
38
+ }
39
+ const index = arrayIndex(token);
40
+ if (index === undefined || index > container.length) fail(`the array index "${token}" is not 0 to ${container.length} or "-"`);
41
+ container.splice(index!, 0, value);
42
+ return document;
43
+ }
44
+ put(container, token, value);
45
+ return document;
46
+ }
47
+
48
+ function remove(document: JsonValue, tokens: readonly string[], fail: (message: string) => never): { document: JsonValue; removed: JsonValue } {
49
+ if (tokens.length === 0) fail("a patch cannot remove the whole value");
50
+ const { container, token } = parentOf(document, tokens, fail);
51
+ if (Array.isArray(container)) {
52
+ const index = arrayIndex(token);
53
+ if (index === undefined || index >= container.length) fail(`the array index "${token}" does not exist`);
54
+ const [removed] = container.splice(index!, 1);
55
+ return { document, removed: removed! };
56
+ }
57
+ if (!Object.prototype.hasOwnProperty.call(container, token)) fail(`the location ${formatPointer(tokens)} does not exist`);
58
+ const removed = container[token]!;
59
+ delete container[token];
60
+ return { document, removed };
61
+ }
62
+
63
+ function replace(document: JsonValue, tokens: readonly string[], value: JsonValue, fail: (message: string) => never): JsonValue {
64
+ if (tokens.length === 0) return value;
65
+ if (!lookup(document, tokens).found) fail(`the location ${formatPointer(tokens)} does not exist`);
66
+ const { container, token } = parentOf(document, tokens, fail);
67
+ if (Array.isArray(container)) container[arrayIndex(token)!] = value;
68
+ else put(container, token, value);
69
+ return document;
70
+ }
71
+
72
+ /** The value of an operation: present, and JSON. */
73
+ function valueOf(operation: PatchOperation, fail: (message: string) => never): JsonValue {
74
+ if (!("value" in operation) || operation.value === undefined) fail(`the operation ${operation.op} needs a value`);
75
+ return cloneJson(operation.value as JsonValue);
76
+ }
77
+
78
+ /**
79
+ * This function applies a JSON Patch to a copy of a value and gives the result. A bad operation,
80
+ * a location that does not exist and a failed `test` throw a `JsonError` that names the index of
81
+ * the operation.
82
+ */
83
+ export function applyPatch(value: JsonValue, operations: readonly PatchOperation[]): JsonValue {
84
+ let document = cloneJson(value);
85
+ operations.forEach((operation, index) => {
86
+ const fail = (message: string, code: "INVALID_PATCH" | "TEST_FAILED" = "INVALID_PATCH"): never => {
87
+ throw new JsonError(code, `Operation ${index} (${operation.op} ${operation.path}): ${message}`);
88
+ };
89
+ if (!isPlainObject(operation) || typeof operation.path !== "string") fail("an operation is an object with op and path");
90
+ const tokens = parsePointer(operation.path, `path of operation ${index}`);
91
+ const fromTokens = (): string[] => {
92
+ if (typeof operation.from !== "string") fail(`the operation ${operation.op} needs from`);
93
+ return parsePointer(operation.from!, `from of operation ${index}`);
94
+ };
95
+ switch (operation.op) {
96
+ case "add":
97
+ document = add(document, tokens, valueOf(operation, fail), fail);
98
+ break;
99
+ case "remove":
100
+ document = remove(document, tokens, fail).document;
101
+ break;
102
+ case "replace":
103
+ document = replace(document, tokens, valueOf(operation, fail), fail);
104
+ break;
105
+ case "move": {
106
+ const from = fromTokens();
107
+ if (from.length === tokens.length && from.every((token, at) => token === tokens[at])) break;
108
+ if (from.length < tokens.length && from.every((token, at) => token === tokens[at])) fail("a value cannot move into itself");
109
+ const removed = remove(document, from, fail);
110
+ document = add(removed.document, tokens, removed.removed, fail);
111
+ break;
112
+ }
113
+ case "copy": {
114
+ const from = fromTokens();
115
+ const found = lookup(document, from);
116
+ if (!found.found) fail(`the location ${formatPointer(from)} does not exist`);
117
+ document = add(document, tokens, cloneJson((found as { value: JsonValue }).value), fail);
118
+ break;
119
+ }
120
+ case "test": {
121
+ const expected = valueOf(operation, fail);
122
+ const found = lookup(document, tokens);
123
+ if (!found.found) fail(`the location ${operation.path} does not exist`, "TEST_FAILED");
124
+ if (!jsonEqual((found as { value: JsonValue }).value, expected)) fail(`the value is not ${JSON.stringify(expected)}`, "TEST_FAILED");
125
+ break;
126
+ }
127
+ default:
128
+ fail(`the operation ${JSON.stringify((operation as { op: unknown }).op)} is not add, remove, replace, move, copy or test`);
129
+ }
130
+ });
131
+ return document;
132
+ }