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.
@@ -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
- node.value = coerceScalarValue(value, node.value);
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) => keyString(pair.key) in value);
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 the parsed {@link Document}; `toSource` clones
32
- * it before applying, so a single `toSchema` result can serve many edits.
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<Document>;
40
- toSource(value: FormValue, binding: Document): string;
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 the parsed {@link Document}; `toSource` clones
17
- * it before applying, so a single `toSchema` result can serve many edits.
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 schema = options?.schema
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
- : (0, infer_1.inferNodeGroup)(data, options?.rootName);
33
- if (!options?.schema && doc.contents)
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 { schema: labeled, binding: doc, initialValue: data };
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 inferred schema as leaf/leafList `radix` display
50
- * hints — the plain-data inference cannot see them, because `toJS()` drops the
51
- * scalar format. The revert needs nothing: scalars are mutated in place, so an
52
- * edited value re-emits in its literal's own base. Across group-list items the
53
- * first non-decimal occurrence wins one shared item schema cannot vary the
54
- * display per item. Schema-driven mode is untouched (JSON Schema has no radix
55
- * vocabulary).
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 'nodeGroup': {
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 = schema.children[pairKey(pair.key)];
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
- const out = {};
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.0",
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",