ng-form-foundry-transformers 0.3.1 → 0.3.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 CHANGED
@@ -43,6 +43,12 @@ For config formats (YAML, JSON) the thing you load and save is the same document
43
43
  type, so `TData` defaults to `TSource`. Schema formats differ: YANG's `toSchema`
44
44
  consumes a model source but its `toSource` reverts to RFC 7951 *data*.
45
45
 
46
+ **Choice values:** the YAML/JSON `toSource` applies the value's keys verbatim,
47
+ so when the schema contains choices (`anyOf`/`oneOf`) pass the **wire value** —
48
+ the library's `serializeForm(schema, form)`, which strips the `__case`
49
+ discriminators from `getRawValue()`. The YANG adapter is the exception: its
50
+ `toYangData` consumes the form value and flattens `__case` itself.
51
+
46
52
  ## YAML transformer
47
53
 
48
54
  Edit a **YAML config file**: turn it into a form, then write the edited value
@@ -74,9 +80,13 @@ covers **draft 2020-12** (back-compatible with draft-07): `object` → nodeGroup
74
80
  a `map` for `additionalProperties`/`patternProperties`), `array` → nodeGroupList /
75
81
  leafList, `anyOf`/`oneOf` → choice (or a nullable leaf for `[T, null]`), `$ref` →
76
82
  `$defs`/`definitions` resolved inline (local **or cross-file** via
77
- `options.refDocuments`, matched by `$id`), `const` → a read-only leaf, and the string /
78
- number constraints (`pattern`, `minLength`, `minimum`, `multipleOf`, `format`, …)
79
- carried onto the leaves as validators.
83
+ `schemaOptions.refDocuments`, matched by `$id`), `const` → a read-only leaf, and the
84
+ string / number constraints (`pattern`, `minLength`, `minimum`, `multipleOf`,
85
+ `format`, …) carried onto the leaves as validators. Non-`required` properties
86
+ become **`presence` nodes** — absent from the form and the serialized value until
87
+ enabled, so the output validates against the source schema (opt out with
88
+ `schemaOptions: { optionalPresence: false }`); a required property mapping to a
89
+ choice is marked `mandatory`.
80
90
 
81
91
  ## JSON transformer
82
92
 
@@ -181,6 +191,19 @@ npm test # node:test on the compiled output (no Python needed)
181
191
 
182
192
  ## Status
183
193
 
194
+ `0.3.3` — schema-valid round-trips for JSON-Schema-driven forms. Non-`required`
195
+ properties now map to `presence` nodes (absent until enabled; opt out with
196
+ `schemaOptions: { optionalPresence: false }`), a required property mapping to a
197
+ choice is flagged `mandatory`, and the YAML/JSON transformers gained
198
+ `options.schemaOptions` — which also makes `refDocuments` (cross-file `$ref`)
199
+ reachable through them for the first time. Pairs with the library's
200
+ required-aware choice-case inference, empty list seeding, and
201
+ materialized-validity rules.
202
+
203
+ `0.3.2` — documentation release alongside the library's `serializeForm`: the
204
+ YAML/JSON `toSource` examples now take the wire value (choice `__case`
205
+ discriminators stripped); no transformer code changes.
206
+
184
207
  `0.3.1` — version alignment with the library's review-fix release (all 59
185
208
  findings of the tree-editor/form-renderer review resolved); no transformer
186
209
  changes.
@@ -47,6 +47,15 @@ export interface JsonSchemaOptions {
47
47
  * document then resolve against it.
48
48
  */
49
49
  refDocuments?: JsonSchema[];
50
+ /**
51
+ * Map non-required properties to `presence: true` nodes (default `true`):
52
+ * absent from the form and the serialized value until the user enables them,
53
+ * which is what `required` + `additionalProperties: false` schemas demand —
54
+ * an untouched optional materialized as `null` fails validation against the
55
+ * very schema the form came from. Set `false` to materialize every property
56
+ * unconditionally.
57
+ */
58
+ optionalPresence?: boolean;
50
59
  }
51
60
  type JsonSchemaType = 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'null';
52
61
  /**
@@ -58,8 +67,11 @@ type JsonSchemaType = 'object' | 'array' | 'string' | 'number' | 'integer' | 'bo
58
67
  * `additionalProperties`/`patternProperties`); `anyOf`/`oneOf` → a {@link Choice}
59
68
  * (or a nullable leaf for the `[T, null]` shape); `array` → nodeGroupList or
60
69
  * leafList; a scalar (with `enum`, `const`, `type: [T, 'null']`, and the string /
61
- * number constraints) → a typed leaf. `required` marks child leaves, `title`
62
- * becomes the label, `default`/`const` carry a scalar value.
70
+ * number constraints) → a typed leaf. `required` marks leaves required and a
71
+ * choice `mandatory`; a property *not* in `required` becomes a `presence` node
72
+ * — absent from the form value until enabled — unless
73
+ * {@link JsonSchemaOptions.optionalPresence} is `false`. `title` becomes the
74
+ * label, `default`/`const` carry a scalar value.
63
75
  */
64
76
  export declare function jsonSchemaToNodeGroup(schema: JsonSchema, name?: string, options?: JsonSchemaOptions): NodeGroup;
65
77
  export {};
@@ -11,17 +11,37 @@ const ROOT = '__root__';
11
11
  * `additionalProperties`/`patternProperties`); `anyOf`/`oneOf` → a {@link Choice}
12
12
  * (or a nullable leaf for the `[T, null]` shape); `array` → nodeGroupList or
13
13
  * leafList; a scalar (with `enum`, `const`, `type: [T, 'null']`, and the string /
14
- * number constraints) → a typed leaf. `required` marks child leaves, `title`
15
- * becomes the label, `default`/`const` carry a scalar value.
14
+ * number constraints) → a typed leaf. `required` marks leaves required and a
15
+ * choice `mandatory`; a property *not* in `required` becomes a `presence` node
16
+ * — absent from the form value until enabled — unless
17
+ * {@link JsonSchemaOptions.optionalPresence} is `false`. `title` becomes the
18
+ * label, `default`/`const` carry a scalar value.
16
19
  */
17
20
  function jsonSchemaToNodeGroup(schema, name = ROOT, options) {
18
21
  const documents = [schema, ...(options?.refDocuments ?? [])];
19
22
  const resolver = new RefResolver(schema, documents);
23
+ const ctx = { optionalPresence: options?.optionalPresence ?? true };
20
24
  const { schema: root, scope } = resolver.resolve(schema);
21
- const group = objectToNodeGroup(root, name, scope, root.title);
25
+ const group = objectToNodeGroup(root, name, scope, ctx, root.title);
22
26
  group.root = true;
23
27
  return group;
24
28
  }
29
+ /**
30
+ * Apply {@link JsonSchemaOptions.optionalPresence} to one *property* node: a
31
+ * non-required property becomes a presence node, so it stays absent from the
32
+ * form value until enabled. Only called at property positions
33
+ * ({@link objectToNodeGroup} children and {@link branchToCase} fields) — a map's
34
+ * `value` template, an array's item type, and a leaf-bodied case are not
35
+ * properties and must never be marked. Lists cannot carry `presence`.
36
+ */
37
+ function markOptionalProperty(node, required, ctx) {
38
+ if (!ctx.optionalPresence || required)
39
+ return node;
40
+ if (node.kind === 'leaf' || node.kind === 'nodeGroup' || node.kind === 'choice' || node.kind === 'map') {
41
+ node.presence = true;
42
+ }
43
+ return node;
44
+ }
25
45
  /**
26
46
  * Resolves `$ref` chains, following them across documents. A resolver is bound to
27
47
  * one document (for `#/…` local refs); crossing into another document via a
@@ -80,11 +100,12 @@ function resolveFragment(doc, fragment) {
80
100
  return node;
81
101
  }
82
102
  // --- objects / maps -----------------------------------------------------------
83
- function objectToNodeGroup(schema, name, resolver, label) {
103
+ function objectToNodeGroup(schema, name, resolver, ctx, label) {
84
104
  const required = new Set(schema.required ?? []);
85
105
  const children = {};
86
106
  for (const [key, propSchema] of Object.entries(schema.properties ?? {})) {
87
- children[key] = schemaToNode(key, propSchema, required.has(key), resolver);
107
+ const node = schemaToNode(key, propSchema, required.has(key), resolver, ctx);
108
+ children[key] = markOptionalProperty(node, required.has(key), ctx);
88
109
  }
89
110
  const group = { kind: 'nodeGroup', name, children };
90
111
  if (label)
@@ -100,7 +121,7 @@ function isOpenMap(schema) {
100
121
  const hasPattern = !!schema.patternProperties && Object.keys(schema.patternProperties).length > 0;
101
122
  return additionalIsSchema || hasPattern;
102
123
  }
103
- function objectToMap(name, schema, resolver) {
124
+ function objectToMap(name, schema, resolver, ctx) {
104
125
  let valueSchema = {};
105
126
  let keyPattern;
106
127
  const pattern = schema.patternProperties && Object.entries(schema.patternProperties)[0];
@@ -110,7 +131,7 @@ function objectToMap(name, schema, resolver) {
110
131
  else if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
111
132
  valueSchema = schema.additionalProperties;
112
133
  }
113
- const node = { kind: 'map', name, value: schemaToNode('value', valueSchema, false, resolver) };
134
+ const node = { kind: 'map', name, value: schemaToNode('value', valueSchema, false, resolver, ctx) };
114
135
  if (schema.title)
115
136
  node.label = schema.title;
116
137
  if (keyPattern)
@@ -123,7 +144,7 @@ function objectToMap(name, schema, resolver) {
123
144
  }
124
145
  // --- anyOf / oneOf → choice ---------------------------------------------------
125
146
  /** The `anyOf`/`oneOf` `[T, null]` shape collapses to a nullable leaf; anything else is a choice. */
126
- function branchesToNode(name, schema, rawBranches, required, resolver) {
147
+ function branchesToNode(name, schema, rawBranches, required, resolver, ctx) {
127
148
  const branches = rawBranches.map((b) => resolver.resolve(b));
128
149
  const nonNull = branches.filter((b) => b.schema.type !== 'null');
129
150
  const sole = nonNull.length === 1 ? nonNull[0] : undefined;
@@ -136,11 +157,13 @@ function branchesToNode(name, schema, rawBranches, required, resolver) {
136
157
  const caseLabels = {};
137
158
  branches.forEach(({ schema: branch, scope }, index) => {
138
159
  const caseName = `case${index}`;
139
- cases[caseName] = branchToCase(branch, scope);
160
+ cases[caseName] = branchToCase(branch, scope, ctx);
140
161
  if (branch.title)
141
162
  caseLabels[caseName] = branch.title;
142
163
  });
143
164
  const choice = { kind: 'choice', name, cases };
165
+ if (required)
166
+ choice.mandatory = true;
144
167
  if (schema.title)
145
168
  choice.label = schema.title;
146
169
  if (Object.keys(caseLabels).length)
@@ -148,29 +171,30 @@ function branchesToNode(name, schema, rawBranches, required, resolver) {
148
171
  return choice;
149
172
  }
150
173
  /** A resolved branch → a case body: a field record for an object branch, else a single node. */
151
- function branchToCase(branch, scope) {
174
+ function branchToCase(branch, scope, ctx) {
152
175
  if (primaryType(branch) === 'object' && branch.properties) {
153
176
  const required = new Set(branch.required ?? []);
154
177
  const fields = {};
155
178
  for (const [key, propSchema] of Object.entries(branch.properties)) {
156
- fields[key] = schemaToNode(key, propSchema, required.has(key), scope);
179
+ const node = schemaToNode(key, propSchema, required.has(key), scope, ctx);
180
+ fields[key] = markOptionalProperty(node, required.has(key), ctx);
157
181
  }
158
182
  return fields;
159
183
  }
160
- return schemaToNode('value', branch, false, scope);
184
+ return schemaToNode('value', branch, false, scope, ctx);
161
185
  }
162
186
  // --- dispatch -----------------------------------------------------------------
163
- function schemaToNode(name, rawSchema, required, resolver) {
187
+ function schemaToNode(name, rawSchema, required, resolver, ctx) {
164
188
  const { schema, scope } = resolver.resolve(rawSchema);
165
189
  const branches = schema.anyOf ?? schema.oneOf;
166
190
  if (branches && branches.length) {
167
- return branchesToNode(name, schema, branches, required, scope);
191
+ return branchesToNode(name, schema, branches, required, scope, ctx);
168
192
  }
169
193
  const type = primaryType(schema);
170
194
  if (type === 'object' || (type === undefined && (schema.properties || isOpenMap(schema)))) {
171
195
  if (isOpenMap(schema))
172
- return objectToMap(name, schema, scope);
173
- return objectToNodeGroup(schema, name, scope, schema.title);
196
+ return objectToMap(name, schema, scope, ctx);
197
+ return objectToNodeGroup(schema, name, scope, ctx, schema.title);
174
198
  }
175
199
  if (type === 'array') {
176
200
  const { schema: items, scope: itemScope } = scope.resolve(schema.items ?? {});
@@ -178,7 +202,7 @@ function schemaToNode(name, rawSchema, required, resolver) {
178
202
  const node = {
179
203
  kind: 'nodeGroupList',
180
204
  name,
181
- type: objectToNodeGroup(items, name, itemScope, items.title),
205
+ type: objectToNodeGroup(items, name, itemScope, ctx, items.title),
182
206
  };
183
207
  if (schema.title)
184
208
  node.label = schema.title;
@@ -86,7 +86,10 @@ export interface Choice {
86
86
  /** Display labels for cases, keyed by case name. */
87
87
  caseLabels?: Record<string, string>;
88
88
  default?: string;
89
+ /** A case must be selected for the form to be valid (a required property). */
89
90
  mandatory?: boolean;
91
+ /** Optional choice whose presence is itself data (mirrors {@link NodeGroup.presence}). */
92
+ presence?: boolean;
90
93
  }
91
94
  /**
92
95
  * An open, arbitrary-keyed record: unlike {@link NodeGroup} (a fixed key set), a
@@ -1,6 +1,6 @@
1
1
  import type { FormValue } from '../../core/schema';
2
2
  import type { TransformResult } from '../../core/transformer';
3
- import { type JsonSchema } from '../../core/json-schema';
3
+ import { type JsonSchema, type JsonSchemaOptions } from '../../core/json-schema';
4
4
  /** Options for {@link jsonTransformer}'s `toSchema`. */
5
5
  export interface JsonOptions {
6
6
  /**
@@ -10,6 +10,8 @@ export interface JsonOptions {
10
10
  schema?: JsonSchema;
11
11
  /** Name for the root node group. Defaults to `__root__`. */
12
12
  rootName?: string;
13
+ /** Options forwarded to `jsonSchemaToNodeGroup` (`refDocuments`, `optionalPresence`). */
14
+ schemaOptions?: JsonSchemaOptions;
13
15
  }
14
16
  /** How the source was formatted, so `toSource` re-emits it the same way. */
15
17
  export interface JsonFormat {
@@ -28,7 +28,7 @@ exports.jsonTransformer = {
28
28
  const parsed = JSON.parse(source, bigIntReviver);
29
29
  const data = collectBigInts(parsed ?? {}, [], bigInts);
30
30
  const schema = options?.schema
31
- ? (0, json_schema_1.jsonSchemaToNodeGroup)(options.schema, options.rootName)
31
+ ? (0, json_schema_1.jsonSchemaToNodeGroup)(options.schema, options.rootName, options.schemaOptions)
32
32
  : (0, infer_1.inferNodeGroup)(data, options?.rootName);
33
33
  const binding = {
34
34
  indent: detectIndent(source),
@@ -1,7 +1,7 @@
1
1
  import { type Document } from 'yaml';
2
2
  import type { FormValue } from '../../core/schema';
3
3
  import type { TransformResult } from '../../core/transformer';
4
- import { type JsonSchema } from '../../core/json-schema';
4
+ import { type JsonSchema, type JsonSchemaOptions } from '../../core/json-schema';
5
5
  /** Options for {@link yamlTransformer}'s `toSchema`. */
6
6
  export interface YamlOptions {
7
7
  /**
@@ -12,6 +12,8 @@ export interface YamlOptions {
12
12
  schema?: JsonSchema;
13
13
  /** Name for the root node group. Defaults to `__root__`. */
14
14
  rootName?: string;
15
+ /** Options forwarded to `jsonSchemaToNodeGroup` (`refDocuments`, `optionalPresence`). */
16
+ schemaOptions?: JsonSchemaOptions;
15
17
  }
16
18
  /**
17
19
  * A YAML {@link Transformer}: turn a YAML config document into a form and write
@@ -27,7 +27,7 @@ exports.yamlTransformer = {
27
27
  const doc = (0, yaml_1.parseDocument)(source, { intAsBigInt: true });
28
28
  const data = (normalizeBigInts(doc.toJS()) ?? {});
29
29
  const schema = options?.schema
30
- ? (0, json_schema_1.jsonSchemaToNodeGroup)(options.schema, options.rootName)
30
+ ? (0, json_schema_1.jsonSchemaToNodeGroup)(options.schema, options.rootName, options.schemaOptions)
31
31
  : (0, infer_1.inferNodeGroup)(data, options?.rootName);
32
32
  return { schema, binding: doc, initialValue: data };
33
33
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ng-form-foundry-transformers",
3
- "version": "0.3.1",
3
+ "version": "0.3.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",