ng-form-foundry-transformers 0.3.2 → 0.3.4
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 +64 -3
- package/dist/core/json-schema.d.ts +14 -2
- package/dist/core/json-schema.js +47 -17
- package/dist/core/schema.d.ts +10 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/transformers/json/json-transformer.d.ts +3 -1
- package/dist/transformers/json/json-transformer.js +1 -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 +49 -0
- package/dist/transformers/libconfig/libconfig-transformer.js +36 -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 +3 -1
- package/dist/transformers/yaml/yaml-transformer.js +1 -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*
|
|
@@ -80,9 +81,16 @@ covers **draft 2020-12** (back-compatible with draft-07): `object` → nodeGroup
|
|
|
80
81
|
a `map` for `additionalProperties`/`patternProperties`), `array` → nodeGroupList /
|
|
81
82
|
leafList, `anyOf`/`oneOf` → choice (or a nullable leaf for `[T, null]`), `$ref` →
|
|
82
83
|
`$defs`/`definitions` resolved inline (local **or cross-file** via
|
|
83
|
-
`
|
|
84
|
-
number constraints (`pattern`, `minLength`, `minimum`, `multipleOf`,
|
|
85
|
-
carried onto the leaves as validators
|
|
84
|
+
`schemaOptions.refDocuments`, matched by `$id`), `const` → a read-only leaf, and the
|
|
85
|
+
string / number constraints (`pattern`, `minLength`, `minimum`, `multipleOf`,
|
|
86
|
+
`format`, …) carried onto the leaves as validators, with
|
|
87
|
+
`minProperties`/`maxProperties` becoming present-children bounds on closed
|
|
88
|
+
objects (`minPresent`/`maxPresent`) or entry bounds on open maps.
|
|
89
|
+
Non-`required` properties become **`presence` nodes** — absent from the form
|
|
90
|
+
and the serialized value until
|
|
91
|
+
enabled, so the output validates against the source schema (opt out with
|
|
92
|
+
`schemaOptions: { optionalPresence: false }`); a required property mapping to a
|
|
93
|
+
choice is marked `mandatory`.
|
|
86
94
|
|
|
87
95
|
## JSON transformer
|
|
88
96
|
|
|
@@ -98,6 +106,42 @@ const { schema, binding, initialValue } = jsonTransformer.toSchema(configText);
|
|
|
98
106
|
const out = jsonTransformer.toSource({ ...initialValue, replicas: 5 }, binding);
|
|
99
107
|
```
|
|
100
108
|
|
|
109
|
+
## libconfig transformer (beta)
|
|
110
|
+
|
|
111
|
+
Edit a **libconfig document** — the `.cfg`/`.conf` format of srsRAN, OAI, and
|
|
112
|
+
other C/C++ software using [libconfig](https://hyperrealm.github.io/libconfig/).
|
|
113
|
+
**Beta**: it logs a one-time warning on first use; diff `toSource` output
|
|
114
|
+
against the original before deploying a write-back.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { libconfigTransformer } from 'ng-form-foundry-transformers';
|
|
118
|
+
|
|
119
|
+
const { schema, binding, initialValue } = libconfigTransformer.toSchema(cfgText);
|
|
120
|
+
// … build the form, let the user edit …
|
|
121
|
+
const editedCfg = libconfigTransformer.toSource(editedValue, binding);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The revert **splices edited spans into the original text**, so comments, key
|
|
125
|
+
order, `=` vs `:`, terminators, and delimiter style survive verbatim on every
|
|
126
|
+
unedited byte. Emission is **type-preserving** — libconfig is statically typed,
|
|
127
|
+
so a float slot edited to an integral value emits `21.0` not `21`, hex re-emits
|
|
128
|
+
in hex at its original width, an int64 keeps its `L` suffix, and values beyond
|
|
129
|
+
2^53 travel as exact decimal strings (the same bigint strategy as YAML/JSON).
|
|
130
|
+
|
|
131
|
+
Without a JSON Schema, the form is inferred from the document's own typed
|
|
132
|
+
literals; a list of groups infers the union of entry keys (keys missing from
|
|
133
|
+
some entries become presence toggles). **Empty and heterogeneous collections
|
|
134
|
+
are then shown read-only** — libconfig gives no element type to edit by; pass a
|
|
135
|
+
JSON Schema (`{ schema, schemaOptions? }`) to type them and make them editable.
|
|
136
|
+
`@include` is rejected by default (the form would show less than the C reader
|
|
137
|
+
sees); `{ includes: 'opaque' }` keeps the directive line verbatim and edits only
|
|
138
|
+
the file's own settings. Newly added settings are value-typed (an integral
|
|
139
|
+
number emits as an int) — route float-typed additions through a JSON Schema
|
|
140
|
+
slot that exists in the source, or accept the int typing.
|
|
141
|
+
|
|
142
|
+
The format, the parser design, and the full round-trip semantics are described
|
|
143
|
+
in [`docs/libconfig.md`](docs/libconfig.md).
|
|
144
|
+
|
|
101
145
|
## YANG transformer
|
|
102
146
|
|
|
103
147
|
Turn a **YANG** model into an ng-form-foundry schema, then revert the edited form
|
|
@@ -187,6 +231,23 @@ npm test # node:test on the compiled output (no Python needed)
|
|
|
187
231
|
|
|
188
232
|
## Status
|
|
189
233
|
|
|
234
|
+
`0.3.4` — the **libconfig transformer debuts as a beta** (one-time console
|
|
235
|
+
warning on first use; lossless span-splicing round-trip with scalar types
|
|
236
|
+
preserved — see [`docs/libconfig.md`](docs/libconfig.md)), and
|
|
237
|
+
`minProperties`/`maxProperties` on closed objects now map to the library's
|
|
238
|
+
`minPresent`/`maxPresent` present-children bounds, so an emptied
|
|
239
|
+
all-optional object (e.g. an A1 `qosObjectives`) is flagged client-side
|
|
240
|
+
instead of only by the server's validator.
|
|
241
|
+
|
|
242
|
+
`0.3.3` — schema-valid round-trips for JSON-Schema-driven forms. Non-`required`
|
|
243
|
+
properties now map to `presence` nodes (absent until enabled; opt out with
|
|
244
|
+
`schemaOptions: { optionalPresence: false }`), a required property mapping to a
|
|
245
|
+
choice is flagged `mandatory`, and the YAML/JSON transformers gained
|
|
246
|
+
`options.schemaOptions` — which also makes `refDocuments` (cross-file `$ref`)
|
|
247
|
+
reachable through them for the first time. Pairs with the library's
|
|
248
|
+
required-aware choice-case inference, empty list seeding, and
|
|
249
|
+
materialized-validity rules.
|
|
250
|
+
|
|
190
251
|
`0.3.2` — documentation release alongside the library's `serializeForm`: the
|
|
191
252
|
YAML/JSON `toSource` examples now take the wire value (choice `__case`
|
|
192
253
|
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,15 +100,22 @@ 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)
|
|
91
112
|
group.label = label;
|
|
113
|
+
// On a closed object these bound the count of *present* keys — meaningful
|
|
114
|
+
// because non-required properties are presence nodes (optionalPresence).
|
|
115
|
+
if (typeof schema.minProperties === 'number')
|
|
116
|
+
group.minPresent = schema.minProperties;
|
|
117
|
+
if (typeof schema.maxProperties === 'number')
|
|
118
|
+
group.maxPresent = schema.maxProperties;
|
|
92
119
|
return group;
|
|
93
120
|
}
|
|
94
121
|
/** An `object` with open keys — `additionalProperties: <schema>` or `patternProperties`, and no fixed `properties`. */
|
|
@@ -100,7 +127,7 @@ function isOpenMap(schema) {
|
|
|
100
127
|
const hasPattern = !!schema.patternProperties && Object.keys(schema.patternProperties).length > 0;
|
|
101
128
|
return additionalIsSchema || hasPattern;
|
|
102
129
|
}
|
|
103
|
-
function objectToMap(name, schema, resolver) {
|
|
130
|
+
function objectToMap(name, schema, resolver, ctx) {
|
|
104
131
|
let valueSchema = {};
|
|
105
132
|
let keyPattern;
|
|
106
133
|
const pattern = schema.patternProperties && Object.entries(schema.patternProperties)[0];
|
|
@@ -110,7 +137,7 @@ function objectToMap(name, schema, resolver) {
|
|
|
110
137
|
else if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
|
|
111
138
|
valueSchema = schema.additionalProperties;
|
|
112
139
|
}
|
|
113
|
-
const node = { kind: 'map', name, value: schemaToNode('value', valueSchema, false, resolver) };
|
|
140
|
+
const node = { kind: 'map', name, value: schemaToNode('value', valueSchema, false, resolver, ctx) };
|
|
114
141
|
if (schema.title)
|
|
115
142
|
node.label = schema.title;
|
|
116
143
|
if (keyPattern)
|
|
@@ -123,7 +150,7 @@ function objectToMap(name, schema, resolver) {
|
|
|
123
150
|
}
|
|
124
151
|
// --- anyOf / oneOf → choice ---------------------------------------------------
|
|
125
152
|
/** The `anyOf`/`oneOf` `[T, null]` shape collapses to a nullable leaf; anything else is a choice. */
|
|
126
|
-
function branchesToNode(name, schema, rawBranches, required, resolver) {
|
|
153
|
+
function branchesToNode(name, schema, rawBranches, required, resolver, ctx) {
|
|
127
154
|
const branches = rawBranches.map((b) => resolver.resolve(b));
|
|
128
155
|
const nonNull = branches.filter((b) => b.schema.type !== 'null');
|
|
129
156
|
const sole = nonNull.length === 1 ? nonNull[0] : undefined;
|
|
@@ -136,11 +163,13 @@ function branchesToNode(name, schema, rawBranches, required, resolver) {
|
|
|
136
163
|
const caseLabels = {};
|
|
137
164
|
branches.forEach(({ schema: branch, scope }, index) => {
|
|
138
165
|
const caseName = `case${index}`;
|
|
139
|
-
cases[caseName] = branchToCase(branch, scope);
|
|
166
|
+
cases[caseName] = branchToCase(branch, scope, ctx);
|
|
140
167
|
if (branch.title)
|
|
141
168
|
caseLabels[caseName] = branch.title;
|
|
142
169
|
});
|
|
143
170
|
const choice = { kind: 'choice', name, cases };
|
|
171
|
+
if (required)
|
|
172
|
+
choice.mandatory = true;
|
|
144
173
|
if (schema.title)
|
|
145
174
|
choice.label = schema.title;
|
|
146
175
|
if (Object.keys(caseLabels).length)
|
|
@@ -148,29 +177,30 @@ function branchesToNode(name, schema, rawBranches, required, resolver) {
|
|
|
148
177
|
return choice;
|
|
149
178
|
}
|
|
150
179
|
/** A resolved branch → a case body: a field record for an object branch, else a single node. */
|
|
151
|
-
function branchToCase(branch, scope) {
|
|
180
|
+
function branchToCase(branch, scope, ctx) {
|
|
152
181
|
if (primaryType(branch) === 'object' && branch.properties) {
|
|
153
182
|
const required = new Set(branch.required ?? []);
|
|
154
183
|
const fields = {};
|
|
155
184
|
for (const [key, propSchema] of Object.entries(branch.properties)) {
|
|
156
|
-
|
|
185
|
+
const node = schemaToNode(key, propSchema, required.has(key), scope, ctx);
|
|
186
|
+
fields[key] = markOptionalProperty(node, required.has(key), ctx);
|
|
157
187
|
}
|
|
158
188
|
return fields;
|
|
159
189
|
}
|
|
160
|
-
return schemaToNode('value', branch, false, scope);
|
|
190
|
+
return schemaToNode('value', branch, false, scope, ctx);
|
|
161
191
|
}
|
|
162
192
|
// --- dispatch -----------------------------------------------------------------
|
|
163
|
-
function schemaToNode(name, rawSchema, required, resolver) {
|
|
193
|
+
function schemaToNode(name, rawSchema, required, resolver, ctx) {
|
|
164
194
|
const { schema, scope } = resolver.resolve(rawSchema);
|
|
165
195
|
const branches = schema.anyOf ?? schema.oneOf;
|
|
166
196
|
if (branches && branches.length) {
|
|
167
|
-
return branchesToNode(name, schema, branches, required, scope);
|
|
197
|
+
return branchesToNode(name, schema, branches, required, scope, ctx);
|
|
168
198
|
}
|
|
169
199
|
const type = primaryType(schema);
|
|
170
200
|
if (type === 'object' || (type === undefined && (schema.properties || isOpenMap(schema)))) {
|
|
171
201
|
if (isOpenMap(schema))
|
|
172
|
-
return objectToMap(name, schema, scope);
|
|
173
|
-
return objectToNodeGroup(schema, name, scope, schema.title);
|
|
202
|
+
return objectToMap(name, schema, scope, ctx);
|
|
203
|
+
return objectToNodeGroup(schema, name, scope, ctx, schema.title);
|
|
174
204
|
}
|
|
175
205
|
if (type === 'array') {
|
|
176
206
|
const { schema: items, scope: itemScope } = scope.resolve(schema.items ?? {});
|
|
@@ -178,7 +208,7 @@ function schemaToNode(name, rawSchema, required, resolver) {
|
|
|
178
208
|
const node = {
|
|
179
209
|
kind: 'nodeGroupList',
|
|
180
210
|
name,
|
|
181
|
-
type: objectToNodeGroup(items, name, itemScope, items.title),
|
|
211
|
+
type: objectToNodeGroup(items, name, itemScope, ctx, items.title),
|
|
182
212
|
};
|
|
183
213
|
if (schema.title)
|
|
184
214
|
node.label = schema.title;
|
package/dist/core/schema.d.ts
CHANGED
|
@@ -59,6 +59,13 @@ 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;
|
|
62
69
|
children: Record<string, NodeType>;
|
|
63
70
|
}
|
|
64
71
|
export interface NodeGroupList {
|
|
@@ -86,7 +93,10 @@ export interface Choice {
|
|
|
86
93
|
/** Display labels for cases, keyed by case name. */
|
|
87
94
|
caseLabels?: Record<string, string>;
|
|
88
95
|
default?: string;
|
|
96
|
+
/** A case must be selected for the form to be valid (a required property). */
|
|
89
97
|
mandatory?: boolean;
|
|
98
|
+
/** Optional choice whose presence is itself data (mirrors {@link NodeGroup.presence}). */
|
|
99
|
+
presence?: boolean;
|
|
90
100
|
}
|
|
91
101
|
/**
|
|
92
102
|
* An open, arbitrary-keyed record: unlike {@link NodeGroup} (a fixed key set), a
|
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,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),
|
|
@@ -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,49 @@
|
|
|
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 } 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
|
+
/** The revert context: the original text and its positioned AST. */
|
|
39
|
+
export interface LibconfigBinding {
|
|
40
|
+
source: string;
|
|
41
|
+
root: CfgGroup;
|
|
42
|
+
}
|
|
43
|
+
/** Test seam: makes the one-time beta warning observable per test. */
|
|
44
|
+
export declare function resetLibconfigBetaWarning(): void;
|
|
45
|
+
export declare const libconfigTransformer: {
|
|
46
|
+
id: string;
|
|
47
|
+
toSchema(source: string, options?: LibconfigOptions): TransformResult<LibconfigBinding>;
|
|
48
|
+
toSource(value: FormValue, binding: LibconfigBinding): string;
|
|
49
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.libconfigTransformer = void 0;
|
|
4
|
+
exports.resetLibconfigBetaWarning = resetLibconfigBetaWarning;
|
|
5
|
+
const json_schema_1 = require("../../core/json-schema");
|
|
6
|
+
const parser_1 = require("./parser");
|
|
7
|
+
const schema_1 = require("./schema");
|
|
8
|
+
const revert_1 = require("./revert");
|
|
9
|
+
let warnedBeta = false;
|
|
10
|
+
function warnBeta() {
|
|
11
|
+
if (warnedBeta)
|
|
12
|
+
return;
|
|
13
|
+
warnedBeta = true;
|
|
14
|
+
console.warn('[ng-form-foundry-transformers] The libconfig transformer is a BETA feature. ' +
|
|
15
|
+
'Verify every write-back (diff toSource output against the original file) before deploying it.');
|
|
16
|
+
}
|
|
17
|
+
/** Test seam: makes the one-time beta warning observable per test. */
|
|
18
|
+
function resetLibconfigBetaWarning() {
|
|
19
|
+
warnedBeta = false;
|
|
20
|
+
}
|
|
21
|
+
exports.libconfigTransformer = {
|
|
22
|
+
id: 'libconfig',
|
|
23
|
+
toSchema(source, options) {
|
|
24
|
+
warnBeta();
|
|
25
|
+
const root = (0, parser_1.parseLibconfig)(source, { includes: options?.includes });
|
|
26
|
+
const schema = options?.schema
|
|
27
|
+
? (0, json_schema_1.jsonSchemaToNodeGroup)(options.schema, options.rootName, options.schemaOptions)
|
|
28
|
+
: (0, schema_1.libconfigToNodeGroup)(root, source, options?.rootName ?? '__root__');
|
|
29
|
+
const initialValue = (0, schema_1.extractValue)(root, source, options?.schema != null);
|
|
30
|
+
return { schema, binding: { source, root }, initialValue };
|
|
31
|
+
},
|
|
32
|
+
toSource(value, binding) {
|
|
33
|
+
warnBeta();
|
|
34
|
+
return (0, revert_1.applyValueToSource)(binding.source, binding.root, value);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A lossless libconfig parser: source text → positioned AST.
|
|
3
|
+
*
|
|
4
|
+
* Every node carries its `[start, end)` span into the original text, and every
|
|
5
|
+
* scalar carries the emission metadata (`radix`, `L` suffix, float-ness, raw
|
|
6
|
+
* lexeme) that {@link import('./revert')} needs to write an edited value back
|
|
7
|
+
* without re-typing the setting for the consuming C program — libconfig is
|
|
8
|
+
* statically typed, so `20` and `20.0` are different types to
|
|
9
|
+
* `config_lookup_*`. Comments and formatting are not modeled: the revert
|
|
10
|
+
* splices only edited value spans, so every unedited byte survives verbatim.
|
|
11
|
+
*
|
|
12
|
+
* Implemented independently from the libconfig manual
|
|
13
|
+
* (https://hyperrealm.github.io/libconfig/libconfig_manual.html); the grammar
|
|
14
|
+
* covers: groups `{}`, arrays `[]` (homogeneous scalars), lists `()`
|
|
15
|
+
* (heterogeneous), settings `name = value;` / `name : value` with an optional
|
|
16
|
+
* `;`/`,` terminator, int (decimal/hex/binary/octal), int64 (`L`/`LL`
|
|
17
|
+
* suffix), float, bool (case-insensitive), strings with escapes and adjacent
|
|
18
|
+
* literal concatenation, `#`/`//`/`/* */` comments, and `@include` lines.
|
|
19
|
+
*/
|
|
20
|
+
/** Half-open source range of a node, in code-unit offsets. */
|
|
21
|
+
export interface Span {
|
|
22
|
+
start: number;
|
|
23
|
+
end: number;
|
|
24
|
+
}
|
|
25
|
+
/** How an integer literal was written, so an edit re-emits the same style. */
|
|
26
|
+
export interface IntMeta {
|
|
27
|
+
radix: 2 | 8 | 10 | 16;
|
|
28
|
+
/** `L`/`LL` suffix, verbatim, or empty. */
|
|
29
|
+
suffix: string;
|
|
30
|
+
/** Hex/binary/octal digit count of the literal, for width-preserving edits. */
|
|
31
|
+
digits: number;
|
|
32
|
+
}
|
|
33
|
+
export type CfgValue = CfgScalar | CfgGroup | CfgArray | CfgList;
|
|
34
|
+
export interface CfgScalar {
|
|
35
|
+
kind: 'scalar';
|
|
36
|
+
span: Span;
|
|
37
|
+
/**
|
|
38
|
+
* `int` and `float` hold a JS number; `int64` holds the exact decimal digits
|
|
39
|
+
* as a string when the value is outside the safe integer range, a number
|
|
40
|
+
* otherwise (the string carry mirrors `core/bigint.ts`).
|
|
41
|
+
*/
|
|
42
|
+
type: 'int' | 'int64' | 'float' | 'bool' | 'string';
|
|
43
|
+
value: number | string | boolean;
|
|
44
|
+
int?: IntMeta;
|
|
45
|
+
}
|
|
46
|
+
export interface CfgSetting {
|
|
47
|
+
name: string;
|
|
48
|
+
/** The whole setting: name through terminator (exclusive of trailing comment). */
|
|
49
|
+
span: Span;
|
|
50
|
+
value: CfgValue;
|
|
51
|
+
}
|
|
52
|
+
export interface CfgGroup {
|
|
53
|
+
kind: 'group';
|
|
54
|
+
span: Span;
|
|
55
|
+
/** Between the braces (the whole file for the root) — the insertion window. */
|
|
56
|
+
innerSpan: Span;
|
|
57
|
+
settings: CfgSetting[];
|
|
58
|
+
}
|
|
59
|
+
export interface CfgArray {
|
|
60
|
+
kind: 'array';
|
|
61
|
+
span: Span;
|
|
62
|
+
innerSpan: Span;
|
|
63
|
+
elements: CfgScalar[];
|
|
64
|
+
}
|
|
65
|
+
export interface CfgList {
|
|
66
|
+
kind: 'list';
|
|
67
|
+
span: Span;
|
|
68
|
+
innerSpan: Span;
|
|
69
|
+
elements: CfgValue[];
|
|
70
|
+
}
|
|
71
|
+
/** Options observed by the parser (a subset of the transformer's options). */
|
|
72
|
+
export interface ParseOptions {
|
|
73
|
+
/**
|
|
74
|
+
* `'reject'` (default) errors on an `@include` line — the parsed form would
|
|
75
|
+
* silently show less than the C reader sees. `'opaque'` skips the directive:
|
|
76
|
+
* the line survives verbatim in the source and the included settings stay
|
|
77
|
+
* invisible to the form.
|
|
78
|
+
*/
|
|
79
|
+
includes?: 'reject' | 'opaque';
|
|
80
|
+
}
|
|
81
|
+
/** A parse failure, pointing at the offending line and column. */
|
|
82
|
+
export declare class LibconfigParseError extends Error {
|
|
83
|
+
readonly line: number;
|
|
84
|
+
readonly column: number;
|
|
85
|
+
constructor(message: string, line: number, column: number);
|
|
86
|
+
}
|
|
87
|
+
/** Parse a libconfig document into its root group (spans index into `source`). */
|
|
88
|
+
export declare function parseLibconfig(source: string, options?: ParseOptions): CfgGroup;
|
|
89
|
+
/** Scalar type family for array homogeneity: int and int64 mix, float does not. */
|
|
90
|
+
export declare function family(type: CfgScalar['type']): 'integer' | 'float' | 'bool' | 'string';
|