ng-form-foundry-transformers 0.3.2 → 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 +16 -3
- package/dist/core/json-schema.d.ts +14 -2
- package/dist/core/json-schema.js +41 -17
- package/dist/core/schema.d.ts +3 -0
- package/dist/transformers/json/json-transformer.d.ts +3 -1
- package/dist/transformers/json/json-transformer.js +1 -1
- package/dist/transformers/yaml/yaml-transformer.d.ts +3 -1
- package/dist/transformers/yaml/yaml-transformer.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -80,9 +80,13 @@ covers **draft 2020-12** (back-compatible with draft-07): `object` → nodeGroup
|
|
|
80
80
|
a `map` for `additionalProperties`/`patternProperties`), `array` → nodeGroupList /
|
|
81
81
|
leafList, `anyOf`/`oneOf` → choice (or a nullable leaf for `[T, null]`), `$ref` →
|
|
82
82
|
`$defs`/`definitions` resolved inline (local **or cross-file** via
|
|
83
|
-
`
|
|
84
|
-
number constraints (`pattern`, `minLength`, `minimum`, `multipleOf`,
|
|
85
|
-
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`.
|
|
86
90
|
|
|
87
91
|
## JSON transformer
|
|
88
92
|
|
|
@@ -187,6 +191,15 @@ npm test # node:test on the compiled output (no Python needed)
|
|
|
187
191
|
|
|
188
192
|
## Status
|
|
189
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
|
+
|
|
190
203
|
`0.3.2` — documentation release alongside the library's `serializeForm`: the
|
|
191
204
|
YAML/JSON `toSource` examples now take the wire value (choice `__case`
|
|
192
205
|
discriminators stripped); no transformer code 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
|
|
62
|
-
*
|
|
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 {};
|
package/dist/core/json-schema.js
CHANGED
|
@@ -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
|
|
15
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/core/schema.d.ts
CHANGED
|
@@ -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.
|
|
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",
|