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
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* libconfig AST → form schema and initial value.
|
|
3
|
+
*
|
|
4
|
+
* Inference works from the AST, not from plain data, because libconfig
|
|
5
|
+
* literals are statically typed: `20`, `20.0`, `true`, and `"x"` are four
|
|
6
|
+
* different types to the consuming C program, and the schema must reflect
|
|
7
|
+
* that. Rules the plain-data inference in `core/infer.ts` cannot express:
|
|
8
|
+
*
|
|
9
|
+
* - int leaves get `integer: true`; int64 values beyond 2^53 become string
|
|
10
|
+
* leaves constrained to integer digits (the `core/bigint.ts` carry).
|
|
11
|
+
* - A list of groups becomes a nodeGroupList whose item type is the union of
|
|
12
|
+
* the keys observed across entries; keys absent from at least one entry are
|
|
13
|
+
* marked `presence: true`, so building an entry that lacks them does not
|
|
14
|
+
* materialize null settings into the write-back.
|
|
15
|
+
* - Empty arrays/lists and heterogeneous lists have no honest element type,
|
|
16
|
+
* so they map to read-only string leaves carrying the raw source span —
|
|
17
|
+
* verbatim round-trip, no mistyping. A user-supplied JSON Schema (see the
|
|
18
|
+
* transformer options) replaces this inference entirely and makes typed
|
|
19
|
+
* empty collections editable.
|
|
20
|
+
*/
|
|
21
|
+
import type { NodeGroup } from '../../core/schema';
|
|
22
|
+
import { type CfgGroup, type CfgList, type CfgValue } from './parser';
|
|
23
|
+
/** Infer the root NodeGroup for a parsed document. */
|
|
24
|
+
export declare function libconfigToNodeGroup(root: CfgGroup, source: string, name: string): NodeGroup;
|
|
25
|
+
/**
|
|
26
|
+
* The plain form value mirroring {@link libconfigToNodeGroup}'s schema.
|
|
27
|
+
*
|
|
28
|
+
* `emptyAsArrays` selects the empty-collection carry: the inferred schema
|
|
29
|
+
* renders an empty `[]`/`()` as a read-only verbatim string (no honest element
|
|
30
|
+
* type exists), while a user-supplied JSON Schema types it — so schema-driven
|
|
31
|
+
* extraction hands the form a real empty array instead.
|
|
32
|
+
*/
|
|
33
|
+
export declare function extractValue(value: CfgValue, source: string, emptyAsArrays?: boolean): unknown;
|
|
34
|
+
type ListShape = 'groups' | 'scalars' | 'empty' | 'heterogeneous';
|
|
35
|
+
/** Classify a `( )` list: all groups, all same-family scalars, empty, or mixed. */
|
|
36
|
+
export declare function listShape(list: CfgList): ListShape;
|
|
37
|
+
/** The verbatim source slice of a node. */
|
|
38
|
+
export declare function raw(value: CfgValue, source: string): string;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.libconfigToNodeGroup = libconfigToNodeGroup;
|
|
4
|
+
exports.extractValue = extractValue;
|
|
5
|
+
exports.listShape = listShape;
|
|
6
|
+
exports.raw = raw;
|
|
7
|
+
const parser_1 = require("./parser");
|
|
8
|
+
/** Digits only — the constraint for int64 values carried as strings. */
|
|
9
|
+
const INTEGER_STRING_PATTERN = '^[-+]?[0-9]+$';
|
|
10
|
+
/** Infer the root NodeGroup for a parsed document. */
|
|
11
|
+
function libconfigToNodeGroup(root, source, name) {
|
|
12
|
+
const group = groupToNode(root, source, name);
|
|
13
|
+
group.root = true;
|
|
14
|
+
return group;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The plain form value mirroring {@link libconfigToNodeGroup}'s schema.
|
|
18
|
+
*
|
|
19
|
+
* `emptyAsArrays` selects the empty-collection carry: the inferred schema
|
|
20
|
+
* renders an empty `[]`/`()` as a read-only verbatim string (no honest element
|
|
21
|
+
* type exists), while a user-supplied JSON Schema types it — so schema-driven
|
|
22
|
+
* extraction hands the form a real empty array instead.
|
|
23
|
+
*/
|
|
24
|
+
function extractValue(value, source, emptyAsArrays = false) {
|
|
25
|
+
switch (value.kind) {
|
|
26
|
+
case 'scalar':
|
|
27
|
+
return value.value;
|
|
28
|
+
case 'group': {
|
|
29
|
+
const out = Object.create(null);
|
|
30
|
+
for (const s of value.settings)
|
|
31
|
+
out[s.name] = extractValue(s.value, source, emptyAsArrays);
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
case 'array':
|
|
35
|
+
if (value.elements.length === 0 && !emptyAsArrays)
|
|
36
|
+
return raw(value, source);
|
|
37
|
+
return arrayValue(value.elements);
|
|
38
|
+
case 'list': {
|
|
39
|
+
const shape = listShape(value);
|
|
40
|
+
if (shape === 'groups')
|
|
41
|
+
return value.elements.map((e) => extractValue(e, source, emptyAsArrays));
|
|
42
|
+
if (shape === 'scalars')
|
|
43
|
+
return arrayValue(value.elements);
|
|
44
|
+
if (shape === 'empty' && emptyAsArrays)
|
|
45
|
+
return [];
|
|
46
|
+
return raw(value, source); // empty or heterogeneous: the verbatim span
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function nodeFor(name, value, source) {
|
|
51
|
+
switch (value.kind) {
|
|
52
|
+
case 'scalar':
|
|
53
|
+
return scalarLeaf(name, value);
|
|
54
|
+
case 'group':
|
|
55
|
+
return groupToNode(value, source, name);
|
|
56
|
+
case 'array':
|
|
57
|
+
return listLeaf(name, value.elements);
|
|
58
|
+
case 'list': {
|
|
59
|
+
const shape = listShape(value);
|
|
60
|
+
if (shape === 'groups')
|
|
61
|
+
return groupListNode(name, value, source);
|
|
62
|
+
if (shape === 'scalars')
|
|
63
|
+
return listLeaf(name, value.elements);
|
|
64
|
+
return rawLeaf(name, shape === 'empty' ? 'empty collection' : 'heterogeneous list');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function groupToNode(group, source, name) {
|
|
69
|
+
const children = Object.create(null);
|
|
70
|
+
for (const s of group.settings)
|
|
71
|
+
children[s.name] = nodeFor(s.name, s.value, source);
|
|
72
|
+
return { kind: 'nodeGroup', name, children };
|
|
73
|
+
}
|
|
74
|
+
function scalarLeaf(name, scalar) {
|
|
75
|
+
switch (scalar.type) {
|
|
76
|
+
case 'bool':
|
|
77
|
+
return { kind: 'leaf', name, type: 'boolean' };
|
|
78
|
+
case 'string':
|
|
79
|
+
return { kind: 'leaf', name, type: 'string' };
|
|
80
|
+
case 'float':
|
|
81
|
+
return { kind: 'leaf', name, type: 'number' };
|
|
82
|
+
case 'int':
|
|
83
|
+
return { kind: 'leaf', name, type: 'number', integer: true };
|
|
84
|
+
case 'int64':
|
|
85
|
+
// Beyond 2^53 the value rides as an exact decimal string.
|
|
86
|
+
return typeof scalar.value === 'string'
|
|
87
|
+
? { kind: 'leaf', name, type: 'string', pattern: INTEGER_STRING_PATTERN }
|
|
88
|
+
: { kind: 'leaf', name, type: 'number', integer: true };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* A homogeneous scalar collection → leafList. One int64 element beyond 2^53
|
|
93
|
+
* degrades the whole list to string carry: a leafList holds one scalar type,
|
|
94
|
+
* so consistency beats convenience (see {@link arrayValue}, which matches).
|
|
95
|
+
*/
|
|
96
|
+
function listLeaf(name, elements) {
|
|
97
|
+
if (elements.length === 0)
|
|
98
|
+
return rawLeaf(name, 'empty collection');
|
|
99
|
+
const f = (0, parser_1.family)(elements[0].type);
|
|
100
|
+
if (f === 'integer' && elements.some((e) => typeof e.value === 'string')) {
|
|
101
|
+
return { kind: 'leafList', name, type: 'string' };
|
|
102
|
+
}
|
|
103
|
+
return { kind: 'leafList', name, type: f === 'integer' || f === 'float' ? 'number' : f === 'bool' ? 'boolean' : 'string' };
|
|
104
|
+
}
|
|
105
|
+
function arrayValue(elements) {
|
|
106
|
+
const stringCarry = elements.length > 0 &&
|
|
107
|
+
(0, parser_1.family)(elements[0].type) === 'integer' &&
|
|
108
|
+
elements.some((e) => typeof e.value === 'string');
|
|
109
|
+
return elements.map((e) => (stringCarry ? String(e.value) : e.value));
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* A list of groups → nodeGroupList typed as the union of keys across entries;
|
|
113
|
+
* keys missing from at least one entry become presence fields, so entries
|
|
114
|
+
* lacking them build without the key instead of materializing nulls.
|
|
115
|
+
*/
|
|
116
|
+
function groupListNode(name, list, source) {
|
|
117
|
+
const groups = list.elements;
|
|
118
|
+
const children = Object.create(null);
|
|
119
|
+
const seenIn = Object.create(null);
|
|
120
|
+
for (const g of groups) {
|
|
121
|
+
for (const s of g.settings) {
|
|
122
|
+
if (!(s.name in children))
|
|
123
|
+
children[s.name] = nodeFor(s.name, s.value, source);
|
|
124
|
+
seenIn[s.name] = (seenIn[s.name] ?? 0) + 1;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const key of Object.keys(children)) {
|
|
128
|
+
if ((seenIn[key] ?? 0) < groups.length) {
|
|
129
|
+
const child = children[key];
|
|
130
|
+
if (child.kind === 'leaf' || child.kind === 'nodeGroup' || child.kind === 'map' || child.kind === 'choice') {
|
|
131
|
+
child.presence = true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return { kind: 'nodeGroupList', name, type: { kind: 'nodeGroup', name, children } };
|
|
136
|
+
}
|
|
137
|
+
/** Classify a `( )` list: all groups, all same-family scalars, empty, or mixed. */
|
|
138
|
+
function listShape(list) {
|
|
139
|
+
if (list.elements.length === 0)
|
|
140
|
+
return 'empty';
|
|
141
|
+
if (list.elements.every((e) => e.kind === 'group'))
|
|
142
|
+
return 'groups';
|
|
143
|
+
if (list.elements.every((e) => e.kind === 'scalar')) {
|
|
144
|
+
const families = new Set(list.elements.map((e) => (0, parser_1.family)(e.type)));
|
|
145
|
+
if (families.size === 1)
|
|
146
|
+
return 'scalars';
|
|
147
|
+
}
|
|
148
|
+
return 'heterogeneous';
|
|
149
|
+
}
|
|
150
|
+
/** Read-only leaf carrying the collection's verbatim source text. */
|
|
151
|
+
function rawLeaf(name, why) {
|
|
152
|
+
return {
|
|
153
|
+
kind: 'leaf',
|
|
154
|
+
name,
|
|
155
|
+
type: 'string',
|
|
156
|
+
readOnly: true,
|
|
157
|
+
description: `Shown verbatim (${why}): libconfig gives no element type to edit by. Provide a JSON Schema to make it editable.`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/** The verbatim source slice of a node. */
|
|
161
|
+
function raw(value, source) {
|
|
162
|
+
return source.slice(value.span.start, value.span.end);
|
|
163
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Document } from 'yaml';
|
|
2
|
-
import type { FormValue } from '../../core/schema';
|
|
2
|
+
import type { FormValue, Thesaurus } from '../../core/schema';
|
|
3
3
|
import type { TransformResult } from '../../core/transformer';
|
|
4
4
|
import { type JsonSchema, type JsonSchemaOptions } from '../../core/json-schema';
|
|
5
5
|
/** Options for {@link yamlTransformer}'s `toSchema`. */
|
|
@@ -14,6 +14,13 @@ export interface YamlOptions {
|
|
|
14
14
|
rootName?: string;
|
|
15
15
|
/** Options forwarded to `jsonSchemaToNodeGroup` (`refDocuments`, `optionalPresence`). */
|
|
16
16
|
schemaOptions?: JsonSchemaOptions;
|
|
17
|
+
/**
|
|
18
|
+
* Display metadata (`label`/`description`/choice `caseLabels`) injected into
|
|
19
|
+
* the produced schema, schema-driven or inferred alike — see
|
|
20
|
+
* `applyThesaurus`. Keys are plain identifier names, matched
|
|
21
|
+
* case-insensitively; never paths.
|
|
22
|
+
*/
|
|
23
|
+
thesaurus?: Thesaurus;
|
|
17
24
|
}
|
|
18
25
|
/**
|
|
19
26
|
* A YAML {@link Transformer}: turn a YAML config document into a form and write
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.yamlTransformer = void 0;
|
|
4
4
|
const yaml_1 = require("yaml");
|
|
5
|
+
const thesaurus_1 = require("../../core/thesaurus");
|
|
5
6
|
const infer_1 = require("../../core/infer");
|
|
6
7
|
const json_schema_1 = require("../../core/json-schema");
|
|
7
8
|
const bigint_1 = require("../../core/bigint");
|
|
@@ -29,7 +30,8 @@ exports.yamlTransformer = {
|
|
|
29
30
|
const schema = options?.schema
|
|
30
31
|
? (0, json_schema_1.jsonSchemaToNodeGroup)(options.schema, options.rootName, options.schemaOptions)
|
|
31
32
|
: (0, infer_1.inferNodeGroup)(data, options?.rootName);
|
|
32
|
-
|
|
33
|
+
const labeled = options?.thesaurus ? (0, thesaurus_1.applyThesaurus)(schema, options.thesaurus) : schema;
|
|
34
|
+
return { schema: labeled, binding: doc, initialValue: data };
|
|
33
35
|
},
|
|
34
36
|
toSource(value, binding) {
|
|
35
37
|
const doc = binding.clone();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ng-form-foundry-transformers",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Framework-agnostic Node + TypeScript catalog of source-format transformers: turn a YANG model (and more) into an ng-form-foundry schema and revert the edited form value back to the source format.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ng-form-foundry",
|