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
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.applyValueToDocument = applyValueToDocument;
|
|
4
4
|
const yaml_1 = require("yaml");
|
|
5
5
|
const bigint_1 = require("../../core/bigint");
|
|
6
|
+
const schema_keys_1 = require("../../core/schema-keys");
|
|
6
7
|
/**
|
|
7
8
|
* Apply an edited form value onto a parsed YAML {@link Document} in place,
|
|
8
9
|
* preserving comments and formatting on every node that survives the edit.
|
|
@@ -14,17 +15,23 @@ const bigint_1 = require("../../core/bigint");
|
|
|
14
15
|
* {@link import('./json-schema')}. Callers should clone the document first if the
|
|
15
16
|
* original must be preserved.
|
|
16
17
|
*
|
|
18
|
+
* With `schema` (the NodeGroup the form was built from) the
|
|
19
|
+
* value is authoritative for **schema-born paths only**: keys the schema does
|
|
20
|
+
* not cover — at any depth — are preserved verbatim, their comments included.
|
|
21
|
+
* Without it the value covers the whole document and a key it lacks is a
|
|
22
|
+
* deletion.
|
|
23
|
+
*
|
|
17
24
|
* Map keys are matched by the *same string form* `doc.toJS()` produced when the
|
|
18
25
|
* form value was built, so non-string keys (a `80:` port map, a `true:` flag)
|
|
19
26
|
* reconcile against their original typed key nodes instead of being appended as
|
|
20
27
|
* duplicate string-keyed pairs.
|
|
21
28
|
*/
|
|
22
|
-
function applyValueToDocument(doc, value) {
|
|
29
|
+
function applyValueToDocument(doc, value, schema) {
|
|
23
30
|
if (doc.contents == null) {
|
|
24
31
|
doc.contents = doc.createNode(value);
|
|
25
32
|
return;
|
|
26
33
|
}
|
|
27
|
-
doc.contents = applyToNode(doc, doc.contents, value);
|
|
34
|
+
doc.contents = applyToNode(doc, doc.contents, value, schema);
|
|
28
35
|
}
|
|
29
36
|
/**
|
|
30
37
|
* Reconcile the parsed `node` at one position with its edited `value`, returning
|
|
@@ -32,22 +39,27 @@ function applyValueToDocument(doc, value) {
|
|
|
32
39
|
* a map/sequence is reconciled child-by-child; a shape change (or a brand-new
|
|
33
40
|
* position) allocates a fresh node.
|
|
34
41
|
*/
|
|
35
|
-
function applyToNode(doc, node, value) {
|
|
42
|
+
function applyToNode(doc, node, value, schema) {
|
|
36
43
|
if (isPlainObject(value)) {
|
|
37
44
|
if (!(0, yaml_1.isMap)(node))
|
|
38
45
|
return doc.createNode(value);
|
|
39
|
-
reconcileMap(doc, node, value);
|
|
46
|
+
reconcileMap(doc, node, value, schema && (0, schema_keys_1.childrenOf)(schema));
|
|
40
47
|
return node;
|
|
41
48
|
}
|
|
42
49
|
if (Array.isArray(value)) {
|
|
43
50
|
if (!(0, yaml_1.isSeq)(node))
|
|
44
51
|
return doc.createNode(value);
|
|
45
|
-
reconcileSeq(doc, node, value);
|
|
52
|
+
reconcileSeq(doc, node, value, (0, schema_keys_1.itemSchemaOf)(schema));
|
|
46
53
|
return node;
|
|
47
54
|
}
|
|
48
55
|
// scalar (string / number / boolean / null)
|
|
49
56
|
if ((0, yaml_1.isScalar)(node)) {
|
|
50
|
-
|
|
57
|
+
const next = coerceScalarValue(value, node.value);
|
|
58
|
+
// An unchanged value keeps the node untouched — the parsed BigInt and the
|
|
59
|
+
// form's number compare by numeric value, so an identity pass never
|
|
60
|
+
// downgrades a BigInt-held integer to a lossy JS number.
|
|
61
|
+
if (!sameScalar(node.value, next))
|
|
62
|
+
node.value = next;
|
|
51
63
|
return node;
|
|
52
64
|
}
|
|
53
65
|
return doc.createNode(value);
|
|
@@ -56,13 +68,24 @@ function applyToNode(doc, node, value) {
|
|
|
56
68
|
* Update a map node against the value object: drop pairs whose key is gone,
|
|
57
69
|
* recurse into the ones that remain (matched by their `toJS` key string, so
|
|
58
70
|
* typed keys line up), and append a fresh pair for each genuinely new key.
|
|
71
|
+
* Under `schema`, keys that are not schema-born are exempt from all three —
|
|
72
|
+
* never dropped, never recursed into, never addable.
|
|
59
73
|
*/
|
|
60
|
-
function reconcileMap(doc, node, value) {
|
|
61
|
-
node.items = node.items.filter((pair) =>
|
|
74
|
+
function reconcileMap(doc, node, value, schema) {
|
|
75
|
+
node.items = node.items.filter((pair) => {
|
|
76
|
+
const key = keyString(pair.key);
|
|
77
|
+
if (schema && !schema.has(key))
|
|
78
|
+
return true; // not schema-born: verbatim
|
|
79
|
+
// Own-key check: `in` would resolve doc keys like `toString` through the
|
|
80
|
+
// prototype chain and make them undeletable.
|
|
81
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
82
|
+
});
|
|
62
83
|
for (const key of Object.keys(value)) {
|
|
84
|
+
if (schema && !schema.has(key))
|
|
85
|
+
continue; // not schema-born: never written
|
|
63
86
|
const pair = node.items.find((p) => keyString(p.key) === key);
|
|
64
87
|
if (pair) {
|
|
65
|
-
pair.value = applyToNode(doc, pair.value, value[key]);
|
|
88
|
+
pair.value = applyToNode(doc, pair.value, value[key], schema?.get(key));
|
|
66
89
|
}
|
|
67
90
|
else {
|
|
68
91
|
node.items.push(doc.createPair(key, value[key]));
|
|
@@ -70,12 +93,12 @@ function reconcileMap(doc, node, value) {
|
|
|
70
93
|
}
|
|
71
94
|
}
|
|
72
95
|
/** Update a sequence node: shrink/grow to the value's length, recurse per item. */
|
|
73
|
-
function reconcileSeq(doc, node, value) {
|
|
96
|
+
function reconcileSeq(doc, node, value, itemSchema) {
|
|
74
97
|
while (node.items.length > value.length)
|
|
75
98
|
node.items.pop();
|
|
76
99
|
for (let i = 0; i < value.length; i++) {
|
|
77
100
|
if (i < node.items.length) {
|
|
78
|
-
node.items[i] = applyToNode(doc, node.items[i], value[i]);
|
|
101
|
+
node.items[i] = applyToNode(doc, node.items[i], value[i], itemSchema);
|
|
79
102
|
}
|
|
80
103
|
else {
|
|
81
104
|
node.items.push(doc.createNode(value[i]));
|
|
@@ -96,6 +119,15 @@ function coerceScalarValue(value, current) {
|
|
|
96
119
|
}
|
|
97
120
|
return value;
|
|
98
121
|
}
|
|
122
|
+
/** Numeric equality across the BigInt/number divide, `===` otherwise. */
|
|
123
|
+
function sameScalar(current, next) {
|
|
124
|
+
if (current === next)
|
|
125
|
+
return true;
|
|
126
|
+
if (typeof current === 'bigint' && typeof next === 'number' && Number.isInteger(next)) {
|
|
127
|
+
return current === BigInt(next);
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
99
131
|
/**
|
|
100
132
|
* The string a key node takes as a JS object key, matching `doc.toJS()`: the
|
|
101
133
|
* scalar value stringified, with `null`/`undefined` collapsing to `''` (the
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Document } from 'yaml';
|
|
2
|
-
import type { FormValue, Thesaurus } from '../../core/schema';
|
|
2
|
+
import type { FormValue, NodeGroup, Thesaurus } from '../../core/schema';
|
|
3
3
|
import type { TransformResult } from '../../core/transformer';
|
|
4
4
|
import { type JsonSchema, type JsonSchemaOptions } from '../../core/json-schema';
|
|
5
5
|
/** Options for {@link yamlTransformer}'s `toSchema`. */
|
|
@@ -21,6 +21,32 @@ export interface YamlOptions {
|
|
|
21
21
|
* case-insensitively; never paths.
|
|
22
22
|
*/
|
|
23
23
|
thesaurus?: Thesaurus;
|
|
24
|
+
/**
|
|
25
|
+
* Schema-driven mode only: what happens to keys the JSON Schema does not
|
|
26
|
+
* cover. `'preserve'` (default) keeps them verbatim — the form never
|
|
27
|
+
* carried them, so a partial schema edits its slice without erasing the
|
|
28
|
+
* rest. `'drop'` makes the edited value authoritative for the whole
|
|
29
|
+
* document, deleting uncovered keys — for consumers whose schema is
|
|
30
|
+
* intentionally complete. `'edit'` surfaces them instead: uncovered keys
|
|
31
|
+
* render as editable fields typed by the data (the inferred schema merged
|
|
32
|
+
* under the JSON Schema), so nothing is invisible and the value covers the
|
|
33
|
+
* whole document. Ignored without a `schema`.
|
|
34
|
+
*/
|
|
35
|
+
unknownKeys?: 'preserve' | 'drop' | 'edit';
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The revert context: the parsed {@link Document} and — in schema-driven
|
|
39
|
+
* mode with `unknownKeys: 'preserve'` or `'edit'` — the NodeGroup the form
|
|
40
|
+
* was built from (the JSON Schema's own under `'preserve'`, the
|
|
41
|
+
* inferred-merged one under `'edit'`), so `toSource` treats the value as
|
|
42
|
+
* authoritative for schema-born paths only. Under `'edit'` that is almost
|
|
43
|
+
* the whole document; the gate still protects the keys no form field can
|
|
44
|
+
* carry (e.g. a key inside a covered choice's object that no case names).
|
|
45
|
+
* Treat it as opaque: build it with `toSchema`, hand it back to `toSource`.
|
|
46
|
+
*/
|
|
47
|
+
export interface YamlBinding {
|
|
48
|
+
doc: Document;
|
|
49
|
+
schema?: NodeGroup;
|
|
24
50
|
}
|
|
25
51
|
/**
|
|
26
52
|
* A YAML {@link Transformer}: turn a YAML config document into a form and write
|
|
@@ -28,14 +54,15 @@ export interface YamlOptions {
|
|
|
28
54
|
* preserves comments, key order, and formatting by applying edits onto the parsed
|
|
29
55
|
* document (see {@link applyValueToDocument}).
|
|
30
56
|
*
|
|
31
|
-
* The `binding` it round-trips is
|
|
32
|
-
* it before applying, so a single `toSchema` result can serve
|
|
57
|
+
* The `binding` it round-trips is a {@link YamlBinding}; `toSource` clones the
|
|
58
|
+
* document inside it before applying, so a single `toSchema` result can serve
|
|
59
|
+
* many edits.
|
|
33
60
|
*
|
|
34
61
|
* With a JSON Schema in {@link YamlOptions}, the form is schema-driven; without
|
|
35
62
|
* one it is inferred from the data.
|
|
36
63
|
*/
|
|
37
64
|
export declare const yamlTransformer: {
|
|
38
65
|
id: string;
|
|
39
|
-
toSchema(source: string, options?: YamlOptions): TransformResult<
|
|
40
|
-
toSource(value: FormValue, binding:
|
|
66
|
+
toSchema(source: string, options?: YamlOptions): TransformResult<YamlBinding>;
|
|
67
|
+
toSource(value: FormValue, binding: YamlBinding): string;
|
|
41
68
|
};
|
|
@@ -5,6 +5,9 @@ const yaml_1 = require("yaml");
|
|
|
5
5
|
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
|
+
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");
|
|
8
11
|
const bigint_1 = require("../../core/bigint");
|
|
9
12
|
const revert_1 = require("./revert");
|
|
10
13
|
/**
|
|
@@ -13,8 +16,9 @@ const revert_1 = require("./revert");
|
|
|
13
16
|
* preserves comments, key order, and formatting by applying edits onto the parsed
|
|
14
17
|
* document (see {@link applyValueToDocument}).
|
|
15
18
|
*
|
|
16
|
-
* The `binding` it round-trips is
|
|
17
|
-
* it before applying, so a single `toSchema` result can serve
|
|
19
|
+
* The `binding` it round-trips is a {@link YamlBinding}; `toSource` clones the
|
|
20
|
+
* document inside it before applying, so a single `toSchema` result can serve
|
|
21
|
+
* many edits.
|
|
18
22
|
*
|
|
19
23
|
* With a JSON Schema in {@link YamlOptions}, the form is schema-driven; without
|
|
20
24
|
* one it is inferred from the data.
|
|
@@ -27,17 +31,36 @@ exports.yamlTransformer = {
|
|
|
27
31
|
// out-of-range integers become strings there (safe ones become plain numbers).
|
|
28
32
|
const doc = (0, yaml_1.parseDocument)(source, { intAsBigInt: true });
|
|
29
33
|
const data = (normalizeBigInts(doc.toJS()) ?? {});
|
|
30
|
-
const
|
|
34
|
+
const unknownKeys = options?.unknownKeys ?? 'preserve';
|
|
35
|
+
const fromJsonSchema = options?.schema
|
|
31
36
|
? (0, json_schema_1.jsonSchemaToNodeGroup)(options.schema, options.rootName, options.schemaOptions)
|
|
32
|
-
:
|
|
33
|
-
|
|
37
|
+
: undefined;
|
|
38
|
+
const schema = fromJsonSchema && unknownKeys === 'edit'
|
|
39
|
+
? (0, merge_inferred_1.mergeInferred)(fromJsonSchema, (0, infer_1.inferNodeGroup)(data, options?.rootName))
|
|
40
|
+
: fromJsonSchema ?? (0, infer_1.inferNodeGroup)(data, options?.rootName);
|
|
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)
|
|
34
49
|
annotateRadix(doc.contents, schema);
|
|
35
50
|
const labeled = options?.thesaurus ? (0, thesaurus_1.applyThesaurus)(schema, options.thesaurus) : schema;
|
|
36
|
-
return {
|
|
51
|
+
return {
|
|
52
|
+
schema: labeled,
|
|
53
|
+
// 'preserve' gates the revert on the JSON Schema's NodeGroup; 'edit' on
|
|
54
|
+
// the merged one (schema-born ≈ everything, but keys no form field can
|
|
55
|
+
// carry stay protected); 'drop' and inferred mode leave the value
|
|
56
|
+
// authoritative for the whole document.
|
|
57
|
+
binding: { doc, schema: fromJsonSchema && unknownKeys !== 'drop' ? schema : undefined },
|
|
58
|
+
initialValue: data,
|
|
59
|
+
};
|
|
37
60
|
},
|
|
38
61
|
toSource(value, binding) {
|
|
39
|
-
const doc = binding.clone();
|
|
40
|
-
(0, revert_1.applyValueToDocument)(doc, value);
|
|
62
|
+
const doc = binding.doc.clone();
|
|
63
|
+
(0, revert_1.applyValueToDocument)(doc, value, binding.schema);
|
|
41
64
|
return String(doc);
|
|
42
65
|
},
|
|
43
66
|
// `satisfies` verifies conformance to the catalog contract while keeping the
|
|
@@ -46,17 +69,23 @@ exports.yamlTransformer = {
|
|
|
46
69
|
const RADIX_BY_FORMAT = { BIN: 2, OCT: 8, HEX: 16 };
|
|
47
70
|
/**
|
|
48
71
|
* Copy non-decimal integer presentation (`0x`/`0o`/`0b` literals) from the
|
|
49
|
-
* parsed document onto the
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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.
|
|
56
82
|
*/
|
|
57
83
|
function annotateRadix(node, schema) {
|
|
58
84
|
switch (schema.kind) {
|
|
59
85
|
case 'leaf': {
|
|
86
|
+
// Only number/string leaves render radix.
|
|
87
|
+
if (schema.type !== 'number' && schema.type !== 'string')
|
|
88
|
+
return;
|
|
60
89
|
const radix = scalarRadix(node);
|
|
61
90
|
if (radix && !schema.radix)
|
|
62
91
|
schema.radix = radix;
|
|
@@ -70,25 +99,27 @@ function annotateRadix(node, schema) {
|
|
|
70
99
|
schema.radix = radixes[0];
|
|
71
100
|
return;
|
|
72
101
|
}
|
|
73
|
-
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.
|
|
74
111
|
if (!(0, yaml_1.isMap)(node))
|
|
75
112
|
return;
|
|
113
|
+
const keys = (0, schema_keys_1.childrenOf)(schema);
|
|
114
|
+
if (!keys)
|
|
115
|
+
return;
|
|
76
116
|
for (const pair of node.items) {
|
|
77
|
-
const child =
|
|
117
|
+
const child = keys.get(pairKey(pair.key));
|
|
78
118
|
if (child)
|
|
79
119
|
annotateRadix(pair.value, child);
|
|
80
120
|
}
|
|
81
121
|
return;
|
|
82
122
|
}
|
|
83
|
-
case 'nodeGroupList': {
|
|
84
|
-
if (!(0, yaml_1.isSeq)(node))
|
|
85
|
-
return;
|
|
86
|
-
for (const item of node.items)
|
|
87
|
-
annotateRadix(item, schema.type);
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
default:
|
|
91
|
-
return; // choice/map never come out of plain-data inference
|
|
92
123
|
}
|
|
93
124
|
}
|
|
94
125
|
/** The display radix of a non-decimal integer scalar node, else undefined. */
|
|
@@ -115,7 +146,8 @@ function normalizeBigInts(value) {
|
|
|
115
146
|
if (Array.isArray(value))
|
|
116
147
|
return value.map(normalizeBigInts);
|
|
117
148
|
if (value && typeof value === 'object') {
|
|
118
|
-
|
|
149
|
+
// Null prototype: a `__proto__` document key must stay a data key.
|
|
150
|
+
const out = Object.create(null);
|
|
119
151
|
for (const key of Object.keys(value))
|
|
120
152
|
out[key] = normalizeBigInts(value[key]);
|
|
121
153
|
return out;
|
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.2",
|
|
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",
|