ng-form-foundry-transformers 0.4.1 → 0.5.1
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 +27 -4
- package/dist/core/infer.d.ts +3 -2
- package/dist/core/infer.js +7 -4
- package/dist/core/merge-inferred.d.ts +20 -0
- package/dist/core/merge-inferred.js +46 -0
- package/dist/core/schema-keys.d.ts +33 -0
- package/dist/core/schema-keys.js +57 -0
- package/dist/core/schema.d.ts +9 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/transformers/json/json-transformer.d.ts +35 -7
- package/dist/transformers/json/json-transformer.js +75 -11
- package/dist/transformers/libconfig/index.d.ts +1 -1
- package/dist/transformers/libconfig/index.js +1 -2
- package/dist/transformers/libconfig/libconfig-transformer.d.ts +29 -5
- package/dist/transformers/libconfig/libconfig-transformer.js +27 -19
- package/dist/transformers/libconfig/revert.d.ts +14 -2
- package/dist/transformers/libconfig/revert.js +30 -13
- package/dist/transformers/libconfig/schema.d.ts +11 -0
- package/dist/transformers/libconfig/schema.js +85 -5
- package/dist/transformers/yaml/index.d.ts +1 -1
- package/dist/transformers/yaml/revert.d.ts +8 -1
- package/dist/transformers/yaml/revert.js +43 -11
- package/dist/transformers/yaml/yaml-transformer.d.ts +32 -5
- package/dist/transformers/yaml/yaml-transformer.js +88 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +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) |
|
|
19
|
+
| `libconfig` | libconfig document (optionally a JSON Schema) | libconfig (comments **and scalar types** preserved) | available |
|
|
20
20
|
|
|
21
21
|
The YAML and JSON transformers share the same format-agnostic form builders in
|
|
22
22
|
`core` (`inferNodeGroup`, `jsonSchemaToNodeGroup`) — a JSON Schema is an *option*
|
|
@@ -155,6 +155,15 @@ enabled, so the output validates against the source schema (opt out with
|
|
|
155
155
|
`schemaOptions: { optionalPresence: false }`); a required property mapping to a
|
|
156
156
|
choice is marked `mandatory`.
|
|
157
157
|
|
|
158
|
+
**The schema may cover any slice of the document.** Keys it does not mention
|
|
159
|
+
are governed by `unknownKeys` (YAML, JSON, and libconfig alike):
|
|
160
|
+
`'preserve'` (default) keeps them verbatim on save — a partial schema edits
|
|
161
|
+
its fields without erasing the rest of a real-world config; `'drop'` deletes
|
|
162
|
+
them — for an intentionally complete schema (sanitizing, template
|
|
163
|
+
enforcement); `'edit'` surfaces them as editable fields typed by the
|
|
164
|
+
document's own values, merged under the JSON Schema in document order, so
|
|
165
|
+
nothing in the file is invisible.
|
|
166
|
+
|
|
158
167
|
## JSON transformer
|
|
159
168
|
|
|
160
169
|
Same as YAML, for **JSON config files** — `jsonTransformer.toSchema(json, { schema? })`
|
|
@@ -169,12 +178,10 @@ const { schema, binding, initialValue } = jsonTransformer.toSchema(configText);
|
|
|
169
178
|
const out = jsonTransformer.toSource({ ...initialValue, replicas: 5 }, binding);
|
|
170
179
|
```
|
|
171
180
|
|
|
172
|
-
## libconfig transformer
|
|
181
|
+
## libconfig transformer
|
|
173
182
|
|
|
174
183
|
Edit a **libconfig document** — the `.cfg`/`.conf` format of srsRAN, OAI, and
|
|
175
184
|
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
185
|
|
|
179
186
|
```ts
|
|
180
187
|
import { libconfigTransformer } from 'ng-form-foundry-transformers';
|
|
@@ -294,6 +301,22 @@ npm test # node:test on the compiled output (no Python needed)
|
|
|
294
301
|
|
|
295
302
|
## Status
|
|
296
303
|
|
|
304
|
+
`0.5.1` — **partial JSON Schemas edit safely**: the new `unknownKeys` option
|
|
305
|
+
(YAML, JSON, libconfig) governs keys the schema does not cover — `'preserve'`
|
|
306
|
+
(default) keeps them verbatim on save, `'drop'` deletes them for
|
|
307
|
+
intentionally complete schemas, `'edit'` surfaces them as editable fields
|
|
308
|
+
typed by the document's own values. The YAML binding is now
|
|
309
|
+
`YamlBinding { doc, schema? }` instead of the bare parsed document (bindings
|
|
310
|
+
are opaque; code that hands them straight back to `toSource` is unaffected).
|
|
311
|
+
|
|
312
|
+
`0.5.0` — the **libconfig transformer is stable**: the one-time beta warning
|
|
313
|
+
is gone (and with it the `resetLibconfigBetaWarning` helper), and the format's
|
|
314
|
+
guarantees and known limitations are documented in the transformers guide.
|
|
315
|
+
Non-decimal integer literals (`0x`, `0b`, `0o`/`0q`) now set a `radix` display
|
|
316
|
+
hint on the generated schema — the paired `ng-form-foundry` release renders
|
|
317
|
+
those fields as hex/octal/binary editors, and the YAML transformer does the
|
|
318
|
+
same for its hex and octal literals.
|
|
319
|
+
|
|
297
320
|
`0.4.1` — libconfig hardening after a conformance battle test against the C
|
|
298
321
|
library's own scanner, test corpus, and real srsRAN/OAI configs: `0q`/`0Q`
|
|
299
322
|
octal, verbatim radix-prefix preservation on edits, sign accepted on decimal
|
package/dist/core/infer.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { NodeGroup } from './schema';
|
|
2
2
|
/**
|
|
3
|
-
* Infer a root {@link NodeGroup} from a parsed data object
|
|
4
|
-
*
|
|
3
|
+
* Infer a root {@link NodeGroup} from a parsed data object — the whole form
|
|
4
|
+
* when no JSON Schema is given, and the layer `mergeInferred` overlays under
|
|
5
|
+
* `unknownKeys: 'edit'`. Structure and types come from the values themselves:
|
|
5
6
|
*
|
|
6
7
|
* - object → nodeGroup (recursed)
|
|
7
8
|
* - array of objects → nodeGroupList (item schema = union of the items' keys)
|
package/dist/core/infer.js
CHANGED
|
@@ -3,8 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.inferNodeGroup = inferNodeGroup;
|
|
4
4
|
const ROOT = '__root__';
|
|
5
5
|
/**
|
|
6
|
-
* Infer a root {@link NodeGroup} from a parsed data object
|
|
7
|
-
*
|
|
6
|
+
* Infer a root {@link NodeGroup} from a parsed data object — the whole form
|
|
7
|
+
* when no JSON Schema is given, and the layer `mergeInferred` overlays under
|
|
8
|
+
* `unknownKeys: 'edit'`. Structure and types come from the values themselves:
|
|
8
9
|
*
|
|
9
10
|
* - object → nodeGroup (recursed)
|
|
10
11
|
* - array of objects → nodeGroupList (item schema = union of the items' keys)
|
|
@@ -21,7 +22,9 @@ function inferNodeGroup(data, name = ROOT) {
|
|
|
21
22
|
return group;
|
|
22
23
|
}
|
|
23
24
|
function objectToNodeGroup(data, name) {
|
|
24
|
-
|
|
25
|
+
// Null prototype: data keys are arbitrary, and assigning a `__proto__` key
|
|
26
|
+
// onto a plain object would silently set the record's prototype instead.
|
|
27
|
+
const children = Object.create(null);
|
|
25
28
|
for (const [key, value] of Object.entries(data)) {
|
|
26
29
|
children[key] = inferNode(key, value);
|
|
27
30
|
}
|
|
@@ -45,7 +48,7 @@ function inferArray(name, items) {
|
|
|
45
48
|
}
|
|
46
49
|
/** A representative object with every key seen across `items`, each mapped to the first non-null sample. */
|
|
47
50
|
function unionKeys(items) {
|
|
48
|
-
const merged =
|
|
51
|
+
const merged = Object.create(null);
|
|
49
52
|
for (const item of items) {
|
|
50
53
|
for (const [key, value] of Object.entries(item)) {
|
|
51
54
|
if (!(key in merged) || (merged[key] == null && value != null))
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema union for `unknownKeys: 'edit'`: overlay a user-supplied JSON
|
|
3
|
+
* Schema's {@link NodeGroup} onto the schema *inferred from the document
|
|
4
|
+
* itself*, so covered keys keep their typed, validated, labeled nodes while
|
|
5
|
+
* every key the JSON Schema does not mention still renders — editable, typed
|
|
6
|
+
* by inference (see `core/infer.ts` and the per-format inference in the
|
|
7
|
+
* transformers). Counterpart of `core/schema-keys.ts`, which handles the
|
|
8
|
+
* *preserve-invisibly* answer to the same question.
|
|
9
|
+
*/
|
|
10
|
+
import type { NodeGroup } from './schema';
|
|
11
|
+
/**
|
|
12
|
+
* Merge per key, recursively. Document order wins for placement: the
|
|
13
|
+
* inferred children mirror the source document, so present keys render in
|
|
14
|
+
* file order with schema-only keys (e.g. presence-able additions) appended
|
|
15
|
+
* after, in schema order. Where both sides describe a key, the schema wins —
|
|
16
|
+
* except that groups and group-lists merge structurally, so a partially
|
|
17
|
+
* covered subtree keeps its uncovered fields at any depth. Group metadata
|
|
18
|
+
* (name, label, presence, bounds) comes from the schema side.
|
|
19
|
+
*/
|
|
20
|
+
export declare function mergeInferred(schema: NodeGroup, inferred: NodeGroup): NodeGroup;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mergeInferred = mergeInferred;
|
|
4
|
+
/**
|
|
5
|
+
* Merge per key, recursively. Document order wins for placement: the
|
|
6
|
+
* inferred children mirror the source document, so present keys render in
|
|
7
|
+
* file order with schema-only keys (e.g. presence-able additions) appended
|
|
8
|
+
* after, in schema order. Where both sides describe a key, the schema wins —
|
|
9
|
+
* except that groups and group-lists merge structurally, so a partially
|
|
10
|
+
* covered subtree keeps its uncovered fields at any depth. Group metadata
|
|
11
|
+
* (name, label, presence, bounds) comes from the schema side.
|
|
12
|
+
*/
|
|
13
|
+
function mergeInferred(schema, inferred) {
|
|
14
|
+
const children = Object.create(null);
|
|
15
|
+
for (const [name, inferredChild] of Object.entries(inferred.children)) {
|
|
16
|
+
const schemaChild = schema.children[name];
|
|
17
|
+
children[name] = schemaChild ? mergeNode(schemaChild, inferredChild) : inferredChild;
|
|
18
|
+
}
|
|
19
|
+
for (const [name, schemaChild] of Object.entries(schema.children)) {
|
|
20
|
+
if (!(name in children))
|
|
21
|
+
children[name] = schemaChild;
|
|
22
|
+
}
|
|
23
|
+
return { ...schema, children };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* One key described by both sides: groups and group-lists merge recursively;
|
|
27
|
+
* everything else — leaves, choices, maps, and shape disagreements — takes
|
|
28
|
+
* the schema node, which is authoritative wherever it speaks. One inferred
|
|
29
|
+
* fact does carry over: a leaf's `radix` display hint, which only the
|
|
30
|
+
* document knows (JSON Schema has no radix vocabulary).
|
|
31
|
+
*/
|
|
32
|
+
function mergeNode(schema, inferred) {
|
|
33
|
+
if (schema.kind === 'nodeGroup' && inferred.kind === 'nodeGroup') {
|
|
34
|
+
return mergeInferred(schema, inferred);
|
|
35
|
+
}
|
|
36
|
+
if (schema.kind === 'nodeGroupList' && inferred.kind === 'nodeGroupList') {
|
|
37
|
+
return { ...schema, type: mergeInferred(schema.type, inferred.type) };
|
|
38
|
+
}
|
|
39
|
+
if ((schema.kind === 'leaf' || schema.kind === 'leafList') &&
|
|
40
|
+
schema.kind === inferred.kind &&
|
|
41
|
+
inferred.radix &&
|
|
42
|
+
!schema.radix) {
|
|
43
|
+
return { ...schema, radix: inferred.radix };
|
|
44
|
+
}
|
|
45
|
+
return schema;
|
|
46
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema-born key resolution for partial-schema reverts.
|
|
3
|
+
*
|
|
4
|
+
* A transformer given a JSON Schema builds its form from the produced
|
|
5
|
+
* {@link NodeGroup} — so `serializeForm` can only ever emit keys that schema
|
|
6
|
+
* describes. When the edited value is written back onto the original
|
|
7
|
+
* document, the value is authoritative for those **schema-born** paths only;
|
|
8
|
+
* every other key was never carried by the form and must be preserved.
|
|
9
|
+
* These helpers answer "which keys can the value legitimately speak for?" at
|
|
10
|
+
* each document level, mirroring the wire encoding of
|
|
11
|
+
* ng-form-foundry's `serializeForm`/`toWireValue`.
|
|
12
|
+
*/
|
|
13
|
+
import type { NodeGroup, NodeType } from './schema';
|
|
14
|
+
/**
|
|
15
|
+
* The schema-born key set of one document level, as `name → child schema`
|
|
16
|
+
* for recursion. `undefined` means "no schema in play": every key is
|
|
17
|
+
* value-authoritative. A key absent from a defined map is not schema-born
|
|
18
|
+
* and must be preserved verbatim.
|
|
19
|
+
*/
|
|
20
|
+
export type SchemaKeys = Map<string, NodeType | undefined> | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* The keys `serializeForm` can emit for a schema node standing at a document
|
|
23
|
+
* object/group, with each key's schema for recursion. A nodeGroup
|
|
24
|
+
* contributes its children by key — a choice child included, since the wire
|
|
25
|
+
* value keeps a choice at its own key (`toWireValue` strips only the case
|
|
26
|
+
* discriminator). Standing at a choice, the keys are the union of every
|
|
27
|
+
* case's fields: the form only emits the active case, so the other cases'
|
|
28
|
+
* keys behave like any non-emitted schema key. A map is open-keyed — every
|
|
29
|
+
* key is schema-born and entries recurse with the map's value schema.
|
|
30
|
+
*/
|
|
31
|
+
export declare function childrenOf(node: NodeType): SchemaKeys;
|
|
32
|
+
/** The per-item schema when `node` describes a list of groups, else undefined. */
|
|
33
|
+
export declare function itemSchemaOf(node: NodeType | undefined): NodeGroup | undefined;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.childrenOf = childrenOf;
|
|
4
|
+
exports.itemSchemaOf = itemSchemaOf;
|
|
5
|
+
/**
|
|
6
|
+
* The keys `serializeForm` can emit for a schema node standing at a document
|
|
7
|
+
* object/group, with each key's schema for recursion. A nodeGroup
|
|
8
|
+
* contributes its children by key — a choice child included, since the wire
|
|
9
|
+
* value keeps a choice at its own key (`toWireValue` strips only the case
|
|
10
|
+
* discriminator). Standing at a choice, the keys are the union of every
|
|
11
|
+
* case's fields: the form only emits the active case, so the other cases'
|
|
12
|
+
* keys behave like any non-emitted schema key. A map is open-keyed — every
|
|
13
|
+
* key is schema-born and entries recurse with the map's value schema.
|
|
14
|
+
*/
|
|
15
|
+
function childrenOf(node) {
|
|
16
|
+
switch (node.kind) {
|
|
17
|
+
case 'nodeGroup':
|
|
18
|
+
return new Map(Object.entries(node.children));
|
|
19
|
+
case 'choice': {
|
|
20
|
+
const out = new Map();
|
|
21
|
+
for (const [name, field] of caseEntries(node))
|
|
22
|
+
out.set(name, field);
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
case 'map':
|
|
26
|
+
return new OpenKeys(node.value);
|
|
27
|
+
default:
|
|
28
|
+
return undefined; // leaf/leafList/list shapes carry no key semantics
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** The per-item schema when `node` describes a list of groups, else undefined. */
|
|
32
|
+
function itemSchemaOf(node) {
|
|
33
|
+
return node?.kind === 'nodeGroupList' ? node.type : undefined;
|
|
34
|
+
}
|
|
35
|
+
/** Field name → schema across every case of a choice (leaf-bodied cases too). */
|
|
36
|
+
function* caseEntries(choice) {
|
|
37
|
+
for (const body of Object.values(choice.cases)) {
|
|
38
|
+
if ('kind' in body)
|
|
39
|
+
yield [body.name, body];
|
|
40
|
+
else
|
|
41
|
+
for (const [name, field] of Object.entries(body))
|
|
42
|
+
yield [name, field];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** A `SchemaKeys` map matching every key — a map node's open dictionary. */
|
|
46
|
+
class OpenKeys extends Map {
|
|
47
|
+
constructor(valueSchema) {
|
|
48
|
+
super();
|
|
49
|
+
this.valueSchema = valueSchema;
|
|
50
|
+
}
|
|
51
|
+
has(_key) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
get(_key) {
|
|
55
|
+
return this.valueSchema;
|
|
56
|
+
}
|
|
57
|
+
}
|
package/dist/core/schema.d.ts
CHANGED
|
@@ -32,6 +32,13 @@ export interface Leaf {
|
|
|
32
32
|
min?: number;
|
|
33
33
|
max?: number;
|
|
34
34
|
multipleOf?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Present the value in this base (16 hex, 8 octal, 2 binary) instead of
|
|
37
|
+
* decimal — set when the source document wrote the literal that way. Purely
|
|
38
|
+
* a display hint: the value itself stays a plain number (or, on a string
|
|
39
|
+
* leaf, the exact decimal-digit carry of an integer beyond ±2^53).
|
|
40
|
+
*/
|
|
41
|
+
radix?: 2 | 8 | 16;
|
|
35
42
|
/** The value may be `null` (JSON Schema `type: [T, 'null']`). */
|
|
36
43
|
nullable?: boolean;
|
|
37
44
|
/** Optional scalar whose presence is itself data (on/off toggle). */
|
|
@@ -47,6 +54,8 @@ export interface LeafList {
|
|
|
47
54
|
label?: string;
|
|
48
55
|
minItems?: number;
|
|
49
56
|
maxItems?: number;
|
|
57
|
+
/** Present every item in this base — see {@link Leaf.radix}. */
|
|
58
|
+
radix?: 2 | 8 | 16;
|
|
50
59
|
}
|
|
51
60
|
export interface NodeGroup {
|
|
52
61
|
kind: 'nodeGroup';
|
package/dist/index.d.ts
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* - `json` — JSON config → form → JSON ({@link jsonTransformer}); same builders
|
|
14
14
|
* as `yaml`, indent preserved.
|
|
15
15
|
* - `libconfig` — libconfig document (srsRAN/OAI-style `.cfg`/`.conf`) → form →
|
|
16
|
-
* libconfig ({@link libconfigTransformer});
|
|
17
|
-
*
|
|
16
|
+
* libconfig ({@link libconfigTransformer}); comment- and
|
|
17
|
+
* type-preserving span splicing on revert.
|
|
18
18
|
*
|
|
19
19
|
* Look transformers up at runtime through {@link TransformerRegistry}, or import
|
|
20
20
|
* the one you need directly.
|
package/dist/index.js
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* - `json` — JSON config → form → JSON ({@link jsonTransformer}); same builders
|
|
15
15
|
* as `yaml`, indent preserved.
|
|
16
16
|
* - `libconfig` — libconfig document (srsRAN/OAI-style `.cfg`/`.conf`) → form →
|
|
17
|
-
* libconfig ({@link libconfigTransformer});
|
|
18
|
-
*
|
|
17
|
+
* libconfig ({@link libconfigTransformer}); comment- and
|
|
18
|
+
* type-preserving span splicing on revert.
|
|
19
19
|
*
|
|
20
20
|
* Look transformers up at runtime through {@link TransformerRegistry}, or import
|
|
21
21
|
* the one you need directly.
|
|
@@ -41,4 +41,4 @@ __exportStar(require("./core"), exports);
|
|
|
41
41
|
__exportStar(require("./transformers/yang"), exports);
|
|
42
42
|
__exportStar(require("./transformers/yaml"), exports);
|
|
43
43
|
__exportStar(require("./transformers/json"), exports);
|
|
44
|
-
__exportStar(require("./transformers/libconfig"), exports);
|
|
44
|
+
__exportStar(require("./transformers/libconfig"), exports);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FormValue, Thesaurus } from '../../core/schema';
|
|
1
|
+
import type { FormValue, NodeGroup, 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`. */
|
|
@@ -19,6 +19,18 @@ export interface JsonOptions {
|
|
|
19
19
|
* case-insensitively; never paths.
|
|
20
20
|
*/
|
|
21
21
|
thesaurus?: Thesaurus;
|
|
22
|
+
/**
|
|
23
|
+
* Schema-driven mode only: what happens to keys the JSON Schema does not
|
|
24
|
+
* cover. `'preserve'` (default) keeps them — the form never carried them,
|
|
25
|
+
* so a partial schema edits its slice without erasing the rest. `'drop'`
|
|
26
|
+
* makes the edited value authoritative for the whole document, deleting
|
|
27
|
+
* uncovered keys — for consumers whose schema is intentionally complete.
|
|
28
|
+
* `'edit'` surfaces them instead: uncovered keys render as editable fields
|
|
29
|
+
* typed by the data (the inferred schema merged under the JSON Schema), so
|
|
30
|
+
* nothing is invisible and the value covers the whole document. Ignored
|
|
31
|
+
* without a `schema`.
|
|
32
|
+
*/
|
|
33
|
+
unknownKeys?: 'preserve' | 'drop' | 'edit';
|
|
22
34
|
}
|
|
23
35
|
/** How the source was formatted, so `toSource` re-emits it the same way. */
|
|
24
36
|
export interface JsonFormat {
|
|
@@ -32,6 +44,19 @@ export interface JsonFormat {
|
|
|
32
44
|
* them back as unquoted numbers so precision survives the round-trip.
|
|
33
45
|
*/
|
|
34
46
|
bigInts: string[];
|
|
47
|
+
/**
|
|
48
|
+
* Present in schema-driven mode with `unknownKeys: 'preserve'` (the
|
|
49
|
+
* default) or `'edit'`: the original parsed data and the NodeGroup the
|
|
50
|
+
* form was built from (the JSON Schema's own under `'preserve'`, the
|
|
51
|
+
* inferred-merged one under `'edit'`). `toSource` then treats the value as
|
|
52
|
+
* authoritative for schema-born paths only and merges it over the
|
|
53
|
+
* original, so uncovered keys survive in their original key order — under
|
|
54
|
+
* `'edit'` that protection is left covering only the keys no form field
|
|
55
|
+
* can carry (e.g. a key inside a covered choice's object that no case
|
|
56
|
+
* names).
|
|
57
|
+
*/
|
|
58
|
+
original?: FormValue;
|
|
59
|
+
schema?: NodeGroup;
|
|
35
60
|
}
|
|
36
61
|
/**
|
|
37
62
|
* A JSON {@link Transformer}: turn a JSON config document into a form and write
|
|
@@ -39,12 +64,15 @@ export interface JsonFormat {
|
|
|
39
64
|
* `core` builders the YAML transformer uses — only parsing (`JSON.parse`) and
|
|
40
65
|
* serialization (`JSON.stringify`) are JSON-specific.
|
|
41
66
|
*
|
|
42
|
-
* Standard JSON has no comments, so revert re-serializes the
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
67
|
+
* Standard JSON has no comments, so revert re-serializes, preserving the
|
|
68
|
+
* source's indent width and trailing newline. In schema-driven mode with
|
|
69
|
+
* `unknownKeys: 'preserve'` or `'edit'` the value is merged over the original
|
|
70
|
+
* data at schema-born paths ({@link mergeSchemaBorn}), so uncovered keys and
|
|
71
|
+
* the original key order survive; otherwise the edited value is serialized
|
|
72
|
+
* as-is, key order following the form value. For JSON **with comments**
|
|
73
|
+
* (JSONC), parse and edit it as YAML with
|
|
74
|
+
* {@link import('../yaml/yaml-transformer').yamlTransformer} instead —
|
|
75
|
+
* `JSON.parse` rejects comments.
|
|
48
76
|
*
|
|
49
77
|
* Integers beyond 2^53 can't survive a `number` round-trip, so they are carried
|
|
50
78
|
* as strings in the form value and re-emitted verbatim as unquoted numbers (the
|
|
@@ -5,18 +5,23 @@ const thesaurus_1 = require("../../core/thesaurus");
|
|
|
5
5
|
const infer_1 = require("../../core/infer");
|
|
6
6
|
const json_schema_1 = require("../../core/json-schema");
|
|
7
7
|
const bigint_1 = require("../../core/bigint");
|
|
8
|
+
const schema_keys_1 = require("../../core/schema-keys");
|
|
9
|
+
const merge_inferred_1 = require("../../core/merge-inferred");
|
|
8
10
|
/**
|
|
9
11
|
* A JSON {@link Transformer}: turn a JSON config document into a form and write
|
|
10
12
|
* the edited value back to JSON. The form is built by the same format-agnostic
|
|
11
13
|
* `core` builders the YAML transformer uses — only parsing (`JSON.parse`) and
|
|
12
14
|
* serialization (`JSON.stringify`) are JSON-specific.
|
|
13
15
|
*
|
|
14
|
-
* Standard JSON has no comments, so revert re-serializes the
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* Standard JSON has no comments, so revert re-serializes, preserving the
|
|
17
|
+
* source's indent width and trailing newline. In schema-driven mode with
|
|
18
|
+
* `unknownKeys: 'preserve'` or `'edit'` the value is merged over the original
|
|
19
|
+
* data at schema-born paths ({@link mergeSchemaBorn}), so uncovered keys and
|
|
20
|
+
* the original key order survive; otherwise the edited value is serialized
|
|
21
|
+
* as-is, key order following the form value. For JSON **with comments**
|
|
22
|
+
* (JSONC), parse and edit it as YAML with
|
|
23
|
+
* {@link import('../yaml/yaml-transformer').yamlTransformer} instead —
|
|
24
|
+
* `JSON.parse` rejects comments.
|
|
20
25
|
*
|
|
21
26
|
* Integers beyond 2^53 can't survive a `number` round-trip, so they are carried
|
|
22
27
|
* as strings in the form value and re-emitted verbatim as unquoted numbers (the
|
|
@@ -28,26 +33,80 @@ exports.jsonTransformer = {
|
|
|
28
33
|
const bigInts = [];
|
|
29
34
|
const parsed = JSON.parse(source, bigIntReviver);
|
|
30
35
|
const data = collectBigInts(parsed ?? {}, [], bigInts);
|
|
31
|
-
const
|
|
36
|
+
const unknownKeys = options?.unknownKeys ?? 'preserve';
|
|
37
|
+
const fromJsonSchema = options?.schema
|
|
32
38
|
? (0, json_schema_1.jsonSchemaToNodeGroup)(options.schema, options.rootName, options.schemaOptions)
|
|
33
|
-
:
|
|
39
|
+
: undefined;
|
|
40
|
+
const schema = fromJsonSchema && unknownKeys === 'edit'
|
|
41
|
+
? (0, merge_inferred_1.mergeInferred)(fromJsonSchema, (0, infer_1.inferNodeGroup)(data, options?.rootName))
|
|
42
|
+
: fromJsonSchema ?? (0, infer_1.inferNodeGroup)(data, options?.rootName);
|
|
34
43
|
const labeled = options?.thesaurus ? (0, thesaurus_1.applyThesaurus)(schema, options.thesaurus) : schema;
|
|
44
|
+
// 'preserve' gates the revert on the JSON Schema's NodeGroup; 'edit' on
|
|
45
|
+
// the merged one (schema-born ≈ everything, but keys no form field can
|
|
46
|
+
// carry stay protected); 'drop' and inferred mode leave the value
|
|
47
|
+
// authoritative for the whole document.
|
|
48
|
+
const preserveUnknown = fromJsonSchema != null && unknownKeys !== 'drop';
|
|
35
49
|
const binding = {
|
|
36
50
|
indent: detectIndent(source),
|
|
37
51
|
trailingNewline: source.endsWith('\n'),
|
|
38
52
|
bigInts,
|
|
53
|
+
original: preserveUnknown ? data : undefined,
|
|
54
|
+
schema: preserveUnknown ? schema : undefined,
|
|
39
55
|
};
|
|
40
56
|
return { schema: labeled, binding, initialValue: data };
|
|
41
57
|
},
|
|
42
58
|
toSource(value, binding) {
|
|
59
|
+
const merged = binding.schema && binding.original !== undefined
|
|
60
|
+
? mergeSchemaBorn(binding.original, value, (0, schema_keys_1.childrenOf)(binding.schema))
|
|
61
|
+
: value;
|
|
43
62
|
const paths = new Set(binding.bigInts);
|
|
44
|
-
const prepared = paths.size ? markBigInts(
|
|
63
|
+
const prepared = paths.size ? markBigInts(merged, [], paths) : merged;
|
|
45
64
|
let text = JSON.stringify(prepared, null, binding.indent);
|
|
46
65
|
if (paths.size)
|
|
47
66
|
text = text.replace(BIGINT_MARK_RE, '$1');
|
|
48
67
|
return binding.trailingNewline ? text + '\n' : text;
|
|
49
68
|
},
|
|
50
69
|
};
|
|
70
|
+
/**
|
|
71
|
+
* Merge the edited value over the original data, value-authoritative for
|
|
72
|
+
* schema-born keys only: an uncovered key keeps its original value and its
|
|
73
|
+
* position in the key order; a covered key takes the edited value (recursing
|
|
74
|
+
* where both sides are objects, and per index into arrays of groups); a
|
|
75
|
+
* covered key absent from the value is omitted (a presence toggle off);
|
|
76
|
+
* covered keys new to the document append after the originals.
|
|
77
|
+
*/
|
|
78
|
+
function mergeSchemaBorn(original, value, schema) {
|
|
79
|
+
if (!isPlainObject(original) || !isPlainObject(value) || !schema)
|
|
80
|
+
return value;
|
|
81
|
+
// Null-prototype output and own-key checks: document keys are arbitrary, so
|
|
82
|
+
// `__proto__` must stay a data key and `toString`/`constructor` must not
|
|
83
|
+
// resolve through the prototype chain (which would drop or fabricate keys).
|
|
84
|
+
const out = Object.create(null);
|
|
85
|
+
for (const key of Object.keys(original)) {
|
|
86
|
+
if (!schema.has(key)) {
|
|
87
|
+
out[key] = original[key]; // not schema-born: verbatim, in place
|
|
88
|
+
}
|
|
89
|
+
else if (hasOwn(value, key)) {
|
|
90
|
+
out[key] = mergeChild(original[key], value[key], schema.get(key));
|
|
91
|
+
} // covered + absent: deleted
|
|
92
|
+
}
|
|
93
|
+
for (const key of Object.keys(value)) {
|
|
94
|
+
if (!(key in out) && !hasOwn(original, key) && schema.has(key))
|
|
95
|
+
out[key] = value[key];
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
/** Per-key recursion: objects merge by their child schema, group arrays per index. */
|
|
100
|
+
function mergeChild(original, value, schema) {
|
|
101
|
+
if (schema && isPlainObject(original) && isPlainObject(value)) {
|
|
102
|
+
return mergeSchemaBorn(original, value, (0, schema_keys_1.childrenOf)(schema));
|
|
103
|
+
}
|
|
104
|
+
const itemSchema = (0, schema_keys_1.itemSchemaOf)(schema);
|
|
105
|
+
if (itemSchema && Array.isArray(original) && Array.isArray(value)) {
|
|
106
|
+
return value.map((item, i) => i < original.length ? mergeChild(original[i], item, itemSchema) : item);
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
51
110
|
/** Indent width (in spaces) of the first indented line, or 2 if none/tabs. */
|
|
52
111
|
function detectIndent(source) {
|
|
53
112
|
const match = source.match(/\n( +)\S/);
|
|
@@ -102,7 +161,8 @@ function collectBigInts(node, path, out) {
|
|
|
102
161
|
if (Array.isArray(node))
|
|
103
162
|
return node.map((v, i) => collectBigInts(v, [...path, i], out));
|
|
104
163
|
if (isPlainObject(node)) {
|
|
105
|
-
|
|
164
|
+
// Null prototype: a `__proto__` document key must stay a data key.
|
|
165
|
+
const res = Object.create(null);
|
|
106
166
|
for (const key of Object.keys(node))
|
|
107
167
|
res[key] = collectBigInts(node[key], [...path, key], out);
|
|
108
168
|
return res;
|
|
@@ -121,7 +181,7 @@ function markBigInts(node, path, paths) {
|
|
|
121
181
|
if (Array.isArray(node))
|
|
122
182
|
return node.map((v, i) => markBigInts(v, [...path, i], paths));
|
|
123
183
|
if (isPlainObject(node)) {
|
|
124
|
-
const res =
|
|
184
|
+
const res = Object.create(null);
|
|
125
185
|
for (const key of Object.keys(node))
|
|
126
186
|
res[key] = markBigInts(node[key], [...path, key], paths);
|
|
127
187
|
return res;
|
|
@@ -131,3 +191,7 @@ function markBigInts(node, path, paths) {
|
|
|
131
191
|
function isPlainObject(v) {
|
|
132
192
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
133
193
|
}
|
|
194
|
+
/** Own-key membership, immune to `Object.prototype` members and null-prototype objects. */
|
|
195
|
+
function hasOwn(obj, key) {
|
|
196
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
197
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { libconfigTransformer,
|
|
1
|
+
export { libconfigTransformer, type LibconfigBinding, type LibconfigOptions, } from './libconfig-transformer';
|
|
2
2
|
export { parseLibconfig, LibconfigParseError } from './parser';
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LibconfigParseError = exports.parseLibconfig = exports.
|
|
3
|
+
exports.LibconfigParseError = exports.parseLibconfig = exports.libconfigTransformer = void 0;
|
|
4
4
|
var libconfig_transformer_1 = require("./libconfig-transformer");
|
|
5
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
6
|
var parser_1 = require("./parser");
|
|
8
7
|
Object.defineProperty(exports, "parseLibconfig", { enumerable: true, get: function () { return parser_1.parseLibconfig; } });
|
|
9
8
|
Object.defineProperty(exports, "LibconfigParseError", { enumerable: true, get: function () { return parser_1.LibconfigParseError; } });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A libconfig {@link Transformer}
|
|
2
|
+
* A libconfig {@link Transformer}: turn a libconfig document
|
|
3
3
|
* (srsRAN/OAI-style `.cfg`/`.conf`) into a form and write the edited value
|
|
4
4
|
* back with comments, formatting, and — critically — scalar *types*
|
|
5
5
|
* preserved (libconfig is statically typed; see {@link import('./revert')}).
|
|
@@ -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, Thesaurus } from '../../core/schema';
|
|
15
|
+
import type { FormValue, NodeGroup, 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,20 @@ export interface LibconfigOptions {
|
|
|
34
34
|
* line verbatim and edits only this file's own settings.
|
|
35
35
|
*/
|
|
36
36
|
includes?: 'reject' | 'opaque';
|
|
37
|
+
/**
|
|
38
|
+
* Schema-driven mode only: what happens to settings the JSON Schema does
|
|
39
|
+
* not cover. `'preserve'` (default) keeps them byte-verbatim in place —
|
|
40
|
+
* the form never carried them, so a partial schema edits its slice without
|
|
41
|
+
* erasing the rest. `'drop'` makes the edited value authoritative for the
|
|
42
|
+
* whole document, deleting uncovered settings — for consumers whose schema
|
|
43
|
+
* is intentionally complete (sanitizing a config, enforcing a strict
|
|
44
|
+
* template). `'edit'` surfaces them instead: uncovered settings render as
|
|
45
|
+
* editable fields typed by the document's own literals (the inferred
|
|
46
|
+
* schema merged under the JSON Schema — see `mergeInferred`), so nothing
|
|
47
|
+
* is invisible and the value covers the whole document. Ignored without a
|
|
48
|
+
* `schema`, where the inferred value covers every setting anyway.
|
|
49
|
+
*/
|
|
50
|
+
unknownKeys?: 'preserve' | 'drop' | 'edit';
|
|
37
51
|
/**
|
|
38
52
|
* Display metadata (`label`/`description`/choice `caseLabels`) injected into
|
|
39
53
|
* the produced schema, schema-driven or inferred alike — see
|
|
@@ -42,13 +56,23 @@ export interface LibconfigOptions {
|
|
|
42
56
|
*/
|
|
43
57
|
thesaurus?: Thesaurus;
|
|
44
58
|
}
|
|
45
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* The revert context: the original text, its positioned AST, and — in
|
|
61
|
+
* schema-driven mode with `unknownKeys: 'preserve'` or `'edit'` — the
|
|
62
|
+
* NodeGroup the form was built from (the JSON Schema's own under
|
|
63
|
+
* `'preserve'`, the inferred-merged one under `'edit'`), so `toSource` knows
|
|
64
|
+
* which paths are schema-born. Settings outside those paths were never
|
|
65
|
+
* carried by the form and survive verbatim; under `'edit'` that is only the
|
|
66
|
+
* keys no form field can carry (e.g. a key inside a covered choice's group
|
|
67
|
+
* that no case names). Absent `schema` (inferred mode, or
|
|
68
|
+
* `unknownKeys: 'drop'`), the value covers the whole document and is
|
|
69
|
+
* authoritative everywhere.
|
|
70
|
+
*/
|
|
46
71
|
export interface LibconfigBinding {
|
|
47
72
|
source: string;
|
|
48
73
|
root: CfgGroup;
|
|
74
|
+
schema?: NodeGroup;
|
|
49
75
|
}
|
|
50
|
-
/** Test seam: makes the one-time beta warning observable per test. */
|
|
51
|
-
export declare function resetLibconfigBetaWarning(): void;
|
|
52
76
|
export declare const libconfigTransformer: {
|
|
53
77
|
id: string;
|
|
54
78
|
toSchema(source: string, options?: LibconfigOptions): TransformResult<LibconfigBinding>;
|