ng-form-foundry-transformers 0.3.4 → 0.3.5
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 +70 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +3 -1
- package/dist/core/json-schema.d.ts +10 -2
- package/dist/core/json-schema.js +5 -2
- package/dist/core/schema.d.ts +28 -0
- package/dist/core/thesaurus.d.ts +43 -0
- package/dist/core/thesaurus.js +128 -0
- package/dist/transformers/json/json-transformer.d.ts +8 -1
- package/dist/transformers/json/json-transformer.js +3 -1
- package/dist/transformers/libconfig/libconfig-transformer.d.ts +8 -1
- package/dist/transformers/libconfig/libconfig-transformer.js +3 -1
- package/dist/transformers/yaml/yaml-transformer.d.ts +8 -1
- package/dist/transformers/yaml/yaml-transformer.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,6 +50,69 @@ the library's `serializeForm(schema, form)`, which strips the `__case`
|
|
|
50
50
|
discriminators from `getRawValue()`. The YANG adapter is the exception: its
|
|
51
51
|
`toYangData` consumes the form value and flattens `__case` itself.
|
|
52
52
|
|
|
53
|
+
## Thesaurus — display metadata for machine schemas
|
|
54
|
+
|
|
55
|
+
Machine schemas (O-RAN A1 policy types, inferred configs) ship without titles,
|
|
56
|
+
so forms fall back to raw attribute names (`guRanUeId`, `mcc`). Every
|
|
57
|
+
`toSchema` accepts a **thesaurus** — identifier → `{ label, description }` —
|
|
58
|
+
injected into the produced schema, JSON-Schema-driven or inferred alike:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const thesaurus = {
|
|
62
|
+
ueId: { label: 'UE ID', description: 'UE identifier.' },
|
|
63
|
+
mcc: { label: 'MCC', description: 'Mobile Country Code (3 digits).' },
|
|
64
|
+
};
|
|
65
|
+
yamlTransformer.toSchema(text, { thesaurus }); // also json / libconfig
|
|
66
|
+
jsonSchemaToNodeGroup(schema, 'body', { thesaurus }); // or standalone
|
|
67
|
+
applyThesaurus(nodeGroup, thesaurus); // post-process anything
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Keys are **plain identifier names**, matched **case-insensitively** against
|
|
71
|
+
each node's property/setting name — one entry covers the identifier wherever
|
|
72
|
+
it appears. Keys are never paths: no separator exists, so a `.` in a key is a
|
|
73
|
+
literal character of the name. The thesaurus fills gaps only — schema-authored
|
|
74
|
+
`title`/`description` always win. Unlabeled choice cases are titled from their
|
|
75
|
+
discriminating required field; when sibling cases collide (identical required
|
|
76
|
+
sets), the library's case selectors disambiguate them by each case's
|
|
77
|
+
distinguishing fields.
|
|
78
|
+
|
|
79
|
+
When the same identifier means different things at different depths, a key
|
|
80
|
+
maps to a **list of variants scoped by `under`** — an ancestor-**name** suffix
|
|
81
|
+
(an array, so still no separators; each segment is a literal name). The
|
|
82
|
+
longest matching scope wins; an entry without `under` is the fallback:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const thesaurus = {
|
|
86
|
+
id: [
|
|
87
|
+
{ under: ['cell'], label: 'Cell ID' }, // any `id` directly inside a `cell`
|
|
88
|
+
{ under: ['slice'], label: 'S-NSSAI' },
|
|
89
|
+
{ label: 'ID' }, // everywhere else
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Scoping follows the **data hierarchy of names**: list indices, map entry keys,
|
|
95
|
+
and — by default — choice case names are transparent. For a **choice**, case
|
|
96
|
+
fields match both with and without their case-name segment, so one scope can
|
|
97
|
+
cover the whole choice or target a single case:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// scope (choice) → cases byUe / byCell, both with a field named `id`
|
|
101
|
+
const thesaurus = {
|
|
102
|
+
id: [
|
|
103
|
+
{ under: ['scope', 'byUe'], label: 'UE ID' }, // only inside the byUe case
|
|
104
|
+
{ under: ['scope', 'byCell'], label: 'Cell ID' }, // only inside byCell
|
|
105
|
+
{ under: ['scope'], label: 'Scope ID' }, // any other case of scope
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
// → byUe.id: "UE ID", byCell.id: "Cell ID"; the per-case labels also flow
|
|
109
|
+
// into caseLabels via each case's discriminating field.
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Scoping by an auto-generated case name (`under: ['case0']`) works but is
|
|
113
|
+
positional — it shifts when the source schema reorders its branches; prefer
|
|
114
|
+
case-name scopes only for hand-named cases.
|
|
115
|
+
|
|
53
116
|
## YAML transformer
|
|
54
117
|
|
|
55
118
|
Edit a **YAML config file**: turn it into a form, then write the edited value
|
|
@@ -231,6 +294,13 @@ npm test # node:test on the compiled output (no Python needed)
|
|
|
231
294
|
|
|
232
295
|
## Status
|
|
233
296
|
|
|
297
|
+
`0.3.5` — the **thesaurus**: every `toSchema` and `jsonSchemaToNodeGroup`
|
|
298
|
+
accept identifier → `{ label, description }` display metadata, with variants
|
|
299
|
+
scoped by ancestor names (`under`) for identifiers that mean different things
|
|
300
|
+
at different depths; choice cases are titled from their labeled discriminating
|
|
301
|
+
field. Adversarially battle-tested (51-agent panel); the paired library
|
|
302
|
+
release guarantees unique case-selector labels.
|
|
303
|
+
|
|
234
304
|
`0.3.4` — the **libconfig transformer debuts as a beta** (one-time console
|
|
235
305
|
warning on first use; lossless span-splicing round-trip with scalar types
|
|
236
306
|
preserved — see [`docs/libconfig.md`](docs/libconfig.md)), and
|
package/dist/core/index.d.ts
CHANGED
|
@@ -12,3 +12,5 @@ export { jsonSchemaToNodeGroup } from './json-schema';
|
|
|
12
12
|
export type { JsonSchema, JsonSchemaOptions } from './json-schema';
|
|
13
13
|
export type { NodeGroup, NodeType, Leaf, LeafList, NodeGroupList, Choice, ChoiceCase, NodeMap, LeafType, FormValue, } from './schema';
|
|
14
14
|
export { CASE_KEY } from './schema';
|
|
15
|
+
export type { Thesaurus, ThesaurusEntry } from './schema';
|
|
16
|
+
export { applyThesaurus } from './thesaurus';
|
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.CASE_KEY = exports.jsonSchemaToNodeGroup = exports.inferNodeGroup = exports.TransformerRegistry = void 0;
|
|
10
|
+
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.
|
|
@@ -17,3 +17,5 @@ var json_schema_1 = require("./json-schema");
|
|
|
17
17
|
Object.defineProperty(exports, "jsonSchemaToNodeGroup", { enumerable: true, get: function () { return json_schema_1.jsonSchemaToNodeGroup; } });
|
|
18
18
|
var schema_1 = require("./schema");
|
|
19
19
|
Object.defineProperty(exports, "CASE_KEY", { enumerable: true, get: function () { return schema_1.CASE_KEY; } });
|
|
20
|
+
var thesaurus_1 = require("./thesaurus");
|
|
21
|
+
Object.defineProperty(exports, "applyThesaurus", { enumerable: true, get: function () { return thesaurus_1.applyThesaurus; } });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { NodeGroup } from './schema';
|
|
1
|
+
import type { NodeGroup, Thesaurus } from './schema';
|
|
2
2
|
/**
|
|
3
3
|
* The subset of JSON Schema (draft 2020-12, back-compatible with draft-07) this
|
|
4
4
|
* transformer maps to a form. Only the keywords that shape a form are read;
|
|
@@ -56,6 +56,12 @@ export interface JsonSchemaOptions {
|
|
|
56
56
|
* unconditionally.
|
|
57
57
|
*/
|
|
58
58
|
optionalPresence?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Display metadata injected into the produced schema (`label`,
|
|
61
|
+
* `description`, choice `caseLabels`) — see {@link applyThesaurus}. Fills
|
|
62
|
+
* gaps only; schema-authored `title`/`description` always win.
|
|
63
|
+
*/
|
|
64
|
+
thesaurus?: Thesaurus;
|
|
59
65
|
}
|
|
60
66
|
type JsonSchemaType = 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'null';
|
|
61
67
|
/**
|
|
@@ -71,7 +77,9 @@ type JsonSchemaType = 'object' | 'array' | 'string' | 'number' | 'integer' | 'bo
|
|
|
71
77
|
* choice `mandatory`; a property *not* in `required` becomes a `presence` node
|
|
72
78
|
* — absent from the form value until enabled — unless
|
|
73
79
|
* {@link JsonSchemaOptions.optionalPresence} is `false`. `title` becomes the
|
|
74
|
-
* label, `default`/`const` carry a scalar value
|
|
80
|
+
* label, `default`/`const` carry a scalar value, and
|
|
81
|
+
* {@link JsonSchemaOptions.thesaurus} fills labels/descriptions the schema
|
|
82
|
+
* does not author.
|
|
75
83
|
*/
|
|
76
84
|
export declare function jsonSchemaToNodeGroup(schema: JsonSchema, name?: string, options?: JsonSchemaOptions): NodeGroup;
|
|
77
85
|
export {};
|
package/dist/core/json-schema.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.jsonSchemaToNodeGroup = jsonSchemaToNodeGroup;
|
|
4
|
+
const thesaurus_1 = require("./thesaurus");
|
|
4
5
|
const ROOT = '__root__';
|
|
5
6
|
/**
|
|
6
7
|
* Map a JSON Schema whose top level is an object into a root {@link NodeGroup}.
|
|
@@ -15,7 +16,9 @@ const ROOT = '__root__';
|
|
|
15
16
|
* choice `mandatory`; a property *not* in `required` becomes a `presence` node
|
|
16
17
|
* — absent from the form value until enabled — unless
|
|
17
18
|
* {@link JsonSchemaOptions.optionalPresence} is `false`. `title` becomes the
|
|
18
|
-
* label, `default`/`const` carry a scalar value
|
|
19
|
+
* label, `default`/`const` carry a scalar value, and
|
|
20
|
+
* {@link JsonSchemaOptions.thesaurus} fills labels/descriptions the schema
|
|
21
|
+
* does not author.
|
|
19
22
|
*/
|
|
20
23
|
function jsonSchemaToNodeGroup(schema, name = ROOT, options) {
|
|
21
24
|
const documents = [schema, ...(options?.refDocuments ?? [])];
|
|
@@ -24,7 +27,7 @@ function jsonSchemaToNodeGroup(schema, name = ROOT, options) {
|
|
|
24
27
|
const { schema: root, scope } = resolver.resolve(schema);
|
|
25
28
|
const group = objectToNodeGroup(root, name, scope, ctx, root.title);
|
|
26
29
|
group.root = true;
|
|
27
|
-
return group;
|
|
30
|
+
return options?.thesaurus ? (0, thesaurus_1.applyThesaurus)(group, options.thesaurus) : group;
|
|
28
31
|
}
|
|
29
32
|
/**
|
|
30
33
|
* Apply {@link JsonSchemaOptions.optionalPresence} to one *property* node: a
|
package/dist/core/schema.d.ts
CHANGED
|
@@ -66,6 +66,7 @@ export interface NodeGroup {
|
|
|
66
66
|
*/
|
|
67
67
|
minPresent?: number;
|
|
68
68
|
maxPresent?: number;
|
|
69
|
+
description?: string;
|
|
69
70
|
children: Record<string, NodeType>;
|
|
70
71
|
}
|
|
71
72
|
export interface NodeGroupList {
|
|
@@ -122,3 +123,30 @@ export type NodeType = Leaf | LeafList | NodeGroup | NodeGroupList | Choice | No
|
|
|
122
123
|
export declare const CASE_KEY = "__case";
|
|
123
124
|
/** A plain, nested value object produced/consumed by a rendered form. */
|
|
124
125
|
export type FormValue = Record<string, unknown>;
|
|
126
|
+
/** Display metadata for one schema identifier (see {@link Thesaurus}). */
|
|
127
|
+
export interface ThesaurusEntry {
|
|
128
|
+
label?: string;
|
|
129
|
+
description?: string;
|
|
130
|
+
/**
|
|
131
|
+
* Ancestor-name suffix this variant applies under: each segment is a
|
|
132
|
+
* **literal name** (no separators exist, so any character is literal),
|
|
133
|
+
* matched case-insensitively against the identifier's ancestor chain.
|
|
134
|
+
* `['scope', 'cell']` applies to a node whose parent is `cell` inside
|
|
135
|
+
* `scope`, at any depth. Longest matching `under` wins; an entry without
|
|
136
|
+
* `under` is the unscoped fallback. Case fields match both with and without
|
|
137
|
+
* their case-name segment, so `['scope']` covers every case of a choice and
|
|
138
|
+
* `['scope', 'byUe']` targets one case.
|
|
139
|
+
*/
|
|
140
|
+
under?: string[];
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Identifier → display metadata, injected into a generated {@link NodeGroup}
|
|
144
|
+
* by `applyThesaurus`. Keys are **plain identifier names** matched
|
|
145
|
+
* **case-insensitively** against node record keys — never paths: no separator
|
|
146
|
+
* exists, so a `.` (or any character) in a key is a literal part of the name.
|
|
147
|
+
*
|
|
148
|
+
* A key maps to one entry, or to a list of variants scoped by
|
|
149
|
+
* {@link ThesaurusEntry.under} when the same identifier carries different
|
|
150
|
+
* meanings at different depths.
|
|
151
|
+
*/
|
|
152
|
+
export type Thesaurus = Record<string, ThesaurusEntry | ThesaurusEntry[]>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A thesaurus: human-readable metadata for schema identifiers, injected into a
|
|
3
|
+
* generated {@link NodeGroup} as `label`/`description`/`caseLabels`.
|
|
4
|
+
*
|
|
5
|
+
* Machine schemas (O-RAN A1 policy types, inferred configs) ship without
|
|
6
|
+
* titles, so forms fall back to raw attribute names (`guRanUeId`, `mcc`).
|
|
7
|
+
* A thesaurus maps those identifiers to display metadata once, and every
|
|
8
|
+
* transformer entry point applies it to whatever schema it produced —
|
|
9
|
+
* JSON-Schema-driven or inferred alike.
|
|
10
|
+
*
|
|
11
|
+
* Keys are **plain identifier names**, matched **case-insensitively** against
|
|
12
|
+
* each node's record key (its property/setting name). They are never paths:
|
|
13
|
+
* no separator exists, so a name containing `.`/`/` or any other character is
|
|
14
|
+
* matched verbatim — one entry covers the identifier wherever it appears in
|
|
15
|
+
* the tree. When the same identifier means different things at different
|
|
16
|
+
* depths, a key maps to a list of variants scoped by
|
|
17
|
+
* {@link ThesaurusEntry.under} (an ancestor-name suffix); the longest
|
|
18
|
+
* matching scope wins, and an unscoped variant is the fallback.
|
|
19
|
+
*/
|
|
20
|
+
import type { NodeGroup, Thesaurus } from './schema';
|
|
21
|
+
/**
|
|
22
|
+
* Return a copy of `schema` with thesaurus metadata filled in. Existing
|
|
23
|
+
* `label`/`description`/`caseLabels` values are respected, never overwritten —
|
|
24
|
+
* the thesaurus fills gaps, it does not restyle authored schemas.
|
|
25
|
+
*
|
|
26
|
+
* Coverage: every node reachable from the root (group children, list item
|
|
27
|
+
* fields, map value templates, choice case fields). Scoping follows the
|
|
28
|
+
* **data hierarchy of names**: fields inside list items scope under the list
|
|
29
|
+
* name (indices are transparent), map value fields under the map name
|
|
30
|
+
* (runtime keys are transparent), and choice case fields match both with and
|
|
31
|
+
* without their case-name segment — `under: ['scope']` covers every case of
|
|
32
|
+
* the `scope` choice, `under: ['scope', 'byUe']` targets one case (scoping by
|
|
33
|
+
* an auto-generated `case0`-style name works but is positional and brittle).
|
|
34
|
+
*
|
|
35
|
+
* A choice case with no label is titled from its discriminating field — the
|
|
36
|
+
* first `required` field (else the first field) whose thesaurus match carries
|
|
37
|
+
* a `label` (description-only entries do not title cases).
|
|
38
|
+
* Sibling cases may end up with the same label when their required sets
|
|
39
|
+
* coincide; the library's `caseDisplayLabels` disambiguates colliding labels
|
|
40
|
+
* in the selectors by each case's distinguishing fields, using the field
|
|
41
|
+
* labels injected here.
|
|
42
|
+
*/
|
|
43
|
+
export declare function applyThesaurus<G extends NodeGroup>(schema: G, thesaurus: Thesaurus): G;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applyThesaurus = applyThesaurus;
|
|
4
|
+
/**
|
|
5
|
+
* Return a copy of `schema` with thesaurus metadata filled in. Existing
|
|
6
|
+
* `label`/`description`/`caseLabels` values are respected, never overwritten —
|
|
7
|
+
* the thesaurus fills gaps, it does not restyle authored schemas.
|
|
8
|
+
*
|
|
9
|
+
* Coverage: every node reachable from the root (group children, list item
|
|
10
|
+
* fields, map value templates, choice case fields). Scoping follows the
|
|
11
|
+
* **data hierarchy of names**: fields inside list items scope under the list
|
|
12
|
+
* name (indices are transparent), map value fields under the map name
|
|
13
|
+
* (runtime keys are transparent), and choice case fields match both with and
|
|
14
|
+
* without their case-name segment — `under: ['scope']` covers every case of
|
|
15
|
+
* the `scope` choice, `under: ['scope', 'byUe']` targets one case (scoping by
|
|
16
|
+
* an auto-generated `case0`-style name works but is positional and brittle).
|
|
17
|
+
*
|
|
18
|
+
* A choice case with no label is titled from its discriminating field — the
|
|
19
|
+
* first `required` field (else the first field) whose thesaurus match carries
|
|
20
|
+
* a `label` (description-only entries do not title cases).
|
|
21
|
+
* Sibling cases may end up with the same label when their required sets
|
|
22
|
+
* coincide; the library's `caseDisplayLabels` disambiguates colliding labels
|
|
23
|
+
* in the selectors by each case's distinguishing fields, using the field
|
|
24
|
+
* labels injected here.
|
|
25
|
+
*/
|
|
26
|
+
function applyThesaurus(schema, thesaurus) {
|
|
27
|
+
const lookup = new Map();
|
|
28
|
+
for (const [key, value] of Object.entries(thesaurus)) {
|
|
29
|
+
const variants = (Array.isArray(value) ? value : [value]).map((entry, order) => ({
|
|
30
|
+
under: (entry.under ?? []).map((segment) => segment.toLowerCase()),
|
|
31
|
+
entry,
|
|
32
|
+
order,
|
|
33
|
+
}));
|
|
34
|
+
lookup.set(key.toLowerCase(), variants);
|
|
35
|
+
}
|
|
36
|
+
const out = structuredClone(schema);
|
|
37
|
+
decorate(out, out.name, [[]], lookup);
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The best-scoped entry for `name` given the node's applicable ancestor
|
|
42
|
+
* chains: the variant with the longest `under` that is a suffix of any chain
|
|
43
|
+
* (an entry without `under` always applies); equal lengths keep author order.
|
|
44
|
+
*/
|
|
45
|
+
function resolve(name, chains, lookup) {
|
|
46
|
+
const variants = lookup.get(name.toLowerCase());
|
|
47
|
+
if (!variants)
|
|
48
|
+
return undefined;
|
|
49
|
+
let best;
|
|
50
|
+
for (const v of variants) {
|
|
51
|
+
if (!chains.some((chain) => isSuffix(v.under, chain)))
|
|
52
|
+
continue;
|
|
53
|
+
if (!best || v.under.length > best.under.length)
|
|
54
|
+
best = v;
|
|
55
|
+
}
|
|
56
|
+
return best?.entry;
|
|
57
|
+
}
|
|
58
|
+
function isSuffix(under, chain) {
|
|
59
|
+
if (under.length > chain.length)
|
|
60
|
+
return false;
|
|
61
|
+
const tail = chain.slice(chain.length - under.length);
|
|
62
|
+
return under.every((segment, i) => segment === tail[i]);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* `transparentKey` marks a structural hop that contributes no ancestor
|
|
66
|
+
* segment: a map's `value` template is not a name in the data — entry fields
|
|
67
|
+
* scope under the map's own name.
|
|
68
|
+
*/
|
|
69
|
+
function decorate(node, key, chains, lookup, transparentKey = false) {
|
|
70
|
+
const entry = resolve(key, chains, lookup) ?? (key === node.name ? undefined : resolve(node.name, chains, lookup));
|
|
71
|
+
if (entry) {
|
|
72
|
+
if (entry.label !== undefined && node.label === undefined)
|
|
73
|
+
node.label = entry.label;
|
|
74
|
+
if (entry.description !== undefined &&
|
|
75
|
+
(node.kind === 'leaf' || node.kind === 'nodeGroup' || node.kind === 'map') &&
|
|
76
|
+
node.description === undefined) {
|
|
77
|
+
node.description = entry.description;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Children's ancestor chains gain this node's key. List items and map
|
|
81
|
+
// entries add no segment of their own — indices and runtime keys are not
|
|
82
|
+
// names — so their fields scope under the list/map name directly.
|
|
83
|
+
const childChains = transparentKey ? chains : chains.map((chain) => [...chain, key.toLowerCase()]);
|
|
84
|
+
switch (node.kind) {
|
|
85
|
+
case 'nodeGroup':
|
|
86
|
+
for (const [childKey, child] of Object.entries(node.children))
|
|
87
|
+
decorate(child, childKey, childChains, lookup);
|
|
88
|
+
return;
|
|
89
|
+
case 'nodeGroupList':
|
|
90
|
+
for (const [childKey, child] of Object.entries(node.type.children))
|
|
91
|
+
decorate(child, childKey, childChains, lookup);
|
|
92
|
+
return;
|
|
93
|
+
case 'map':
|
|
94
|
+
decorate(node.value, node.value.name, childChains, lookup, true);
|
|
95
|
+
return;
|
|
96
|
+
case 'choice': {
|
|
97
|
+
for (const [caseName, body] of Object.entries(node.cases)) {
|
|
98
|
+
// Case fields match with and without the case segment: the wire
|
|
99
|
+
// encoding is inline (case-transparent), while the extra chain lets a
|
|
100
|
+
// scope target one case of the choice.
|
|
101
|
+
const caseChains = [...childChains, ...childChains.map((c) => [...c, caseName.toLowerCase()])];
|
|
102
|
+
const fields = caseFieldRecord(body);
|
|
103
|
+
for (const [fieldKey, field] of Object.entries(fields))
|
|
104
|
+
decorate(field, fieldKey, caseChains, lookup);
|
|
105
|
+
if (node.caseLabels?.[caseName] !== undefined)
|
|
106
|
+
continue;
|
|
107
|
+
// The discriminant must carry a LABEL — a description-only entry on an
|
|
108
|
+
// earlier field must not block a labeled sibling from titling the case.
|
|
109
|
+
const keys = Object.keys(fields);
|
|
110
|
+
const labelOf = (k) => resolve(k, caseChains, lookup)?.label;
|
|
111
|
+
const discriminant = keys.find((k) => fields[k].required && labelOf(k) !== undefined) ??
|
|
112
|
+
keys.find((k) => labelOf(k) !== undefined);
|
|
113
|
+
const label = discriminant ? labelOf(discriminant) : undefined;
|
|
114
|
+
if (label !== undefined)
|
|
115
|
+
node.caseLabels = { ...(node.caseLabels ?? {}), [caseName]: label };
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
default:
|
|
120
|
+
return; // leaf / leafList: nothing beneath
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** A case body as a field record (a leaf-bodied case is a one-field record keyed by its name). */
|
|
124
|
+
function caseFieldRecord(body) {
|
|
125
|
+
return typeof body.kind === 'string'
|
|
126
|
+
? { [body.name]: body }
|
|
127
|
+
: body;
|
|
128
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FormValue } from '../../core/schema';
|
|
1
|
+
import type { FormValue, Thesaurus } from '../../core/schema';
|
|
2
2
|
import type { TransformResult } from '../../core/transformer';
|
|
3
3
|
import { type JsonSchema, type JsonSchemaOptions } from '../../core/json-schema';
|
|
4
4
|
/** Options for {@link jsonTransformer}'s `toSchema`. */
|
|
@@ -12,6 +12,13 @@ export interface JsonOptions {
|
|
|
12
12
|
rootName?: string;
|
|
13
13
|
/** Options forwarded to `jsonSchemaToNodeGroup` (`refDocuments`, `optionalPresence`). */
|
|
14
14
|
schemaOptions?: JsonSchemaOptions;
|
|
15
|
+
/**
|
|
16
|
+
* Display metadata (`label`/`description`/choice `caseLabels`) injected into
|
|
17
|
+
* the produced schema, schema-driven or inferred alike — see
|
|
18
|
+
* `applyThesaurus`. Keys are plain identifier names, matched
|
|
19
|
+
* case-insensitively; never paths.
|
|
20
|
+
*/
|
|
21
|
+
thesaurus?: Thesaurus;
|
|
15
22
|
}
|
|
16
23
|
/** How the source was formatted, so `toSource` re-emits it the same way. */
|
|
17
24
|
export interface JsonFormat {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.jsonTransformer = void 0;
|
|
4
|
+
const thesaurus_1 = require("../../core/thesaurus");
|
|
4
5
|
const infer_1 = require("../../core/infer");
|
|
5
6
|
const json_schema_1 = require("../../core/json-schema");
|
|
6
7
|
const bigint_1 = require("../../core/bigint");
|
|
@@ -30,12 +31,13 @@ exports.jsonTransformer = {
|
|
|
30
31
|
const schema = options?.schema
|
|
31
32
|
? (0, json_schema_1.jsonSchemaToNodeGroup)(options.schema, options.rootName, options.schemaOptions)
|
|
32
33
|
: (0, infer_1.inferNodeGroup)(data, options?.rootName);
|
|
34
|
+
const labeled = options?.thesaurus ? (0, thesaurus_1.applyThesaurus)(schema, options.thesaurus) : schema;
|
|
33
35
|
const binding = {
|
|
34
36
|
indent: detectIndent(source),
|
|
35
37
|
trailingNewline: source.endsWith('\n'),
|
|
36
38
|
bigInts,
|
|
37
39
|
};
|
|
38
|
-
return { schema, binding, initialValue: data };
|
|
40
|
+
return { schema: labeled, binding, initialValue: data };
|
|
39
41
|
},
|
|
40
42
|
toSource(value, binding) {
|
|
41
43
|
const paths = new Set(binding.bigInts);
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* are then shown read-only. A JSON Schema in the options unlocks typed empty
|
|
13
13
|
* collections, presence toggles for optional settings, enums, and ranges.
|
|
14
14
|
*/
|
|
15
|
-
import type { FormValue } from '../../core/schema';
|
|
15
|
+
import type { FormValue, Thesaurus } from '../../core/schema';
|
|
16
16
|
import type { TransformResult } from '../../core/transformer';
|
|
17
17
|
import { type JsonSchema, type JsonSchemaOptions } from '../../core/json-schema';
|
|
18
18
|
import { type CfgGroup } from './parser';
|
|
@@ -34,6 +34,13 @@ export interface LibconfigOptions {
|
|
|
34
34
|
* line verbatim and edits only this file's own settings.
|
|
35
35
|
*/
|
|
36
36
|
includes?: 'reject' | 'opaque';
|
|
37
|
+
/**
|
|
38
|
+
* Display metadata (`label`/`description`/choice `caseLabels`) injected into
|
|
39
|
+
* the produced schema, schema-driven or inferred alike — see
|
|
40
|
+
* `applyThesaurus`. Keys are plain identifier names, matched
|
|
41
|
+
* case-insensitively; never paths.
|
|
42
|
+
*/
|
|
43
|
+
thesaurus?: Thesaurus;
|
|
37
44
|
}
|
|
38
45
|
/** The revert context: the original text and its positioned AST. */
|
|
39
46
|
export interface LibconfigBinding {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.libconfigTransformer = void 0;
|
|
4
4
|
exports.resetLibconfigBetaWarning = resetLibconfigBetaWarning;
|
|
5
|
+
const thesaurus_1 = require("../../core/thesaurus");
|
|
5
6
|
const json_schema_1 = require("../../core/json-schema");
|
|
6
7
|
const parser_1 = require("./parser");
|
|
7
8
|
const schema_1 = require("./schema");
|
|
@@ -26,8 +27,9 @@ exports.libconfigTransformer = {
|
|
|
26
27
|
const schema = options?.schema
|
|
27
28
|
? (0, json_schema_1.jsonSchemaToNodeGroup)(options.schema, options.rootName, options.schemaOptions)
|
|
28
29
|
: (0, schema_1.libconfigToNodeGroup)(root, source, options?.rootName ?? '__root__');
|
|
30
|
+
const labeled = options?.thesaurus ? (0, thesaurus_1.applyThesaurus)(schema, options.thesaurus) : schema;
|
|
29
31
|
const initialValue = (0, schema_1.extractValue)(root, source, options?.schema != null);
|
|
30
|
-
return { schema, binding: { source, root }, initialValue };
|
|
32
|
+
return { schema: labeled, binding: { source, root }, initialValue };
|
|
31
33
|
},
|
|
32
34
|
toSource(value, binding) {
|
|
33
35
|
warnBeta();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Document } from 'yaml';
|
|
2
|
-
import type { FormValue } from '../../core/schema';
|
|
2
|
+
import type { FormValue, 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`. */
|
|
@@ -14,6 +14,13 @@ export interface YamlOptions {
|
|
|
14
14
|
rootName?: string;
|
|
15
15
|
/** Options forwarded to `jsonSchemaToNodeGroup` (`refDocuments`, `optionalPresence`). */
|
|
16
16
|
schemaOptions?: JsonSchemaOptions;
|
|
17
|
+
/**
|
|
18
|
+
* Display metadata (`label`/`description`/choice `caseLabels`) injected into
|
|
19
|
+
* the produced schema, schema-driven or inferred alike — see
|
|
20
|
+
* `applyThesaurus`. Keys are plain identifier names, matched
|
|
21
|
+
* case-insensitively; never paths.
|
|
22
|
+
*/
|
|
23
|
+
thesaurus?: Thesaurus;
|
|
17
24
|
}
|
|
18
25
|
/**
|
|
19
26
|
* A YAML {@link Transformer}: turn a YAML config document into a form and write
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.yamlTransformer = void 0;
|
|
4
4
|
const yaml_1 = require("yaml");
|
|
5
|
+
const thesaurus_1 = require("../../core/thesaurus");
|
|
5
6
|
const infer_1 = require("../../core/infer");
|
|
6
7
|
const json_schema_1 = require("../../core/json-schema");
|
|
7
8
|
const bigint_1 = require("../../core/bigint");
|
|
@@ -29,7 +30,8 @@ exports.yamlTransformer = {
|
|
|
29
30
|
const schema = options?.schema
|
|
30
31
|
? (0, json_schema_1.jsonSchemaToNodeGroup)(options.schema, options.rootName, options.schemaOptions)
|
|
31
32
|
: (0, infer_1.inferNodeGroup)(data, options?.rootName);
|
|
32
|
-
|
|
33
|
+
const labeled = options?.thesaurus ? (0, thesaurus_1.applyThesaurus)(schema, options.thesaurus) : schema;
|
|
34
|
+
return { schema: labeled, binding: doc, initialValue: data };
|
|
33
35
|
},
|
|
34
36
|
toSource(value, binding) {
|
|
35
37
|
const doc = binding.clone();
|
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.5",
|
|
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",
|