ng-form-foundry-transformers 0.5.0 → 0.5.2
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/README.md +34 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +3 -1
- package/dist/core/infer.d.ts +3 -2
- package/dist/core/infer.js +7 -4
- package/dist/core/merge-inferred.d.ts +20 -0
- package/dist/core/merge-inferred.js +46 -0
- package/dist/core/schema-keys.d.ts +40 -0
- package/dist/core/schema-keys.js +67 -0
- package/dist/core/shape-check.d.ts +40 -0
- package/dist/core/shape-check.js +147 -0
- package/dist/transformers/json/index.d.ts +1 -0
- package/dist/transformers/json/index.js +5 -1
- package/dist/transformers/json/json-transformer.d.ts +35 -7
- package/dist/transformers/json/json-transformer.js +80 -11
- package/dist/transformers/libconfig/index.d.ts +1 -0
- package/dist/transformers/libconfig/index.js +5 -1
- package/dist/transformers/libconfig/libconfig-transformer.d.ts +28 -2
- package/dist/transformers/libconfig/libconfig-transformer.js +37 -4
- package/dist/transformers/libconfig/revert.d.ts +14 -2
- package/dist/transformers/libconfig/revert.js +34 -14
- package/dist/transformers/libconfig/schema.d.ts +25 -1
- package/dist/transformers/libconfig/schema.js +118 -0
- package/dist/transformers/yaml/index.d.ts +2 -1
- package/dist/transformers/yaml/index.js +5 -1
- package/dist/transformers/yaml/revert.d.ts +8 -1
- package/dist/transformers/yaml/revert.js +43 -11
- package/dist/transformers/yaml/yaml-transformer.d.ts +32 -5
- package/dist/transformers/yaml/yaml-transformer.js +59 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -155,6 +155,23 @@ enabled, so the output validates against the source schema (opt out with
|
|
|
155
155
|
`schemaOptions: { optionalPresence: false }`); a required property mapping to a
|
|
156
156
|
choice is marked `mandatory`.
|
|
157
157
|
|
|
158
|
+
**The schema must agree with the document on container shapes.** A covered
|
|
159
|
+
key whose schema says array where the document holds an object (or scalar
|
|
160
|
+
where it holds a collection) makes `toSchema` throw a `SchemaShapeError`
|
|
161
|
+
naming the path — such a form could not carry the section and saving would
|
|
162
|
+
erase it. Catch it to fall back to inferred editing. Scalar-vs-scalar
|
|
163
|
+
differences (a quoted `"0xe00"` under an `integer` schema) stay editable,
|
|
164
|
+
and an integral edit writes back an integer literal.
|
|
165
|
+
|
|
166
|
+
**The schema may cover any slice of the document.** Keys it does not mention
|
|
167
|
+
are governed by `unknownKeys` (YAML, JSON, and libconfig alike):
|
|
168
|
+
`'preserve'` (default) keeps them verbatim on save — a partial schema edits
|
|
169
|
+
its fields without erasing the rest of a real-world config; `'drop'` deletes
|
|
170
|
+
them — for an intentionally complete schema (sanitizing, template
|
|
171
|
+
enforcement); `'edit'` surfaces them as editable fields typed by the
|
|
172
|
+
document's own values, merged under the JSON Schema in document order, so
|
|
173
|
+
nothing in the file is invisible.
|
|
174
|
+
|
|
158
175
|
## JSON transformer
|
|
159
176
|
|
|
160
177
|
Same as YAML, for **JSON config files** — `jsonTransformer.toSchema(json, { schema? })`
|
|
@@ -292,6 +309,23 @@ npm test # node:test on the compiled output (no Python needed)
|
|
|
292
309
|
|
|
293
310
|
## Status
|
|
294
311
|
|
|
312
|
+
`0.5.2` — **shape safety and operator-friendly display**: a container-shape
|
|
313
|
+
disagreement between document and schema (array declared where the document
|
|
314
|
+
holds an object, or scalar where it holds a collection — inside choices
|
|
315
|
+
included) throws a `SchemaShapeError` naming the path instead of producing
|
|
316
|
+
an empty form whose save would erase the section; a type-changing integral
|
|
317
|
+
edit writes an integer literal, never a float; and schema-driven fields
|
|
318
|
+
display hex/octal/binary values in the base the document wrote them, in
|
|
319
|
+
every `unknownKeys` mode. Adversarially proof-tested before release.
|
|
320
|
+
|
|
321
|
+
`0.5.1` — **partial JSON Schemas edit safely**: the new `unknownKeys` option
|
|
322
|
+
(YAML, JSON, libconfig) governs keys the schema does not cover — `'preserve'`
|
|
323
|
+
(default) keeps them verbatim on save, `'drop'` deletes them for
|
|
324
|
+
intentionally complete schemas, `'edit'` surfaces them as editable fields
|
|
325
|
+
typed by the document's own values. The YAML binding is now
|
|
326
|
+
`YamlBinding { doc, schema? }` instead of the bare parsed document (bindings
|
|
327
|
+
are opaque; code that hands them straight back to `toSource` is unaffected).
|
|
328
|
+
|
|
295
329
|
`0.5.0` — the **libconfig transformer is stable**: the one-time beta warning
|
|
296
330
|
is gone (and with it the `resetLibconfigBetaWarning` helper), and the format's
|
|
297
331
|
guarantees and known limitations are documented in the transformers guide.
|
package/dist/core/index.d.ts
CHANGED
|
@@ -14,3 +14,4 @@ export type { NodeGroup, NodeType, Leaf, LeafList, NodeGroupList, Choice, Choice
|
|
|
14
14
|
export { CASE_KEY } from './schema';
|
|
15
15
|
export type { Thesaurus, ThesaurusEntry } from './schema';
|
|
16
16
|
export { applyThesaurus } from './thesaurus';
|
|
17
|
+
export { SchemaShapeError } from './shape-check';
|
package/dist/core/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* map a JSON Schema) that any data-oriented transformer reuses.
|
|
8
8
|
*/
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.applyThesaurus = exports.CASE_KEY = exports.jsonSchemaToNodeGroup = exports.inferNodeGroup = exports.TransformerRegistry = void 0;
|
|
10
|
+
exports.SchemaShapeError = exports.applyThesaurus = exports.CASE_KEY = exports.jsonSchemaToNodeGroup = exports.inferNodeGroup = exports.TransformerRegistry = void 0;
|
|
11
11
|
var registry_1 = require("./registry");
|
|
12
12
|
Object.defineProperty(exports, "TransformerRegistry", { enumerable: true, get: function () { return registry_1.TransformerRegistry; } });
|
|
13
13
|
// Shared schema builders — used by the YAML and JSON transformers alike.
|
|
@@ -19,3 +19,5 @@ var schema_1 = require("./schema");
|
|
|
19
19
|
Object.defineProperty(exports, "CASE_KEY", { enumerable: true, get: function () { return schema_1.CASE_KEY; } });
|
|
20
20
|
var thesaurus_1 = require("./thesaurus");
|
|
21
21
|
Object.defineProperty(exports, "applyThesaurus", { enumerable: true, get: function () { return thesaurus_1.applyThesaurus; } });
|
|
22
|
+
var shape_check_1 = require("./shape-check");
|
|
23
|
+
Object.defineProperty(exports, "SchemaShapeError", { enumerable: true, get: function () { return shape_check_1.SchemaShapeError; } });
|
package/dist/core/infer.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { NodeGroup } from './schema';
|
|
2
2
|
/**
|
|
3
|
-
* Infer a root {@link NodeGroup} from a parsed data object
|
|
4
|
-
*
|
|
3
|
+
* Infer a root {@link NodeGroup} from a parsed data object — the whole form
|
|
4
|
+
* when no JSON Schema is given, and the layer `mergeInferred` overlays under
|
|
5
|
+
* `unknownKeys: 'edit'`. Structure and types come from the values themselves:
|
|
5
6
|
*
|
|
6
7
|
* - object → nodeGroup (recursed)
|
|
7
8
|
* - array of objects → nodeGroupList (item schema = union of the items' keys)
|
package/dist/core/infer.js
CHANGED
|
@@ -3,8 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.inferNodeGroup = inferNodeGroup;
|
|
4
4
|
const ROOT = '__root__';
|
|
5
5
|
/**
|
|
6
|
-
* Infer a root {@link NodeGroup} from a parsed data object
|
|
7
|
-
*
|
|
6
|
+
* Infer a root {@link NodeGroup} from a parsed data object — the whole form
|
|
7
|
+
* when no JSON Schema is given, and the layer `mergeInferred` overlays under
|
|
8
|
+
* `unknownKeys: 'edit'`. Structure and types come from the values themselves:
|
|
8
9
|
*
|
|
9
10
|
* - object → nodeGroup (recursed)
|
|
10
11
|
* - array of objects → nodeGroupList (item schema = union of the items' keys)
|
|
@@ -21,7 +22,9 @@ function inferNodeGroup(data, name = ROOT) {
|
|
|
21
22
|
return group;
|
|
22
23
|
}
|
|
23
24
|
function objectToNodeGroup(data, name) {
|
|
24
|
-
|
|
25
|
+
// Null prototype: data keys are arbitrary, and assigning a `__proto__` key
|
|
26
|
+
// onto a plain object would silently set the record's prototype instead.
|
|
27
|
+
const children = Object.create(null);
|
|
25
28
|
for (const [key, value] of Object.entries(data)) {
|
|
26
29
|
children[key] = inferNode(key, value);
|
|
27
30
|
}
|
|
@@ -45,7 +48,7 @@ function inferArray(name, items) {
|
|
|
45
48
|
}
|
|
46
49
|
/** A representative object with every key seen across `items`, each mapped to the first non-null sample. */
|
|
47
50
|
function unionKeys(items) {
|
|
48
|
-
const merged =
|
|
51
|
+
const merged = Object.create(null);
|
|
49
52
|
for (const item of items) {
|
|
50
53
|
for (const [key, value] of Object.entries(item)) {
|
|
51
54
|
if (!(key in merged) || (merged[key] == null && value != null))
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema union for `unknownKeys: 'edit'`: overlay a user-supplied JSON
|
|
3
|
+
* Schema's {@link NodeGroup} onto the schema *inferred from the document
|
|
4
|
+
* itself*, so covered keys keep their typed, validated, labeled nodes while
|
|
5
|
+
* every key the JSON Schema does not mention still renders — editable, typed
|
|
6
|
+
* by inference (see `core/infer.ts` and the per-format inference in the
|
|
7
|
+
* transformers). Counterpart of `core/schema-keys.ts`, which handles the
|
|
8
|
+
* *preserve-invisibly* answer to the same question.
|
|
9
|
+
*/
|
|
10
|
+
import type { NodeGroup } from './schema';
|
|
11
|
+
/**
|
|
12
|
+
* Merge per key, recursively. Document order wins for placement: the
|
|
13
|
+
* inferred children mirror the source document, so present keys render in
|
|
14
|
+
* file order with schema-only keys (e.g. presence-able additions) appended
|
|
15
|
+
* after, in schema order. Where both sides describe a key, the schema wins —
|
|
16
|
+
* except that groups and group-lists merge structurally, so a partially
|
|
17
|
+
* covered subtree keeps its uncovered fields at any depth. Group metadata
|
|
18
|
+
* (name, label, presence, bounds) comes from the schema side.
|
|
19
|
+
*/
|
|
20
|
+
export declare function mergeInferred(schema: NodeGroup, inferred: NodeGroup): NodeGroup;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mergeInferred = mergeInferred;
|
|
4
|
+
/**
|
|
5
|
+
* Merge per key, recursively. Document order wins for placement: the
|
|
6
|
+
* inferred children mirror the source document, so present keys render in
|
|
7
|
+
* file order with schema-only keys (e.g. presence-able additions) appended
|
|
8
|
+
* after, in schema order. Where both sides describe a key, the schema wins —
|
|
9
|
+
* except that groups and group-lists merge structurally, so a partially
|
|
10
|
+
* covered subtree keeps its uncovered fields at any depth. Group metadata
|
|
11
|
+
* (name, label, presence, bounds) comes from the schema side.
|
|
12
|
+
*/
|
|
13
|
+
function mergeInferred(schema, inferred) {
|
|
14
|
+
const children = Object.create(null);
|
|
15
|
+
for (const [name, inferredChild] of Object.entries(inferred.children)) {
|
|
16
|
+
const schemaChild = schema.children[name];
|
|
17
|
+
children[name] = schemaChild ? mergeNode(schemaChild, inferredChild) : inferredChild;
|
|
18
|
+
}
|
|
19
|
+
for (const [name, schemaChild] of Object.entries(schema.children)) {
|
|
20
|
+
if (!(name in children))
|
|
21
|
+
children[name] = schemaChild;
|
|
22
|
+
}
|
|
23
|
+
return { ...schema, children };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* One key described by both sides: groups and group-lists merge recursively;
|
|
27
|
+
* everything else — leaves, choices, maps, and shape disagreements — takes
|
|
28
|
+
* the schema node, which is authoritative wherever it speaks. One inferred
|
|
29
|
+
* fact does carry over: a leaf's `radix` display hint, which only the
|
|
30
|
+
* document knows (JSON Schema has no radix vocabulary).
|
|
31
|
+
*/
|
|
32
|
+
function mergeNode(schema, inferred) {
|
|
33
|
+
if (schema.kind === 'nodeGroup' && inferred.kind === 'nodeGroup') {
|
|
34
|
+
return mergeInferred(schema, inferred);
|
|
35
|
+
}
|
|
36
|
+
if (schema.kind === 'nodeGroupList' && inferred.kind === 'nodeGroupList') {
|
|
37
|
+
return { ...schema, type: mergeInferred(schema.type, inferred.type) };
|
|
38
|
+
}
|
|
39
|
+
if ((schema.kind === 'leaf' || schema.kind === 'leafList') &&
|
|
40
|
+
schema.kind === inferred.kind &&
|
|
41
|
+
inferred.radix &&
|
|
42
|
+
!schema.radix) {
|
|
43
|
+
return { ...schema, radix: inferred.radix };
|
|
44
|
+
}
|
|
45
|
+
return schema;
|
|
46
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema-born key resolution for partial-schema reverts.
|
|
3
|
+
*
|
|
4
|
+
* A transformer given a JSON Schema builds its form from the produced
|
|
5
|
+
* {@link NodeGroup} — so `serializeForm` can only ever emit keys that schema
|
|
6
|
+
* describes. When the edited value is written back onto the original
|
|
7
|
+
* document, the value is authoritative for those **schema-born** paths only;
|
|
8
|
+
* every other key was never carried by the form and must be preserved.
|
|
9
|
+
* These helpers answer "which keys can the value legitimately speak for?" at
|
|
10
|
+
* each document level, mirroring the wire encoding of
|
|
11
|
+
* ng-form-foundry's `serializeForm`/`toWireValue`.
|
|
12
|
+
*/
|
|
13
|
+
import type { Choice, NodeGroup, NodeType } from './schema';
|
|
14
|
+
/**
|
|
15
|
+
* The schema-born key set of one document level, as `name → child schema`
|
|
16
|
+
* for recursion. `undefined` means "no schema in play": every key is
|
|
17
|
+
* value-authoritative. A key absent from a defined map is not schema-born
|
|
18
|
+
* and must be preserved verbatim.
|
|
19
|
+
*/
|
|
20
|
+
export type SchemaKeys = Map<string, NodeType | undefined> | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* The keys `serializeForm` can emit for a schema node standing at a document
|
|
23
|
+
* object/group, with each key's schema for recursion. A nodeGroup
|
|
24
|
+
* contributes its children by key — a choice child included, since the wire
|
|
25
|
+
* value keeps a choice at its own key (`toWireValue` strips only the case
|
|
26
|
+
* discriminator). Standing at a choice, the keys are the union of every
|
|
27
|
+
* case's fields: the form only emits the active case, so the other cases'
|
|
28
|
+
* keys behave like any non-emitted schema key. A map is open-keyed — every
|
|
29
|
+
* key is schema-born and entries recurse with the map's value schema.
|
|
30
|
+
*/
|
|
31
|
+
export declare function childrenOf(node: NodeType): SchemaKeys;
|
|
32
|
+
/** The per-item schema when `node` describes a list of groups, else undefined. */
|
|
33
|
+
export declare function itemSchemaOf(node: NodeType | undefined): NodeGroup | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* A case body is a single node exactly when its `kind` is a *string* — a
|
|
36
|
+
* record body whose fields include one literally named `kind` (k8s-style
|
|
37
|
+
* schemas) holds a NodeType there, not a string. The `in` operator cannot
|
|
38
|
+
* tell the two apart.
|
|
39
|
+
*/
|
|
40
|
+
export declare function isSingleNodeBody(body: Choice['cases'][string]): body is NodeType;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.childrenOf = childrenOf;
|
|
4
|
+
exports.itemSchemaOf = itemSchemaOf;
|
|
5
|
+
exports.isSingleNodeBody = isSingleNodeBody;
|
|
6
|
+
/**
|
|
7
|
+
* The keys `serializeForm` can emit for a schema node standing at a document
|
|
8
|
+
* object/group, with each key's schema for recursion. A nodeGroup
|
|
9
|
+
* contributes its children by key — a choice child included, since the wire
|
|
10
|
+
* value keeps a choice at its own key (`toWireValue` strips only the case
|
|
11
|
+
* discriminator). Standing at a choice, the keys are the union of every
|
|
12
|
+
* case's fields: the form only emits the active case, so the other cases'
|
|
13
|
+
* keys behave like any non-emitted schema key. A map is open-keyed — every
|
|
14
|
+
* key is schema-born and entries recurse with the map's value schema.
|
|
15
|
+
*/
|
|
16
|
+
function childrenOf(node) {
|
|
17
|
+
switch (node.kind) {
|
|
18
|
+
case 'nodeGroup':
|
|
19
|
+
return new Map(Object.entries(node.children));
|
|
20
|
+
case 'choice': {
|
|
21
|
+
const out = new Map();
|
|
22
|
+
for (const [name, field] of caseEntries(node))
|
|
23
|
+
out.set(name, field);
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
case 'map':
|
|
27
|
+
return new OpenKeys(node.value);
|
|
28
|
+
default:
|
|
29
|
+
return undefined; // leaf/leafList/list shapes carry no key semantics
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** The per-item schema when `node` describes a list of groups, else undefined. */
|
|
33
|
+
function itemSchemaOf(node) {
|
|
34
|
+
return node?.kind === 'nodeGroupList' ? node.type : undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A case body is a single node exactly when its `kind` is a *string* — a
|
|
38
|
+
* record body whose fields include one literally named `kind` (k8s-style
|
|
39
|
+
* schemas) holds a NodeType there, not a string. The `in` operator cannot
|
|
40
|
+
* tell the two apart.
|
|
41
|
+
*/
|
|
42
|
+
function isSingleNodeBody(body) {
|
|
43
|
+
return typeof body.kind === 'string';
|
|
44
|
+
}
|
|
45
|
+
/** Field name → schema across every case of a choice (leaf-bodied cases too). */
|
|
46
|
+
function* caseEntries(choice) {
|
|
47
|
+
for (const body of Object.values(choice.cases)) {
|
|
48
|
+
if (isSingleNodeBody(body))
|
|
49
|
+
yield [body.name, body];
|
|
50
|
+
else
|
|
51
|
+
for (const [name, field] of Object.entries(body))
|
|
52
|
+
yield [name, field];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** A `SchemaKeys` map matching every key — a map node's open dictionary. */
|
|
56
|
+
class OpenKeys extends Map {
|
|
57
|
+
constructor(valueSchema) {
|
|
58
|
+
super();
|
|
59
|
+
this.valueSchema = valueSchema;
|
|
60
|
+
}
|
|
61
|
+
has(_key) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
get(_key) {
|
|
65
|
+
return this.valueSchema;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Container-shape validation between a parsed document and a user-supplied
|
|
3
|
+
* JSON Schema's {@link NodeGroup}.
|
|
4
|
+
*
|
|
5
|
+
* A schema that declares a section a list while the document holds a group
|
|
6
|
+
* (or scalar, or vice versa) produces a form that cannot carry the section's
|
|
7
|
+
* contents — the form builder coerces the unusable initial value to an empty
|
|
8
|
+
* control, the form looks valid, and a save erases the section. For config
|
|
9
|
+
* files that program live systems, that silent path is the worst failure
|
|
10
|
+
* mode, so schema-driven `toSchema` refuses up front: the consumer catches
|
|
11
|
+
* {@link SchemaShapeError} and falls back to inferred editing or fixes the
|
|
12
|
+
* schema. Scalar-vs-scalar differences are *not* errors — a quoted `"0xe00"`
|
|
13
|
+
* under an `integer` schema is a legitimate wild-file shape the form can
|
|
14
|
+
* carry and edit.
|
|
15
|
+
*/
|
|
16
|
+
import type { NodeGroup } from './schema';
|
|
17
|
+
/** A document/schema container-shape disagreement, pointing at the key path. */
|
|
18
|
+
export declare class SchemaShapeError extends Error {
|
|
19
|
+
/** Path of the offending key, e.g. `gNBs` or `cells[0].id`. */
|
|
20
|
+
readonly path: string;
|
|
21
|
+
/** What the document holds there. */
|
|
22
|
+
readonly found: 'object' | 'array' | 'scalar';
|
|
23
|
+
/** What the schema declares there. */
|
|
24
|
+
readonly expected: 'object' | 'array' | 'scalar';
|
|
25
|
+
constructor(
|
|
26
|
+
/** Path of the offending key, e.g. `gNBs` or `cells[0].id`. */
|
|
27
|
+
path: string,
|
|
28
|
+
/** What the document holds there. */
|
|
29
|
+
found: 'object' | 'array' | 'scalar',
|
|
30
|
+
/** What the schema declares there. */
|
|
31
|
+
expected: 'object' | 'array' | 'scalar');
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Walk the extracted document value against the JSON-Schema-derived schema
|
|
35
|
+
* and throw {@link SchemaShapeError} on the first container-kind
|
|
36
|
+
* disagreement. Only schema-covered keys are checked (absent keys are
|
|
37
|
+
* presence, uncovered keys are inference's business, and inference always
|
|
38
|
+
* matches the document by construction).
|
|
39
|
+
*/
|
|
40
|
+
export declare function assertSchemaShapes(data: unknown, schema: NodeGroup): void;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SchemaShapeError = void 0;
|
|
4
|
+
exports.assertSchemaShapes = assertSchemaShapes;
|
|
5
|
+
const schema_keys_1 = require("./schema-keys");
|
|
6
|
+
/** A document/schema container-shape disagreement, pointing at the key path. */
|
|
7
|
+
class SchemaShapeError extends Error {
|
|
8
|
+
constructor(
|
|
9
|
+
/** Path of the offending key, e.g. `gNBs` or `cells[0].id`. */
|
|
10
|
+
path,
|
|
11
|
+
/** What the document holds there. */
|
|
12
|
+
found,
|
|
13
|
+
/** What the schema declares there. */
|
|
14
|
+
expected) {
|
|
15
|
+
super(`'${path}': the document holds ${article(found)} here, but the schema expects ` +
|
|
16
|
+
`${article(expected)} — the form cannot carry its contents, and saving would erase them. ` +
|
|
17
|
+
`Align the schema with the document, or leave this key out of the schema.`);
|
|
18
|
+
this.path = path;
|
|
19
|
+
this.found = found;
|
|
20
|
+
this.expected = expected;
|
|
21
|
+
this.name = 'SchemaShapeError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.SchemaShapeError = SchemaShapeError;
|
|
25
|
+
function article(shape) {
|
|
26
|
+
return shape === 'array' ? 'an array' : shape === 'object' ? 'an object' : 'a scalar';
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Walk the extracted document value against the JSON-Schema-derived schema
|
|
30
|
+
* and throw {@link SchemaShapeError} on the first container-kind
|
|
31
|
+
* disagreement. Only schema-covered keys are checked (absent keys are
|
|
32
|
+
* presence, uncovered keys are inference's business, and inference always
|
|
33
|
+
* matches the document by construction).
|
|
34
|
+
*/
|
|
35
|
+
function assertSchemaShapes(data, schema) {
|
|
36
|
+
checkNode(data, schema, '');
|
|
37
|
+
}
|
|
38
|
+
function checkNode(value, node, path) {
|
|
39
|
+
// Absent is presence semantics; null is either a nullable leaf's value or
|
|
40
|
+
// the empty-container idiom (YAML `section:`) — nothing exists to erase.
|
|
41
|
+
if (value == null)
|
|
42
|
+
return;
|
|
43
|
+
switch (node.kind) {
|
|
44
|
+
case 'leaf':
|
|
45
|
+
// Any primitive is carryable (string carries, quoted ints). An *empty*
|
|
46
|
+
// collection is too: the libconfig empty-collection carry under a
|
|
47
|
+
// string leaf round-trips as a no-op — only content can be erased.
|
|
48
|
+
if (isRecord(value))
|
|
49
|
+
throw new SchemaShapeError(path, 'object', 'scalar');
|
|
50
|
+
if (Array.isArray(value) && value.length > 0)
|
|
51
|
+
throw new SchemaShapeError(path, 'array', 'scalar');
|
|
52
|
+
return;
|
|
53
|
+
case 'leafList':
|
|
54
|
+
if (!Array.isArray(value))
|
|
55
|
+
throw new SchemaShapeError(path, shapeOf(value), 'array');
|
|
56
|
+
value.forEach((item, i) => {
|
|
57
|
+
if (isRecord(item) || Array.isArray(item)) {
|
|
58
|
+
throw new SchemaShapeError(`${path}[${i}]`, shapeOf(item), 'scalar');
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
case 'nodeGroupList':
|
|
63
|
+
if (!Array.isArray(value))
|
|
64
|
+
throw new SchemaShapeError(path, shapeOf(value), 'array');
|
|
65
|
+
value.forEach((item, i) => {
|
|
66
|
+
if (!isRecord(item))
|
|
67
|
+
throw new SchemaShapeError(`${path}[${i}]`, shapeOf(item), 'object');
|
|
68
|
+
checkChildren(item, node.type, `${path}[${i}]`);
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
case 'nodeGroup':
|
|
72
|
+
case 'map':
|
|
73
|
+
if (!isRecord(value))
|
|
74
|
+
throw new SchemaShapeError(path, shapeOf(value), 'object');
|
|
75
|
+
checkChildren(value, node, path);
|
|
76
|
+
return;
|
|
77
|
+
case 'choice': {
|
|
78
|
+
// The document's shape must be one some case can carry: a record for
|
|
79
|
+
// record-bodied cases, an array for a collection-bodied case, a scalar
|
|
80
|
+
// for a leaf-bodied case. An array under object-only cases (or a
|
|
81
|
+
// record under scalar-only cases) is exactly the uncarryable-section
|
|
82
|
+
// erasure this check exists to refuse.
|
|
83
|
+
const allowed = caseShapes(node);
|
|
84
|
+
const found = shapeOf(value);
|
|
85
|
+
if (!allowed.has(found)) {
|
|
86
|
+
throw new SchemaShapeError(path, found, preferredShape(allowed));
|
|
87
|
+
}
|
|
88
|
+
if (isRecord(value))
|
|
89
|
+
checkChildren(value, node, path);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** The container shapes a choice's cases can carry, unioned across cases. */
|
|
95
|
+
function caseShapes(choice) {
|
|
96
|
+
const shapes = new Set();
|
|
97
|
+
for (const body of Object.values(choice.cases)) {
|
|
98
|
+
if (!(0, schema_keys_1.isSingleNodeBody)(body)) {
|
|
99
|
+
shapes.add('object'); // a record of named fields
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
switch (body.kind) {
|
|
103
|
+
case 'nodeGroup':
|
|
104
|
+
case 'map':
|
|
105
|
+
shapes.add('object');
|
|
106
|
+
break;
|
|
107
|
+
case 'nodeGroupList':
|
|
108
|
+
case 'leafList':
|
|
109
|
+
shapes.add('array');
|
|
110
|
+
break;
|
|
111
|
+
case 'leaf':
|
|
112
|
+
shapes.add('scalar');
|
|
113
|
+
break;
|
|
114
|
+
case 'choice': // a nested choice's own cases are unknowable here: permissive
|
|
115
|
+
shapes.add('object').add('array').add('scalar');
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return shapes;
|
|
120
|
+
}
|
|
121
|
+
/** A single representative shape for the error message, objects first. */
|
|
122
|
+
function preferredShape(allowed) {
|
|
123
|
+
if (allowed.has('object'))
|
|
124
|
+
return 'object';
|
|
125
|
+
if (allowed.has('array'))
|
|
126
|
+
return 'array';
|
|
127
|
+
return 'scalar';
|
|
128
|
+
}
|
|
129
|
+
/** Recurse a record's own keys through the node's schema-born children. */
|
|
130
|
+
function checkChildren(record, node, path) {
|
|
131
|
+
const keys = (0, schema_keys_1.childrenOf)(node);
|
|
132
|
+
if (!keys)
|
|
133
|
+
return;
|
|
134
|
+
for (const key of Object.keys(record)) {
|
|
135
|
+
const child = keys.get(key);
|
|
136
|
+
if (child)
|
|
137
|
+
checkNode(record[key], child, path ? `${path}.${key}` : key);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function shapeOf(value) {
|
|
141
|
+
if (Array.isArray(value))
|
|
142
|
+
return 'array';
|
|
143
|
+
return isRecord(value) ? 'object' : 'scalar';
|
|
144
|
+
}
|
|
145
|
+
function isRecord(v) {
|
|
146
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
147
|
+
}
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* schema builders in `core`.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.jsonTransformer = void 0;
|
|
9
|
+
exports.SchemaShapeError = exports.jsonTransformer = void 0;
|
|
10
10
|
var json_transformer_1 = require("./json-transformer");
|
|
11
11
|
Object.defineProperty(exports, "jsonTransformer", { enumerable: true, get: function () { return json_transformer_1.jsonTransformer; } });
|
|
12
|
+
// Thrown by schema-driven toSchema; re-exported so subpath consumers can
|
|
13
|
+
// catch it by instance.
|
|
14
|
+
var shape_check_1 = require("../../core/shape-check");
|
|
15
|
+
Object.defineProperty(exports, "SchemaShapeError", { enumerable: true, get: function () { return shape_check_1.SchemaShapeError; } });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FormValue, Thesaurus } from '../../core/schema';
|
|
1
|
+
import type { FormValue, NodeGroup, Thesaurus } from '../../core/schema';
|
|
2
2
|
import type { TransformResult } from '../../core/transformer';
|
|
3
3
|
import { type JsonSchema, type JsonSchemaOptions } from '../../core/json-schema';
|
|
4
4
|
/** Options for {@link jsonTransformer}'s `toSchema`. */
|
|
@@ -19,6 +19,18 @@ export interface JsonOptions {
|
|
|
19
19
|
* case-insensitively; never paths.
|
|
20
20
|
*/
|
|
21
21
|
thesaurus?: Thesaurus;
|
|
22
|
+
/**
|
|
23
|
+
* Schema-driven mode only: what happens to keys the JSON Schema does not
|
|
24
|
+
* cover. `'preserve'` (default) keeps them — the form never carried them,
|
|
25
|
+
* so a partial schema edits its slice without erasing the rest. `'drop'`
|
|
26
|
+
* makes the edited value authoritative for the whole document, deleting
|
|
27
|
+
* uncovered keys — for consumers whose schema is intentionally complete.
|
|
28
|
+
* `'edit'` surfaces them instead: uncovered keys render as editable fields
|
|
29
|
+
* typed by the data (the inferred schema merged under the JSON Schema), so
|
|
30
|
+
* nothing is invisible and the value covers the whole document. Ignored
|
|
31
|
+
* without a `schema`.
|
|
32
|
+
*/
|
|
33
|
+
unknownKeys?: 'preserve' | 'drop' | 'edit';
|
|
22
34
|
}
|
|
23
35
|
/** How the source was formatted, so `toSource` re-emits it the same way. */
|
|
24
36
|
export interface JsonFormat {
|
|
@@ -32,6 +44,19 @@ export interface JsonFormat {
|
|
|
32
44
|
* them back as unquoted numbers so precision survives the round-trip.
|
|
33
45
|
*/
|
|
34
46
|
bigInts: string[];
|
|
47
|
+
/**
|
|
48
|
+
* Present in schema-driven mode with `unknownKeys: 'preserve'` (the
|
|
49
|
+
* default) or `'edit'`: the original parsed data and the NodeGroup the
|
|
50
|
+
* form was built from (the JSON Schema's own under `'preserve'`, the
|
|
51
|
+
* inferred-merged one under `'edit'`). `toSource` then treats the value as
|
|
52
|
+
* authoritative for schema-born paths only and merges it over the
|
|
53
|
+
* original, so uncovered keys survive in their original key order — under
|
|
54
|
+
* `'edit'` that protection is left covering only the keys no form field
|
|
55
|
+
* can carry (e.g. a key inside a covered choice's object that no case
|
|
56
|
+
* names).
|
|
57
|
+
*/
|
|
58
|
+
original?: FormValue;
|
|
59
|
+
schema?: NodeGroup;
|
|
35
60
|
}
|
|
36
61
|
/**
|
|
37
62
|
* A JSON {@link Transformer}: turn a JSON config document into a form and write
|
|
@@ -39,12 +64,15 @@ export interface JsonFormat {
|
|
|
39
64
|
* `core` builders the YAML transformer uses — only parsing (`JSON.parse`) and
|
|
40
65
|
* serialization (`JSON.stringify`) are JSON-specific.
|
|
41
66
|
*
|
|
42
|
-
* Standard JSON has no comments, so revert re-serializes the
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
67
|
+
* Standard JSON has no comments, so revert re-serializes, preserving the
|
|
68
|
+
* source's indent width and trailing newline. In schema-driven mode with
|
|
69
|
+
* `unknownKeys: 'preserve'` or `'edit'` the value is merged over the original
|
|
70
|
+
* data at schema-born paths ({@link mergeSchemaBorn}), so uncovered keys and
|
|
71
|
+
* the original key order survive; otherwise the edited value is serialized
|
|
72
|
+
* as-is, key order following the form value. For JSON **with comments**
|
|
73
|
+
* (JSONC), parse and edit it as YAML with
|
|
74
|
+
* {@link import('../yaml/yaml-transformer').yamlTransformer} instead —
|
|
75
|
+
* `JSON.parse` rejects comments.
|
|
48
76
|
*
|
|
49
77
|
* Integers beyond 2^53 can't survive a `number` round-trip, so they are carried
|
|
50
78
|
* as strings in the form value and re-emitted verbatim as unquoted numbers (the
|