ng-form-foundry-transformers 0.5.1 → 0.5.3
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 +21 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +3 -1
- package/dist/core/schema-keys.d.ts +8 -1
- package/dist/core/schema-keys.js +11 -1
- 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.js +5 -0
- package/dist/transformers/libconfig/index.d.ts +1 -0
- package/dist/transformers/libconfig/index.js +5 -1
- package/dist/transformers/libconfig/libconfig-transformer.js +10 -0
- package/dist/transformers/libconfig/revert.js +4 -1
- package/dist/transformers/libconfig/schema.d.ts +14 -1
- package/dist/transformers/libconfig/schema.js +58 -0
- package/dist/transformers/yaml/index.d.ts +1 -0
- package/dist/transformers/yaml/index.js +5 -1
- package/dist/transformers/yaml/yaml-transformer.js +36 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -155,6 +155,14 @@ 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
|
+
|
|
158
166
|
**The schema may cover any slice of the document.** Keys it does not mention
|
|
159
167
|
are governed by `unknownKeys` (YAML, JSON, and libconfig alike):
|
|
160
168
|
`'preserve'` (default) keeps them verbatim on save — a partial schema edits
|
|
@@ -301,6 +309,19 @@ npm test # node:test on the compiled output (no Python needed)
|
|
|
301
309
|
|
|
302
310
|
## Status
|
|
303
311
|
|
|
312
|
+
`0.5.3` — version-alignment release with the workspace (the paired
|
|
313
|
+
`ng-form-foundry` 0.5.3 ships hover-revealed field actions and indexed
|
|
314
|
+
group-list add labels); no transformer changes.
|
|
315
|
+
|
|
316
|
+
`0.5.2` — **shape safety and operator-friendly display**: a container-shape
|
|
317
|
+
disagreement between document and schema (array declared where the document
|
|
318
|
+
holds an object, or scalar where it holds a collection — inside choices
|
|
319
|
+
included) throws a `SchemaShapeError` naming the path instead of producing
|
|
320
|
+
an empty form whose save would erase the section; a type-changing integral
|
|
321
|
+
edit writes an integer literal, never a float; and schema-driven fields
|
|
322
|
+
display hex/octal/binary values in the base the document wrote them, in
|
|
323
|
+
every `unknownKeys` mode. Adversarially proof-tested before release.
|
|
324
|
+
|
|
304
325
|
`0.5.1` — **partial JSON Schemas edit safely**: the new `unknownKeys` option
|
|
305
326
|
(YAML, JSON, libconfig) governs keys the schema does not cover — `'preserve'`
|
|
306
327
|
(default) keeps them verbatim on save, `'drop'` deletes them for
|
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; } });
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* each document level, mirroring the wire encoding of
|
|
11
11
|
* ng-form-foundry's `serializeForm`/`toWireValue`.
|
|
12
12
|
*/
|
|
13
|
-
import type { NodeGroup, NodeType } from './schema';
|
|
13
|
+
import type { Choice, NodeGroup, NodeType } from './schema';
|
|
14
14
|
/**
|
|
15
15
|
* The schema-born key set of one document level, as `name → child schema`
|
|
16
16
|
* for recursion. `undefined` means "no schema in play": every key is
|
|
@@ -31,3 +31,10 @@ export type SchemaKeys = Map<string, NodeType | undefined> | undefined;
|
|
|
31
31
|
export declare function childrenOf(node: NodeType): SchemaKeys;
|
|
32
32
|
/** The per-item schema when `node` describes a list of groups, else undefined. */
|
|
33
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;
|
package/dist/core/schema-keys.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.childrenOf = childrenOf;
|
|
4
4
|
exports.itemSchemaOf = itemSchemaOf;
|
|
5
|
+
exports.isSingleNodeBody = isSingleNodeBody;
|
|
5
6
|
/**
|
|
6
7
|
* The keys `serializeForm` can emit for a schema node standing at a document
|
|
7
8
|
* object/group, with each key's schema for recursion. A nodeGroup
|
|
@@ -32,10 +33,19 @@ function childrenOf(node) {
|
|
|
32
33
|
function itemSchemaOf(node) {
|
|
33
34
|
return node?.kind === 'nodeGroupList' ? node.type : undefined;
|
|
34
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
|
+
}
|
|
35
45
|
/** Field name → schema across every case of a choice (leaf-bodied cases too). */
|
|
36
46
|
function* caseEntries(choice) {
|
|
37
47
|
for (const body of Object.values(choice.cases)) {
|
|
38
|
-
if (
|
|
48
|
+
if (isSingleNodeBody(body))
|
|
39
49
|
yield [body.name, body];
|
|
40
50
|
else
|
|
41
51
|
for (const [name, field] of Object.entries(body))
|
|
@@ -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; } });
|
|
@@ -7,6 +7,7 @@ const json_schema_1 = require("../../core/json-schema");
|
|
|
7
7
|
const bigint_1 = require("../../core/bigint");
|
|
8
8
|
const schema_keys_1 = require("../../core/schema-keys");
|
|
9
9
|
const merge_inferred_1 = require("../../core/merge-inferred");
|
|
10
|
+
const shape_check_1 = require("../../core/shape-check");
|
|
10
11
|
/**
|
|
11
12
|
* A JSON {@link Transformer}: turn a JSON config document into a form and write
|
|
12
13
|
* the edited value back to JSON. The form is built by the same format-agnostic
|
|
@@ -40,6 +41,10 @@ exports.jsonTransformer = {
|
|
|
40
41
|
const schema = fromJsonSchema && unknownKeys === 'edit'
|
|
41
42
|
? (0, merge_inferred_1.mergeInferred)(fromJsonSchema, (0, infer_1.inferNodeGroup)(data, options?.rootName))
|
|
42
43
|
: fromJsonSchema ?? (0, infer_1.inferNodeGroup)(data, options?.rootName);
|
|
44
|
+
// A container-shape disagreement between document and schema would erase
|
|
45
|
+
// the section on save: refuse up front (see assertSchemaShapes).
|
|
46
|
+
if (fromJsonSchema)
|
|
47
|
+
(0, shape_check_1.assertSchemaShapes)(data, fromJsonSchema);
|
|
43
48
|
const labeled = options?.thesaurus ? (0, thesaurus_1.applyThesaurus)(schema, options.thesaurus) : schema;
|
|
44
49
|
// 'preserve' gates the revert on the JSON Schema's NodeGroup; 'edit' on
|
|
45
50
|
// the merged one (schema-born ≈ everything, but keys no form field can
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LibconfigParseError = exports.parseLibconfig = exports.libconfigTransformer = void 0;
|
|
3
|
+
exports.SchemaShapeError = exports.LibconfigParseError = exports.parseLibconfig = exports.libconfigTransformer = void 0;
|
|
4
4
|
var libconfig_transformer_1 = require("./libconfig-transformer");
|
|
5
5
|
Object.defineProperty(exports, "libconfigTransformer", { enumerable: true, get: function () { return libconfig_transformer_1.libconfigTransformer; } });
|
|
6
6
|
var parser_1 = require("./parser");
|
|
7
7
|
Object.defineProperty(exports, "parseLibconfig", { enumerable: true, get: function () { return parser_1.parseLibconfig; } });
|
|
8
8
|
Object.defineProperty(exports, "LibconfigParseError", { enumerable: true, get: function () { return parser_1.LibconfigParseError; } });
|
|
9
|
+
// Thrown by schema-driven toSchema; re-exported so browser consumers of this
|
|
10
|
+
// subpath can catch it by instance.
|
|
11
|
+
var shape_check_1 = require("../../core/shape-check");
|
|
12
|
+
Object.defineProperty(exports, "SchemaShapeError", { enumerable: true, get: function () { return shape_check_1.SchemaShapeError; } });
|
|
@@ -5,6 +5,7 @@ const thesaurus_1 = require("../../core/thesaurus");
|
|
|
5
5
|
const json_schema_1 = require("../../core/json-schema");
|
|
6
6
|
const merge_inferred_1 = require("../../core/merge-inferred");
|
|
7
7
|
const schema_keys_1 = require("../../core/schema-keys");
|
|
8
|
+
const shape_check_1 = require("../../core/shape-check");
|
|
8
9
|
const parser_1 = require("./parser");
|
|
9
10
|
const schema_1 = require("./schema");
|
|
10
11
|
const revert_1 = require("./revert");
|
|
@@ -19,8 +20,17 @@ exports.libconfigTransformer = {
|
|
|
19
20
|
const schema = fromJsonSchema && unknownKeys === 'edit'
|
|
20
21
|
? (0, merge_inferred_1.mergeInferred)(fromJsonSchema, (0, schema_1.libconfigToNodeGroup)(root, source, options?.rootName ?? '__root__'))
|
|
21
22
|
: fromJsonSchema ?? (0, schema_1.libconfigToNodeGroup)(root, source, options?.rootName ?? '__root__');
|
|
23
|
+
// Schema-driven leaves display in the base the document wrote them in —
|
|
24
|
+
// the JSON Schema cannot know it, the document does (see annotateSchemaRadix).
|
|
25
|
+
if (fromJsonSchema)
|
|
26
|
+
(0, schema_1.annotateSchemaRadix)(root, schema);
|
|
22
27
|
const labeled = options?.thesaurus ? (0, thesaurus_1.applyThesaurus)(schema, options.thesaurus) : schema;
|
|
23
28
|
const initialValue = (0, schema_1.extractValue)(root, source, options?.schema != null);
|
|
29
|
+
// A container-shape disagreement (group vs list vs scalar) produces a
|
|
30
|
+
// form that cannot carry the section and would erase it on save: refuse
|
|
31
|
+
// up front so the consumer can fall back to inferred editing.
|
|
32
|
+
if (fromJsonSchema)
|
|
33
|
+
(0, shape_check_1.assertSchemaShapes)(initialValue, fromJsonSchema);
|
|
24
34
|
if (fromJsonSchema && unknownKeys === 'edit') {
|
|
25
35
|
// Uncovered empty collections merged as read-only carries: their value
|
|
26
36
|
// must be the verbatim slice, not the typed [] the extraction produced.
|
|
@@ -197,7 +197,10 @@ function emitScalar(value, node) {
|
|
|
197
197
|
if ((node.type === 'int' || node.type === 'int64') && Number.isInteger(value)) {
|
|
198
198
|
return intLiteral(value, node.int);
|
|
199
199
|
}
|
|
200
|
-
|
|
200
|
+
// A type-changing edit (e.g. a quoted "0xe00" slot edited to a number
|
|
201
|
+
// under an integer schema): integral values emit an int literal —
|
|
202
|
+
// float form is reserved for genuinely fractional values.
|
|
203
|
+
return Number.isInteger(value) ? String(value) : floatLiteral(value);
|
|
201
204
|
}
|
|
202
205
|
return serialize(value, '');
|
|
203
206
|
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* transformer options) replaces this inference entirely and makes typed
|
|
19
19
|
* empty collections editable.
|
|
20
20
|
*/
|
|
21
|
-
import type { NodeGroup } from '../../core/schema';
|
|
21
|
+
import type { NodeGroup, NodeType } from '../../core/schema';
|
|
22
22
|
import { type SchemaKeys } from '../../core/schema-keys';
|
|
23
23
|
import { type CfgGroup, type CfgList, type CfgValue } from './parser';
|
|
24
24
|
/** Infer the root NodeGroup for a parsed document. */
|
|
@@ -37,6 +37,19 @@ type ListShape = 'groups' | 'scalars' | 'empty' | 'heterogeneous';
|
|
|
37
37
|
export declare function listShape(list: CfgList): ListShape;
|
|
38
38
|
/** The verbatim source slice of a node. */
|
|
39
39
|
export declare function raw(value: CfgValue, source: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Stamp the document's non-decimal integer presentation onto a
|
|
42
|
+
* **schema-driven** NodeGroup as `radix` display hints — the JSON Schema has
|
|
43
|
+
* no radix vocabulary, so the document is the only authority on how a value
|
|
44
|
+
* is written. Covered number/string leaves whose literal is hex/octal/binary
|
|
45
|
+
* (and homogeneous collections thereof) display in that base in every
|
|
46
|
+
* `unknownKeys` mode, matching what inference does for uncovered fields.
|
|
47
|
+
* Fills gaps only: a `radix` already on the leaf (e.g. carried by
|
|
48
|
+
* `mergeInferred` under `'edit'`) wins. Keyed containers — groups, choices,
|
|
49
|
+
* maps — resolve through {@link childrenOf}, so case fields and map entries
|
|
50
|
+
* annotate too.
|
|
51
|
+
*/
|
|
52
|
+
export declare function annotateSchemaRadix(node: CfgValue, schema: NodeType): void;
|
|
40
53
|
/**
|
|
41
54
|
* `unknownKeys: 'edit'` extraction fixup. The whole-document extraction runs
|
|
42
55
|
* with `emptyAsArrays` (schema-covered empty collections are typed and
|
|
@@ -4,6 +4,7 @@ exports.libconfigToNodeGroup = libconfigToNodeGroup;
|
|
|
4
4
|
exports.extractValue = extractValue;
|
|
5
5
|
exports.listShape = listShape;
|
|
6
6
|
exports.raw = raw;
|
|
7
|
+
exports.annotateSchemaRadix = annotateSchemaRadix;
|
|
7
8
|
exports.carryUncoveredEmpties = carryUncoveredEmpties;
|
|
8
9
|
const schema_keys_1 = require("../../core/schema-keys");
|
|
9
10
|
const parser_1 = require("./parser");
|
|
@@ -186,6 +187,63 @@ function rawLeaf(name, why) {
|
|
|
186
187
|
function raw(value, source) {
|
|
187
188
|
return source.slice(value.span.start, value.span.end);
|
|
188
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Stamp the document's non-decimal integer presentation onto a
|
|
192
|
+
* **schema-driven** NodeGroup as `radix` display hints — the JSON Schema has
|
|
193
|
+
* no radix vocabulary, so the document is the only authority on how a value
|
|
194
|
+
* is written. Covered number/string leaves whose literal is hex/octal/binary
|
|
195
|
+
* (and homogeneous collections thereof) display in that base in every
|
|
196
|
+
* `unknownKeys` mode, matching what inference does for uncovered fields.
|
|
197
|
+
* Fills gaps only: a `radix` already on the leaf (e.g. carried by
|
|
198
|
+
* `mergeInferred` under `'edit'`) wins. Keyed containers — groups, choices,
|
|
199
|
+
* maps — resolve through {@link childrenOf}, so case fields and map entries
|
|
200
|
+
* annotate too.
|
|
201
|
+
*/
|
|
202
|
+
function annotateSchemaRadix(node, schema) {
|
|
203
|
+
switch (schema.kind) {
|
|
204
|
+
case 'leaf': {
|
|
205
|
+
// Only number/string leaves render radix; a boolean/enum leaf covering
|
|
206
|
+
// an int literal is a scalar-type mismatch the form handles otherwise.
|
|
207
|
+
if (schema.type !== 'number' && schema.type !== 'string')
|
|
208
|
+
return;
|
|
209
|
+
if (node.kind === 'scalar' && node.int && node.int.radix !== 10 && !schema.radix) {
|
|
210
|
+
schema.radix = node.int.radix;
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
case 'leafList': {
|
|
215
|
+
if (schema.radix)
|
|
216
|
+
return;
|
|
217
|
+
if ((node.kind === 'array' || node.kind === 'list') && node.elements.every((e) => e.kind === 'scalar')) {
|
|
218
|
+
const radix = sharedRadix(node.elements);
|
|
219
|
+
if (radix)
|
|
220
|
+
schema.radix = radix;
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
case 'nodeGroupList': {
|
|
225
|
+
if (node.kind !== 'list')
|
|
226
|
+
return;
|
|
227
|
+
for (const el of node.elements)
|
|
228
|
+
annotateSchemaRadix(el, schema.type);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
default: {
|
|
232
|
+
// nodeGroup / choice / map — keyed containers over a document group.
|
|
233
|
+
if (node.kind !== 'group')
|
|
234
|
+
return;
|
|
235
|
+
const keys = (0, schema_keys_1.childrenOf)(schema);
|
|
236
|
+
if (!keys)
|
|
237
|
+
return;
|
|
238
|
+
for (const setting of node.settings) {
|
|
239
|
+
const child = keys.get(setting.name);
|
|
240
|
+
if (child)
|
|
241
|
+
annotateSchemaRadix(setting.value, child);
|
|
242
|
+
}
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
189
247
|
/**
|
|
190
248
|
* `unknownKeys: 'edit'` extraction fixup. The whole-document extraction runs
|
|
191
249
|
* with `emptyAsArrays` (schema-covered empty collections are typed and
|
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
* `core` — they are format-agnostic and shared with the JSON transformer.
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
-
exports.applyValueToDocument = exports.yamlTransformer = void 0;
|
|
11
|
+
exports.SchemaShapeError = exports.applyValueToDocument = exports.yamlTransformer = void 0;
|
|
12
12
|
var yaml_transformer_1 = require("./yaml-transformer");
|
|
13
13
|
Object.defineProperty(exports, "yamlTransformer", { enumerable: true, get: function () { return yaml_transformer_1.yamlTransformer; } });
|
|
14
14
|
var revert_1 = require("./revert");
|
|
15
15
|
Object.defineProperty(exports, "applyValueToDocument", { enumerable: true, get: function () { return revert_1.applyValueToDocument; } });
|
|
16
|
+
// Thrown by schema-driven toSchema; re-exported so subpath consumers can
|
|
17
|
+
// catch it by instance.
|
|
18
|
+
var shape_check_1 = require("../../core/shape-check");
|
|
19
|
+
Object.defineProperty(exports, "SchemaShapeError", { enumerable: true, get: function () { return shape_check_1.SchemaShapeError; } });
|
|
@@ -6,6 +6,8 @@ const thesaurus_1 = require("../../core/thesaurus");
|
|
|
6
6
|
const infer_1 = require("../../core/infer");
|
|
7
7
|
const json_schema_1 = require("../../core/json-schema");
|
|
8
8
|
const merge_inferred_1 = require("../../core/merge-inferred");
|
|
9
|
+
const schema_keys_1 = require("../../core/schema-keys");
|
|
10
|
+
const shape_check_1 = require("../../core/shape-check");
|
|
9
11
|
const bigint_1 = require("../../core/bigint");
|
|
10
12
|
const revert_1 = require("./revert");
|
|
11
13
|
/**
|
|
@@ -36,9 +38,14 @@ exports.yamlTransformer = {
|
|
|
36
38
|
const schema = fromJsonSchema && unknownKeys === 'edit'
|
|
37
39
|
? (0, merge_inferred_1.mergeInferred)(fromJsonSchema, (0, infer_1.inferNodeGroup)(data, options?.rootName))
|
|
38
40
|
: fromJsonSchema ?? (0, infer_1.inferNodeGroup)(data, options?.rootName);
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
if (
|
|
41
|
+
// A container-shape disagreement between document and schema would erase
|
|
42
|
+
// the section on save: refuse up front (see assertSchemaShapes).
|
|
43
|
+
if (fromJsonSchema)
|
|
44
|
+
(0, shape_check_1.assertSchemaShapes)(data, fromJsonSchema);
|
|
45
|
+
// Every mode picks up the document's hex/octal presentation: inference
|
|
46
|
+
// for its own leaves, schema-driven leaves too — the JSON Schema cannot
|
|
47
|
+
// know the base a value was written in, the document does.
|
|
48
|
+
if (doc.contents)
|
|
42
49
|
annotateRadix(doc.contents, schema);
|
|
43
50
|
const labeled = options?.thesaurus ? (0, thesaurus_1.applyThesaurus)(schema, options.thesaurus) : schema;
|
|
44
51
|
return {
|
|
@@ -62,17 +69,23 @@ exports.yamlTransformer = {
|
|
|
62
69
|
const RADIX_BY_FORMAT = { BIN: 2, OCT: 8, HEX: 16 };
|
|
63
70
|
/**
|
|
64
71
|
* Copy non-decimal integer presentation (`0x`/`0o`/`0b` literals) from the
|
|
65
|
-
* parsed document onto the
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
+
* parsed document onto the schema as leaf/leafList `radix` display hints —
|
|
73
|
+
* neither plain-data inference (`toJS()` drops the scalar format) nor a JSON
|
|
74
|
+
* Schema (no radix vocabulary) can know the base a value was written in; the
|
|
75
|
+
* document is the only authority. Runs in every mode and fills gaps only, so
|
|
76
|
+
* a `radix` already on a leaf wins. The revert needs nothing: scalars are
|
|
77
|
+
* mutated in place, so an edited value re-emits in its literal's own base.
|
|
78
|
+
* Across group-list items the first non-decimal occurrence wins — one shared
|
|
79
|
+
* item schema cannot vary the display per item. Keyed containers — groups,
|
|
80
|
+
* choices, maps — resolve their children through {@link childrenOf}, so case
|
|
81
|
+
* fields and map entries annotate too.
|
|
72
82
|
*/
|
|
73
83
|
function annotateRadix(node, schema) {
|
|
74
84
|
switch (schema.kind) {
|
|
75
85
|
case 'leaf': {
|
|
86
|
+
// Only number/string leaves render radix.
|
|
87
|
+
if (schema.type !== 'number' && schema.type !== 'string')
|
|
88
|
+
return;
|
|
76
89
|
const radix = scalarRadix(node);
|
|
77
90
|
if (radix && !schema.radix)
|
|
78
91
|
schema.radix = radix;
|
|
@@ -86,25 +99,27 @@ function annotateRadix(node, schema) {
|
|
|
86
99
|
schema.radix = radixes[0];
|
|
87
100
|
return;
|
|
88
101
|
}
|
|
89
|
-
case '
|
|
102
|
+
case 'nodeGroupList': {
|
|
103
|
+
if (!(0, yaml_1.isSeq)(node))
|
|
104
|
+
return;
|
|
105
|
+
for (const item of node.items)
|
|
106
|
+
annotateRadix(item, schema.type);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
default: {
|
|
110
|
+
// nodeGroup / choice / map — keyed containers over a document mapping.
|
|
90
111
|
if (!(0, yaml_1.isMap)(node))
|
|
91
112
|
return;
|
|
113
|
+
const keys = (0, schema_keys_1.childrenOf)(schema);
|
|
114
|
+
if (!keys)
|
|
115
|
+
return;
|
|
92
116
|
for (const pair of node.items) {
|
|
93
|
-
const child =
|
|
117
|
+
const child = keys.get(pairKey(pair.key));
|
|
94
118
|
if (child)
|
|
95
119
|
annotateRadix(pair.value, child);
|
|
96
120
|
}
|
|
97
121
|
return;
|
|
98
122
|
}
|
|
99
|
-
case 'nodeGroupList': {
|
|
100
|
-
if (!(0, yaml_1.isSeq)(node))
|
|
101
|
-
return;
|
|
102
|
-
for (const item of node.items)
|
|
103
|
-
annotateRadix(item, schema.type);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
default:
|
|
107
|
-
return; // choice/map never come out of plain-data inference
|
|
108
123
|
}
|
|
109
124
|
}
|
|
110
125
|
/** The display radix of a non-decimal integer scalar node, else undefined. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ng-form-foundry-transformers",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "Framework-agnostic Node + TypeScript catalog of source-format transformers: turn a YANG model (and more) into an ng-form-foundry schema and revert the edited form value back to the source format.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ng-form-foundry",
|