ng-form-foundry-transformers 0.3.3 → 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 +120 -2
- 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 +11 -2
- package/dist/core/schema.d.ts +35 -0
- package/dist/core/thesaurus.d.ts +43 -0
- package/dist/core/thesaurus.js +128 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/transformers/json/json-transformer.d.ts +8 -1
- package/dist/transformers/json/json-transformer.js +3 -1
- package/dist/transformers/libconfig/index.d.ts +2 -0
- package/dist/transformers/libconfig/index.js +9 -0
- package/dist/transformers/libconfig/libconfig-transformer.d.ts +56 -0
- package/dist/transformers/libconfig/libconfig-transformer.js +38 -0
- package/dist/transformers/libconfig/parser.d.ts +90 -0
- package/dist/transformers/libconfig/parser.js +296 -0
- package/dist/transformers/libconfig/revert.d.ts +18 -0
- package/dist/transformers/libconfig/revert.js +243 -0
- package/dist/transformers/libconfig/schema.d.ts +39 -0
- package/dist/transformers/libconfig/schema.js +163 -0
- 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
|
@@ -16,6 +16,7 @@ or a CLI just as easily.
|
|
|
16
16
|
| `yang` | YANG model (via an engine) | RFC 7951 instance data | available |
|
|
17
17
|
| `yaml` | YAML config (optionally a JSON Schema) | YAML (comments preserved) | available |
|
|
18
18
|
| `json` | JSON config (optionally a JSON Schema) | JSON (indent preserved) | available |
|
|
19
|
+
| `libconfig` | libconfig document (optionally a JSON Schema) | libconfig (comments **and scalar types** preserved) | **beta** — warns on first use |
|
|
19
20
|
|
|
20
21
|
The YAML and JSON transformers share the same format-agnostic form builders in
|
|
21
22
|
`core` (`inferNodeGroup`, `jsonSchemaToNodeGroup`) — a JSON Schema is an *option*
|
|
@@ -49,6 +50,69 @@ the library's `serializeForm(schema, form)`, which strips the `__case`
|
|
|
49
50
|
discriminators from `getRawValue()`. The YANG adapter is the exception: its
|
|
50
51
|
`toYangData` consumes the form value and flattens `__case` itself.
|
|
51
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
|
+
|
|
52
116
|
## YAML transformer
|
|
53
117
|
|
|
54
118
|
Edit a **YAML config file**: turn it into a form, then write the edited value
|
|
@@ -82,8 +146,11 @@ leafList, `anyOf`/`oneOf` → choice (or a nullable leaf for `[T, null]`), `$ref
|
|
|
82
146
|
`$defs`/`definitions` resolved inline (local **or cross-file** via
|
|
83
147
|
`schemaOptions.refDocuments`, matched by `$id`), `const` → a read-only leaf, and the
|
|
84
148
|
string / number constraints (`pattern`, `minLength`, `minimum`, `multipleOf`,
|
|
85
|
-
`format`, …) carried onto the leaves as validators
|
|
86
|
-
|
|
149
|
+
`format`, …) carried onto the leaves as validators, with
|
|
150
|
+
`minProperties`/`maxProperties` becoming present-children bounds on closed
|
|
151
|
+
objects (`minPresent`/`maxPresent`) or entry bounds on open maps.
|
|
152
|
+
Non-`required` properties become **`presence` nodes** — absent from the form
|
|
153
|
+
and the serialized value until
|
|
87
154
|
enabled, so the output validates against the source schema (opt out with
|
|
88
155
|
`schemaOptions: { optionalPresence: false }`); a required property mapping to a
|
|
89
156
|
choice is marked `mandatory`.
|
|
@@ -102,6 +169,42 @@ const { schema, binding, initialValue } = jsonTransformer.toSchema(configText);
|
|
|
102
169
|
const out = jsonTransformer.toSource({ ...initialValue, replicas: 5 }, binding);
|
|
103
170
|
```
|
|
104
171
|
|
|
172
|
+
## libconfig transformer (beta)
|
|
173
|
+
|
|
174
|
+
Edit a **libconfig document** — the `.cfg`/`.conf` format of srsRAN, OAI, and
|
|
175
|
+
other C/C++ software using [libconfig](https://hyperrealm.github.io/libconfig/).
|
|
176
|
+
**Beta**: it logs a one-time warning on first use; diff `toSource` output
|
|
177
|
+
against the original before deploying a write-back.
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import { libconfigTransformer } from 'ng-form-foundry-transformers';
|
|
181
|
+
|
|
182
|
+
const { schema, binding, initialValue } = libconfigTransformer.toSchema(cfgText);
|
|
183
|
+
// … build the form, let the user edit …
|
|
184
|
+
const editedCfg = libconfigTransformer.toSource(editedValue, binding);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The revert **splices edited spans into the original text**, so comments, key
|
|
188
|
+
order, `=` vs `:`, terminators, and delimiter style survive verbatim on every
|
|
189
|
+
unedited byte. Emission is **type-preserving** — libconfig is statically typed,
|
|
190
|
+
so a float slot edited to an integral value emits `21.0` not `21`, hex re-emits
|
|
191
|
+
in hex at its original width, an int64 keeps its `L` suffix, and values beyond
|
|
192
|
+
2^53 travel as exact decimal strings (the same bigint strategy as YAML/JSON).
|
|
193
|
+
|
|
194
|
+
Without a JSON Schema, the form is inferred from the document's own typed
|
|
195
|
+
literals; a list of groups infers the union of entry keys (keys missing from
|
|
196
|
+
some entries become presence toggles). **Empty and heterogeneous collections
|
|
197
|
+
are then shown read-only** — libconfig gives no element type to edit by; pass a
|
|
198
|
+
JSON Schema (`{ schema, schemaOptions? }`) to type them and make them editable.
|
|
199
|
+
`@include` is rejected by default (the form would show less than the C reader
|
|
200
|
+
sees); `{ includes: 'opaque' }` keeps the directive line verbatim and edits only
|
|
201
|
+
the file's own settings. Newly added settings are value-typed (an integral
|
|
202
|
+
number emits as an int) — route float-typed additions through a JSON Schema
|
|
203
|
+
slot that exists in the source, or accept the int typing.
|
|
204
|
+
|
|
205
|
+
The format, the parser design, and the full round-trip semantics are described
|
|
206
|
+
in [`docs/libconfig.md`](docs/libconfig.md).
|
|
207
|
+
|
|
105
208
|
## YANG transformer
|
|
106
209
|
|
|
107
210
|
Turn a **YANG** model into an ng-form-foundry schema, then revert the edited form
|
|
@@ -191,6 +294,21 @@ npm test # node:test on the compiled output (no Python needed)
|
|
|
191
294
|
|
|
192
295
|
## Status
|
|
193
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
|
+
|
|
304
|
+
`0.3.4` — the **libconfig transformer debuts as a beta** (one-time console
|
|
305
|
+
warning on first use; lossless span-splicing round-trip with scalar types
|
|
306
|
+
preserved — see [`docs/libconfig.md`](docs/libconfig.md)), and
|
|
307
|
+
`minProperties`/`maxProperties` on closed objects now map to the library's
|
|
308
|
+
`minPresent`/`maxPresent` present-children bounds, so an emptied
|
|
309
|
+
all-optional object (e.g. an A1 `qosObjectives`) is flagged client-side
|
|
310
|
+
instead of only by the server's validator.
|
|
311
|
+
|
|
194
312
|
`0.3.3` — schema-valid round-trips for JSON-Schema-driven forms. Non-`required`
|
|
195
313
|
properties now map to `presence` nodes (absent until enabled; opt out with
|
|
196
314
|
`schemaOptions: { optionalPresence: false }`), a required property mapping to a
|
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
|
|
@@ -110,6 +113,12 @@ function objectToNodeGroup(schema, name, resolver, ctx, label) {
|
|
|
110
113
|
const group = { kind: 'nodeGroup', name, children };
|
|
111
114
|
if (label)
|
|
112
115
|
group.label = label;
|
|
116
|
+
// On a closed object these bound the count of *present* keys — meaningful
|
|
117
|
+
// because non-required properties are presence nodes (optionalPresence).
|
|
118
|
+
if (typeof schema.minProperties === 'number')
|
|
119
|
+
group.minPresent = schema.minProperties;
|
|
120
|
+
if (typeof schema.maxProperties === 'number')
|
|
121
|
+
group.maxPresent = schema.maxProperties;
|
|
113
122
|
return group;
|
|
114
123
|
}
|
|
115
124
|
/** An `object` with open keys — `additionalProperties: <schema>` or `patternProperties`, and no fixed `properties`. */
|
package/dist/core/schema.d.ts
CHANGED
|
@@ -59,6 +59,14 @@ export interface NodeGroup {
|
|
|
59
59
|
* serializes as `{}`, absent is omitted.
|
|
60
60
|
*/
|
|
61
61
|
presence?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Minimum / maximum number of keys present in the group's value (JSON Schema
|
|
64
|
+
* `minProperties`/`maxProperties` on a closed object with presence-optional
|
|
65
|
+
* children); the library validates the enabled-children count against them.
|
|
66
|
+
*/
|
|
67
|
+
minPresent?: number;
|
|
68
|
+
maxPresent?: number;
|
|
69
|
+
description?: string;
|
|
62
70
|
children: Record<string, NodeType>;
|
|
63
71
|
}
|
|
64
72
|
export interface NodeGroupList {
|
|
@@ -115,3 +123,30 @@ export type NodeType = Leaf | LeafList | NodeGroup | NodeGroupList | Choice | No
|
|
|
115
123
|
export declare const CASE_KEY = "__case";
|
|
116
124
|
/** A plain, nested value object produced/consumed by a rendered form. */
|
|
117
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
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* driven or inferred from the data, comment-preserving on revert.
|
|
13
13
|
* - `json` — JSON config → form → JSON ({@link jsonTransformer}); same builders
|
|
14
14
|
* as `yaml`, indent preserved.
|
|
15
|
+
* - `libconfig` — libconfig document (srsRAN/OAI-style `.cfg`/`.conf`) → form →
|
|
16
|
+
* libconfig ({@link libconfigTransformer}); **BETA**, warns once on
|
|
17
|
+
* first use; comment- and type-preserving span splicing on revert.
|
|
15
18
|
*
|
|
16
19
|
* Look transformers up at runtime through {@link TransformerRegistry}, or import
|
|
17
20
|
* the one you need directly.
|
|
@@ -20,3 +23,4 @@ export * from './core';
|
|
|
20
23
|
export * from './transformers/yang';
|
|
21
24
|
export * from './transformers/yaml';
|
|
22
25
|
export * from './transformers/json';
|
|
26
|
+
export * from './transformers/libconfig';
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
* driven or inferred from the data, comment-preserving on revert.
|
|
14
14
|
* - `json` — JSON config → form → JSON ({@link jsonTransformer}); same builders
|
|
15
15
|
* as `yaml`, indent preserved.
|
|
16
|
+
* - `libconfig` — libconfig document (srsRAN/OAI-style `.cfg`/`.conf`) → form →
|
|
17
|
+
* libconfig ({@link libconfigTransformer}); **BETA**, warns once on
|
|
18
|
+
* first use; comment- and type-preserving span splicing on revert.
|
|
16
19
|
*
|
|
17
20
|
* Look transformers up at runtime through {@link TransformerRegistry}, or import
|
|
18
21
|
* the one you need directly.
|
|
@@ -38,3 +41,4 @@ __exportStar(require("./core"), exports);
|
|
|
38
41
|
__exportStar(require("./transformers/yang"), exports);
|
|
39
42
|
__exportStar(require("./transformers/yaml"), exports);
|
|
40
43
|
__exportStar(require("./transformers/json"), exports);
|
|
44
|
+
__exportStar(require("./transformers/libconfig"), exports); // BETA — warns once on first use
|
|
@@ -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);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LibconfigParseError = exports.parseLibconfig = exports.resetLibconfigBetaWarning = exports.libconfigTransformer = void 0;
|
|
4
|
+
var libconfig_transformer_1 = require("./libconfig-transformer");
|
|
5
|
+
Object.defineProperty(exports, "libconfigTransformer", { enumerable: true, get: function () { return libconfig_transformer_1.libconfigTransformer; } });
|
|
6
|
+
Object.defineProperty(exports, "resetLibconfigBetaWarning", { enumerable: true, get: function () { return libconfig_transformer_1.resetLibconfigBetaWarning; } });
|
|
7
|
+
var parser_1 = require("./parser");
|
|
8
|
+
Object.defineProperty(exports, "parseLibconfig", { enumerable: true, get: function () { return parser_1.parseLibconfig; } });
|
|
9
|
+
Object.defineProperty(exports, "LibconfigParseError", { enumerable: true, get: function () { return parser_1.LibconfigParseError; } });
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A libconfig {@link Transformer} (BETA): turn a libconfig document
|
|
3
|
+
* (srsRAN/OAI-style `.cfg`/`.conf`) into a form and write the edited value
|
|
4
|
+
* back with comments, formatting, and — critically — scalar *types*
|
|
5
|
+
* preserved (libconfig is statically typed; see {@link import('./revert')}).
|
|
6
|
+
*
|
|
7
|
+
* The `binding` is the original source plus its positioned AST; `toSource`
|
|
8
|
+
* splices edited spans into the source, so unedited bytes survive verbatim.
|
|
9
|
+
*
|
|
10
|
+
* Without a JSON Schema the form is inferred from the document's own typed
|
|
11
|
+
* literals ({@link import('./schema')}); empty and heterogeneous collections
|
|
12
|
+
* are then shown read-only. A JSON Schema in the options unlocks typed empty
|
|
13
|
+
* collections, presence toggles for optional settings, enums, and ranges.
|
|
14
|
+
*/
|
|
15
|
+
import type { FormValue, Thesaurus } from '../../core/schema';
|
|
16
|
+
import type { TransformResult } from '../../core/transformer';
|
|
17
|
+
import { type JsonSchema, type JsonSchemaOptions } from '../../core/json-schema';
|
|
18
|
+
import { type CfgGroup } from './parser';
|
|
19
|
+
/** Options for {@link libconfigTransformer}'s `toSchema`. */
|
|
20
|
+
export interface LibconfigOptions {
|
|
21
|
+
/**
|
|
22
|
+
* A JSON Schema describing the config. When given, the form is built from it
|
|
23
|
+
* (types, required/presence, enums, empty-collection element types); when
|
|
24
|
+
* omitted, the form is inferred from the document's typed literals.
|
|
25
|
+
*/
|
|
26
|
+
schema?: JsonSchema;
|
|
27
|
+
/** Name for the root node group. Defaults to `__root__`. */
|
|
28
|
+
rootName?: string;
|
|
29
|
+
/** Options forwarded to `jsonSchemaToNodeGroup` (`refDocuments`, `optionalPresence`). */
|
|
30
|
+
schemaOptions?: JsonSchemaOptions;
|
|
31
|
+
/**
|
|
32
|
+
* `@include` handling: `'reject'` (default) errors, because the form would
|
|
33
|
+
* silently show less than the C reader sees; `'opaque'` keeps the directive
|
|
34
|
+
* line verbatim and edits only this file's own settings.
|
|
35
|
+
*/
|
|
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;
|
|
44
|
+
}
|
|
45
|
+
/** The revert context: the original text and its positioned AST. */
|
|
46
|
+
export interface LibconfigBinding {
|
|
47
|
+
source: string;
|
|
48
|
+
root: CfgGroup;
|
|
49
|
+
}
|
|
50
|
+
/** Test seam: makes the one-time beta warning observable per test. */
|
|
51
|
+
export declare function resetLibconfigBetaWarning(): void;
|
|
52
|
+
export declare const libconfigTransformer: {
|
|
53
|
+
id: string;
|
|
54
|
+
toSchema(source: string, options?: LibconfigOptions): TransformResult<LibconfigBinding>;
|
|
55
|
+
toSource(value: FormValue, binding: LibconfigBinding): string;
|
|
56
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.libconfigTransformer = void 0;
|
|
4
|
+
exports.resetLibconfigBetaWarning = resetLibconfigBetaWarning;
|
|
5
|
+
const thesaurus_1 = require("../../core/thesaurus");
|
|
6
|
+
const json_schema_1 = require("../../core/json-schema");
|
|
7
|
+
const parser_1 = require("./parser");
|
|
8
|
+
const schema_1 = require("./schema");
|
|
9
|
+
const revert_1 = require("./revert");
|
|
10
|
+
let warnedBeta = false;
|
|
11
|
+
function warnBeta() {
|
|
12
|
+
if (warnedBeta)
|
|
13
|
+
return;
|
|
14
|
+
warnedBeta = true;
|
|
15
|
+
console.warn('[ng-form-foundry-transformers] The libconfig transformer is a BETA feature. ' +
|
|
16
|
+
'Verify every write-back (diff toSource output against the original file) before deploying it.');
|
|
17
|
+
}
|
|
18
|
+
/** Test seam: makes the one-time beta warning observable per test. */
|
|
19
|
+
function resetLibconfigBetaWarning() {
|
|
20
|
+
warnedBeta = false;
|
|
21
|
+
}
|
|
22
|
+
exports.libconfigTransformer = {
|
|
23
|
+
id: 'libconfig',
|
|
24
|
+
toSchema(source, options) {
|
|
25
|
+
warnBeta();
|
|
26
|
+
const root = (0, parser_1.parseLibconfig)(source, { includes: options?.includes });
|
|
27
|
+
const schema = options?.schema
|
|
28
|
+
? (0, json_schema_1.jsonSchemaToNodeGroup)(options.schema, options.rootName, options.schemaOptions)
|
|
29
|
+
: (0, schema_1.libconfigToNodeGroup)(root, source, options?.rootName ?? '__root__');
|
|
30
|
+
const labeled = options?.thesaurus ? (0, thesaurus_1.applyThesaurus)(schema, options.thesaurus) : schema;
|
|
31
|
+
const initialValue = (0, schema_1.extractValue)(root, source, options?.schema != null);
|
|
32
|
+
return { schema: labeled, binding: { source, root }, initialValue };
|
|
33
|
+
},
|
|
34
|
+
toSource(value, binding) {
|
|
35
|
+
warnBeta();
|
|
36
|
+
return (0, revert_1.applyValueToSource)(binding.source, binding.root, value);
|
|
37
|
+
},
|
|
38
|
+
};
|