ng-form-foundry-transformers 0.2.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/LICENSE +201 -0
- package/README.md +186 -0
- package/dist/core/index.d.ts +14 -0
- package/dist/core/index.js +19 -0
- package/dist/core/infer.d.ts +15 -0
- package/dist/core/infer.js +67 -0
- package/dist/core/json-schema.d.ts +26 -0
- package/dist/core/json-schema.js +87 -0
- package/dist/core/registry.d.ts +18 -0
- package/dist/core/registry.js +40 -0
- package/dist/core/schema.d.ts +69 -0
- package/dist/core/schema.js +15 -0
- package/dist/core/transformer.d.ts +48 -0
- package/dist/core/transformer.js +2 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +40 -0
- package/dist/transformers/json/index.d.ts +8 -0
- package/dist/transformers/json/index.js +11 -0
- package/dist/transformers/json/json-transformer.d.ts +38 -0
- package/dist/transformers/json/json-transformer.js +41 -0
- package/dist/transformers/yaml/index.d.ts +11 -0
- package/dist/transformers/yaml/index.js +15 -0
- package/dist/transformers/yaml/revert.d.ts +13 -0
- package/dist/transformers/yaml/revert.js +74 -0
- package/dist/transformers/yaml/yaml-transformer.d.ts +32 -0
- package/dist/transformers/yaml/yaml-transformer.js +37 -0
- package/dist/transformers/yang/adapter.d.ts +40 -0
- package/dist/transformers/yang/adapter.js +68 -0
- package/dist/transformers/yang/cache.d.ts +18 -0
- package/dist/transformers/yang/cache.js +19 -0
- package/dist/transformers/yang/engine.d.ts +41 -0
- package/dist/transformers/yang/engine.js +2 -0
- package/dist/transformers/yang/engines/fake-engine.d.ts +15 -0
- package/dist/transformers/yang/engines/fake-engine.js +26 -0
- package/dist/transformers/yang/engines/subprocess-engine.d.ts +30 -0
- package/dist/transformers/yang/engines/subprocess-engine.js +64 -0
- package/dist/transformers/yang/index.d.ts +20 -0
- package/dist/transformers/yang/index.js +27 -0
- package/dist/transformers/yang/mapper.d.ts +14 -0
- package/dist/transformers/yang/mapper.js +96 -0
- package/dist/transformers/yang/model.d.ts +111 -0
- package/dist/transformers/yang/model.js +13 -0
- package/dist/transformers/yang/revert.d.ts +6 -0
- package/dist/transformers/yang/revert.js +173 -0
- package/dist/transformers/yang/rfc7951.d.ts +38 -0
- package/dist/transformers/yang/rfc7951.js +80 -0
- package/dist/transformers/yang/yang-transformer.d.ts +16 -0
- package/dist/transformers/yang/yang-transformer.js +29 -0
- package/package.json +55 -0
- package/python/emit_effective_tree.py +227 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toFormValue = toFormValue;
|
|
4
|
+
exports.toYangData = toYangData;
|
|
5
|
+
const schema_1 = require("../../core/schema");
|
|
6
|
+
const rfc7951_1 = require("./rfc7951");
|
|
7
|
+
/**
|
|
8
|
+
* The round-trip between a plain form value and RFC 7951 instance data, driven
|
|
9
|
+
* entirely by the resolved {@link EffectiveModel} (the server-side binding).
|
|
10
|
+
*
|
|
11
|
+
* `toFormValue` strips module qualification and decodes each leaf to the shape
|
|
12
|
+
* the form renders; `toYangData` re-encodes, keeps list keys as ordinary
|
|
13
|
+
* members, and drops `config false` state. Neither coerces plain scalar values,
|
|
14
|
+
* so int64/uint64/decimal64 (carried as strings) keep full precision.
|
|
15
|
+
*
|
|
16
|
+
* Per-type wire handling (RFC 7951 §6): `empty` is present as `[null]` /
|
|
17
|
+
* absent; `bits` is a space-separated flag string modeled as a boolean group;
|
|
18
|
+
* `identityref` is `module:identity` qualified only across module boundaries.
|
|
19
|
+
* A `choice` (§7.9) has no wrapper on the wire — the active case's fields are
|
|
20
|
+
* inline; in the form value it becomes `{ __case, ...fields }`, which this
|
|
21
|
+
* module flattens on encode and reconstructs on decode.
|
|
22
|
+
*
|
|
23
|
+
* Counterpart of the forward `mapToSchema` in `mapper.ts`.
|
|
24
|
+
*/
|
|
25
|
+
/** Sentinel returned by the encoder to mean "omit this member entirely". */
|
|
26
|
+
const OMIT = Symbol('omit');
|
|
27
|
+
/** RFC 7951 JSON instance data → a plain form value keyed by bare names. */
|
|
28
|
+
function toFormValue(rfc7951, model) {
|
|
29
|
+
return decodeObject(model.roots, asObject(rfc7951), null);
|
|
30
|
+
}
|
|
31
|
+
/** A plain form value → RFC 7951 JSON instance data ready for write-back. */
|
|
32
|
+
function toYangData(value, model) {
|
|
33
|
+
return encodeObject(model.roots, value, null);
|
|
34
|
+
}
|
|
35
|
+
// --- decode (RFC 7951 -> form) ------------------------------------------------
|
|
36
|
+
function decodeObject(children, raw, parentModule) {
|
|
37
|
+
const out = {};
|
|
38
|
+
for (const child of children) {
|
|
39
|
+
if (child.kind === 'choice') {
|
|
40
|
+
const decoded = decodeChoice(child, raw, parentModule);
|
|
41
|
+
if (decoded !== undefined)
|
|
42
|
+
out[child.name] = decoded;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const rawChild = readMember(raw, child, parentModule);
|
|
46
|
+
if (rawChild !== undefined)
|
|
47
|
+
out[child.name] = decodeNode(child, rawChild);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
function decodeChoice(choice, raw, parentModule) {
|
|
53
|
+
for (const c of choice.cases) {
|
|
54
|
+
const fields = c.children.filter((f) => f.kind !== 'choice');
|
|
55
|
+
if (!fields.some((f) => readMember(raw, f, parentModule) !== undefined))
|
|
56
|
+
continue;
|
|
57
|
+
const caseValue = { [schema_1.CASE_KEY]: c.name };
|
|
58
|
+
for (const f of fields) {
|
|
59
|
+
const rawF = readMember(raw, f, parentModule);
|
|
60
|
+
if (rawF !== undefined)
|
|
61
|
+
caseValue[f.name] = decodeNode(f, rawF);
|
|
62
|
+
}
|
|
63
|
+
return caseValue;
|
|
64
|
+
}
|
|
65
|
+
return choice.default !== undefined ? { [schema_1.CASE_KEY]: choice.default } : undefined;
|
|
66
|
+
}
|
|
67
|
+
function decodeNode(node, raw) {
|
|
68
|
+
switch (node.kind) {
|
|
69
|
+
case 'leaf':
|
|
70
|
+
return decodeLeaf(node, raw);
|
|
71
|
+
case 'leaf-list':
|
|
72
|
+
return Array.isArray(raw) ? [...raw] : raw;
|
|
73
|
+
case 'container':
|
|
74
|
+
return decodeObject(node.children, asObject(raw), node.module);
|
|
75
|
+
case 'list':
|
|
76
|
+
return (Array.isArray(raw) ? raw : []).map((entry) => decodeObject(node.children, asObject(entry), node.module));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function decodeLeaf(node, raw) {
|
|
80
|
+
switch (node.type.base) {
|
|
81
|
+
case 'empty':
|
|
82
|
+
return true;
|
|
83
|
+
case 'bits': {
|
|
84
|
+
const set = new Set(typeof raw === 'string' ? raw.split(/\s+/).filter(Boolean) : []);
|
|
85
|
+
const group = {};
|
|
86
|
+
for (const bit of node.type.bits ?? [])
|
|
87
|
+
group[bit] = set.has(bit);
|
|
88
|
+
return group;
|
|
89
|
+
}
|
|
90
|
+
case 'identityref':
|
|
91
|
+
return typeof raw === 'string' ? (0, rfc7951_1.bareIdentity)(raw) : raw;
|
|
92
|
+
default:
|
|
93
|
+
return raw;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// --- encode (form -> RFC 7951) ------------------------------------------------
|
|
97
|
+
function encodeObject(children, value, parentModule) {
|
|
98
|
+
const out = {};
|
|
99
|
+
for (const child of children) {
|
|
100
|
+
if (child.kind === 'choice') {
|
|
101
|
+
encodeChoice(child, value[child.name], parentModule, out);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (isExcluded(child))
|
|
105
|
+
continue;
|
|
106
|
+
const v = value[child.name];
|
|
107
|
+
if (v === undefined)
|
|
108
|
+
continue;
|
|
109
|
+
const encoded = encodeNode(child, v);
|
|
110
|
+
if (encoded !== OMIT)
|
|
111
|
+
out[(0, rfc7951_1.qualifiedName)(child.name, child.module, parentModule)] = encoded;
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
function encodeChoice(choice, choiceValue, parentModule, out) {
|
|
116
|
+
if (!choiceValue || typeof choiceValue !== 'object')
|
|
117
|
+
return;
|
|
118
|
+
const caseName = choiceValue[schema_1.CASE_KEY];
|
|
119
|
+
const active = choice.cases.find((c) => c.name === caseName);
|
|
120
|
+
if (!active)
|
|
121
|
+
return;
|
|
122
|
+
// The active case's fields serialize inline at the choice's location.
|
|
123
|
+
for (const f of active.children) {
|
|
124
|
+
if (f.kind === 'choice' || isExcluded(f))
|
|
125
|
+
continue;
|
|
126
|
+
const v = choiceValue[f.name];
|
|
127
|
+
if (v === undefined)
|
|
128
|
+
continue;
|
|
129
|
+
const encoded = encodeNode(f, v);
|
|
130
|
+
if (encoded !== OMIT)
|
|
131
|
+
out[(0, rfc7951_1.qualifiedName)(f.name, f.module, parentModule)] = encoded;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function encodeNode(node, value) {
|
|
135
|
+
switch (node.kind) {
|
|
136
|
+
case 'leaf':
|
|
137
|
+
return encodeLeaf(node, value);
|
|
138
|
+
case 'leaf-list':
|
|
139
|
+
return Array.isArray(value) ? [...value] : value;
|
|
140
|
+
case 'container':
|
|
141
|
+
return encodeObject(node.children, asObject(value), node.module);
|
|
142
|
+
case 'list':
|
|
143
|
+
return (Array.isArray(value) ? value : []).map((entry) => encodeObject(node.children, asObject(entry), node.module));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function encodeLeaf(node, value) {
|
|
147
|
+
switch (node.type.base) {
|
|
148
|
+
case 'empty':
|
|
149
|
+
return value ? [null] : OMIT;
|
|
150
|
+
case 'bits': {
|
|
151
|
+
const group = asObject(value);
|
|
152
|
+
return (node.type.bits ?? []).filter((bit) => group[bit]).join(' ');
|
|
153
|
+
}
|
|
154
|
+
case 'identityref':
|
|
155
|
+
return typeof value === 'string' ? (0, rfc7951_1.qualifyIdentity)(value, node.module, node.type) : value;
|
|
156
|
+
default:
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// --- helpers ------------------------------------------------------------------
|
|
161
|
+
/** Read a node's value from an RFC 7951 object, tolerating a missing module prefix. */
|
|
162
|
+
function readMember(obj, node, parentModule) {
|
|
163
|
+
const qualified = (0, rfc7951_1.qualifiedName)(node.name, node.module, parentModule);
|
|
164
|
+
if (qualified in obj)
|
|
165
|
+
return obj[qualified];
|
|
166
|
+
return node.name in obj ? obj[node.name] : undefined;
|
|
167
|
+
}
|
|
168
|
+
function isExcluded(node) {
|
|
169
|
+
return 'config' in node && node.config === false;
|
|
170
|
+
}
|
|
171
|
+
function asObject(v) {
|
|
172
|
+
return v && typeof v === 'object' && !Array.isArray(v) ? v : {};
|
|
173
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { LeafType } from '../../core/schema';
|
|
2
|
+
import { YangBase, YangType } from './model';
|
|
3
|
+
/** Whether a leaf of this base must keep a string form to preserve precision. */
|
|
4
|
+
export declare function isStringEncoded(base: YangBase): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Map a resolved YANG type to the form model's four leaf value types.
|
|
7
|
+
*
|
|
8
|
+
* - `boolean` and `empty` → boolean (empty is a present/absent checkbox).
|
|
9
|
+
* - `enumeration` and `identityref` → enum (a dropdown of names).
|
|
10
|
+
* - the fixed-width integers (int/uint 8..32) → number.
|
|
11
|
+
* - everything else — string, binary, leafref, instance-identifier, union, and
|
|
12
|
+
* the string-encoded int64/uint64/decimal64 — renders as text.
|
|
13
|
+
*
|
|
14
|
+
* 64-bit integers and decimal64 map to `'string'` deliberately: a JS `number`
|
|
15
|
+
* cannot hold them without loss, so they stay strings end to end. `bits` has no
|
|
16
|
+
* leaf equivalent and is mapped to a group of boolean checkboxes by the mapper,
|
|
17
|
+
* so it never reaches this function.
|
|
18
|
+
*/
|
|
19
|
+
export declare function toFormLeafType(t: YangType): LeafType;
|
|
20
|
+
/**
|
|
21
|
+
* RFC 7951 identityref value (§6.8): `module:identity` when the identity is
|
|
22
|
+
* defined in a different module than the referencing leaf, otherwise the bare
|
|
23
|
+
* identity name.
|
|
24
|
+
*/
|
|
25
|
+
export declare function qualifyIdentity(name: string, leafModule: string, t: YangType): string;
|
|
26
|
+
/** The local identity name, dropping any `module:` prefix. */
|
|
27
|
+
export declare function bareIdentity(value: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* RFC 7951 member name for a node (§4). A name is qualified `module:name` when
|
|
30
|
+
* the node is at the top level (no parent module) or its module differs from its
|
|
31
|
+
* parent's; otherwise the bare name is used.
|
|
32
|
+
*/
|
|
33
|
+
export declare function qualifiedName(name: string, module: string, parentModule: string | null): string;
|
|
34
|
+
/** Split an RFC 7951 member name into its optional module prefix and local name. */
|
|
35
|
+
export declare function splitQualified(member: string): {
|
|
36
|
+
module?: string;
|
|
37
|
+
name: string;
|
|
38
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isStringEncoded = isStringEncoded;
|
|
4
|
+
exports.toFormLeafType = toFormLeafType;
|
|
5
|
+
exports.qualifyIdentity = qualifyIdentity;
|
|
6
|
+
exports.bareIdentity = bareIdentity;
|
|
7
|
+
exports.qualifiedName = qualifiedName;
|
|
8
|
+
exports.splitQualified = splitQualified;
|
|
9
|
+
/**
|
|
10
|
+
* RFC 7951 (JSON Encoding of YANG Data) helpers.
|
|
11
|
+
*
|
|
12
|
+
* The two round-trip hazards this module owns: member-name namespace
|
|
13
|
+
* qualification (§4) and the value encodings that differ from a naive JS mapping
|
|
14
|
+
* (§6) — notably that int64/uint64/decimal64 are JSON *strings* to avoid IEEE-754
|
|
15
|
+
* precision loss.
|
|
16
|
+
*/
|
|
17
|
+
/** Base types whose RFC 7951 value is a JSON string, not a number (§6.1). */
|
|
18
|
+
const STRING_ENCODED = new Set([
|
|
19
|
+
'int64',
|
|
20
|
+
'uint64',
|
|
21
|
+
'decimal64',
|
|
22
|
+
]);
|
|
23
|
+
const NUMBER_BASES = new Set([
|
|
24
|
+
'int8', 'int16', 'int32',
|
|
25
|
+
'uint8', 'uint16', 'uint32',
|
|
26
|
+
]);
|
|
27
|
+
/** Whether a leaf of this base must keep a string form to preserve precision. */
|
|
28
|
+
function isStringEncoded(base) {
|
|
29
|
+
return STRING_ENCODED.has(base);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Map a resolved YANG type to the form model's four leaf value types.
|
|
33
|
+
*
|
|
34
|
+
* - `boolean` and `empty` → boolean (empty is a present/absent checkbox).
|
|
35
|
+
* - `enumeration` and `identityref` → enum (a dropdown of names).
|
|
36
|
+
* - the fixed-width integers (int/uint 8..32) → number.
|
|
37
|
+
* - everything else — string, binary, leafref, instance-identifier, union, and
|
|
38
|
+
* the string-encoded int64/uint64/decimal64 — renders as text.
|
|
39
|
+
*
|
|
40
|
+
* 64-bit integers and decimal64 map to `'string'` deliberately: a JS `number`
|
|
41
|
+
* cannot hold them without loss, so they stay strings end to end. `bits` has no
|
|
42
|
+
* leaf equivalent and is mapped to a group of boolean checkboxes by the mapper,
|
|
43
|
+
* so it never reaches this function.
|
|
44
|
+
*/
|
|
45
|
+
function toFormLeafType(t) {
|
|
46
|
+
if (t.base === 'boolean' || t.base === 'empty')
|
|
47
|
+
return 'boolean';
|
|
48
|
+
if (t.base === 'enumeration' || t.base === 'identityref')
|
|
49
|
+
return 'enum';
|
|
50
|
+
if (NUMBER_BASES.has(t.base))
|
|
51
|
+
return 'number';
|
|
52
|
+
return 'string';
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* RFC 7951 identityref value (§6.8): `module:identity` when the identity is
|
|
56
|
+
* defined in a different module than the referencing leaf, otherwise the bare
|
|
57
|
+
* identity name.
|
|
58
|
+
*/
|
|
59
|
+
function qualifyIdentity(name, leafModule, t) {
|
|
60
|
+
const id = t.identities?.find((i) => i.name === name);
|
|
61
|
+
return id && id.module !== leafModule ? `${id.module}:${name}` : name;
|
|
62
|
+
}
|
|
63
|
+
/** The local identity name, dropping any `module:` prefix. */
|
|
64
|
+
function bareIdentity(value) {
|
|
65
|
+
const i = value.indexOf(':');
|
|
66
|
+
return i === -1 ? value : value.slice(i + 1);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* RFC 7951 member name for a node (§4). A name is qualified `module:name` when
|
|
70
|
+
* the node is at the top level (no parent module) or its module differs from its
|
|
71
|
+
* parent's; otherwise the bare name is used.
|
|
72
|
+
*/
|
|
73
|
+
function qualifiedName(name, module, parentModule) {
|
|
74
|
+
return parentModule === null || module !== parentModule ? `${module}:${name}` : name;
|
|
75
|
+
}
|
|
76
|
+
/** Split an RFC 7951 member name into its optional module prefix and local name. */
|
|
77
|
+
function splitQualified(member) {
|
|
78
|
+
const i = member.indexOf(':');
|
|
79
|
+
return i === -1 ? { name: member } : { module: member.slice(0, i), name: member.slice(i + 1) };
|
|
80
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Transformer } from '../../core/transformer';
|
|
2
|
+
import type { YangEngine, CompileRequest } from './engine';
|
|
3
|
+
import type { EffectiveModel } from './model';
|
|
4
|
+
/**
|
|
5
|
+
* A YANG {@link Transformer} for the catalog: resolves a YANG source through an
|
|
6
|
+
* engine into a form schema, and reverts the edited value to RFC 7951 data.
|
|
7
|
+
*
|
|
8
|
+
* This is the stateless, catalog-conformant view. For caching by `modelId`,
|
|
9
|
+
* engine-side validation on write-back, and RFC 7951 → form-value conversion,
|
|
10
|
+
* use the fuller {@link import('./adapter').YangFormAdapter} instead — both sit
|
|
11
|
+
* on the same engine, {@link mapToSchema}, and {@link toYangData}.
|
|
12
|
+
*
|
|
13
|
+
* The `binding` it round-trips is the resolved {@link EffectiveModel}, which the
|
|
14
|
+
* server keeps and never sends to the form UI.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createYangTransformer(engine: YangEngine): Transformer<CompileRequest, Record<string, unknown>, EffectiveModel, void>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createYangTransformer = createYangTransformer;
|
|
4
|
+
const mapper_1 = require("./mapper");
|
|
5
|
+
const revert_1 = require("./revert");
|
|
6
|
+
/**
|
|
7
|
+
* A YANG {@link Transformer} for the catalog: resolves a YANG source through an
|
|
8
|
+
* engine into a form schema, and reverts the edited value to RFC 7951 data.
|
|
9
|
+
*
|
|
10
|
+
* This is the stateless, catalog-conformant view. For caching by `modelId`,
|
|
11
|
+
* engine-side validation on write-back, and RFC 7951 → form-value conversion,
|
|
12
|
+
* use the fuller {@link import('./adapter').YangFormAdapter} instead — both sit
|
|
13
|
+
* on the same engine, {@link mapToSchema}, and {@link toYangData}.
|
|
14
|
+
*
|
|
15
|
+
* The `binding` it round-trips is the resolved {@link EffectiveModel}, which the
|
|
16
|
+
* server keeps and never sends to the form UI.
|
|
17
|
+
*/
|
|
18
|
+
function createYangTransformer(engine) {
|
|
19
|
+
return {
|
|
20
|
+
id: 'yang',
|
|
21
|
+
async toSchema(source) {
|
|
22
|
+
const binding = await engine.resolve(source);
|
|
23
|
+
return { schema: (0, mapper_1.mapToSchema)(binding), binding };
|
|
24
|
+
},
|
|
25
|
+
toSource(value, binding) {
|
|
26
|
+
return (0, revert_1.toYangData)(value, binding);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ng-form-foundry-transformers",
|
|
3
|
+
"version": "0.2.1",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"ng-form-foundry",
|
|
7
|
+
"form",
|
|
8
|
+
"transformer",
|
|
9
|
+
"yang",
|
|
10
|
+
"netconf",
|
|
11
|
+
"restconf",
|
|
12
|
+
"rfc7950",
|
|
13
|
+
"rfc7951",
|
|
14
|
+
"json-schema",
|
|
15
|
+
"yaml",
|
|
16
|
+
"json",
|
|
17
|
+
"network-management",
|
|
18
|
+
"typescript"
|
|
19
|
+
],
|
|
20
|
+
"author": "Mathias Santos de Brito",
|
|
21
|
+
"license": "Apache-2.0",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/mathiasbrito/ng-form-foundry.git",
|
|
25
|
+
"directory": "packages/ng-form-foundry-transformers"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://ng-form-foundry.readthedocs.io",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/mathiasbrito/ng-form-foundry/issues"
|
|
30
|
+
},
|
|
31
|
+
"type": "commonjs",
|
|
32
|
+
"main": "dist/index.js",
|
|
33
|
+
"types": "dist/index.d.ts",
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"python"
|
|
37
|
+
],
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc -p tsconfig.build.json",
|
|
43
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
44
|
+
"pretest": "tsc -p tsconfig.json",
|
|
45
|
+
"test": "node --test out-tsc/test/*.test.js",
|
|
46
|
+
"clean": "rm -rf dist out-tsc"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^20.0.0",
|
|
50
|
+
"typescript": "~5.8.2"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"yaml": "^2.9.0"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Resolve a YANG module with pyang and emit the effective model as JSON.
|
|
3
|
+
|
|
4
|
+
This is the integration counterpart of the TypeScript ``SubprocessEngine``. It
|
|
5
|
+
uses pyang to parse and validate a module (resolving uses/augment/typedef/
|
|
6
|
+
groupings), then walks pyang's resolved child tree (``i_children``) and prints an
|
|
7
|
+
``EffectiveModel`` matching ``src/model.ts`` to stdout.
|
|
8
|
+
|
|
9
|
+
Requires pyang on the host: ``pip install pyang``. The JS unit tests do not run
|
|
10
|
+
this — they use ``FakeEngine``. It emits container/list/leaf/leaf-list, choice
|
|
11
|
+
and the built-in types, with keys, config, default, mandatory, and the type
|
|
12
|
+
metadata (enum names, bits, union members, identities, leafref path).
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
emit_effective_tree.py --entry <module> --source <dir> [--path <dir> ...] \\
|
|
16
|
+
[--datastore config|operational]
|
|
17
|
+
"""
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import sys
|
|
21
|
+
|
|
22
|
+
from pyang import repository, context, statements
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def resolved_base(type_stmt):
|
|
26
|
+
"""Follow typedefs to a built-in base type name."""
|
|
27
|
+
spec = getattr(type_stmt, "i_type_spec", None)
|
|
28
|
+
if spec is not None and getattr(spec, "name", None):
|
|
29
|
+
return spec.name
|
|
30
|
+
# Fall back to walking i_typedef chains, else the literal type argument.
|
|
31
|
+
td = getattr(type_stmt, "i_typedef", None)
|
|
32
|
+
while td is not None:
|
|
33
|
+
inner = td.search_one("type")
|
|
34
|
+
if inner is None:
|
|
35
|
+
break
|
|
36
|
+
nxt = getattr(inner, "i_typedef", None)
|
|
37
|
+
if nxt is None:
|
|
38
|
+
return inner.arg
|
|
39
|
+
td = nxt
|
|
40
|
+
return type_stmt.arg
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def build_type(stmt):
|
|
44
|
+
type_stmt = stmt.search_one("type")
|
|
45
|
+
if type_stmt is None:
|
|
46
|
+
return {"base": "string"}
|
|
47
|
+
return type_info(type_stmt)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def type_info(type_stmt):
|
|
51
|
+
base = resolved_base(type_stmt)
|
|
52
|
+
out = {"base": base}
|
|
53
|
+
if base == "enumeration":
|
|
54
|
+
out["enums"] = [e.arg for e in type_stmt.search("enum")]
|
|
55
|
+
elif base == "bits":
|
|
56
|
+
out["bits"] = [b.arg for b in type_stmt.search("bit")]
|
|
57
|
+
elif base == "union":
|
|
58
|
+
out["members"] = [type_info(t) for t in type_stmt.search("type")]
|
|
59
|
+
elif base == "identityref":
|
|
60
|
+
out["identities"] = collect_identities(type_stmt)
|
|
61
|
+
elif base == "decimal64":
|
|
62
|
+
fd = type_stmt.search_one("fraction-digits")
|
|
63
|
+
if fd is not None:
|
|
64
|
+
out["fractionDigits"] = int(fd.arg)
|
|
65
|
+
elif base == "leafref":
|
|
66
|
+
path = type_stmt.search_one("path")
|
|
67
|
+
if path is not None:
|
|
68
|
+
out["leafrefPath"] = path.arg
|
|
69
|
+
return out
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def collect_identities(type_stmt):
|
|
73
|
+
"""Derived identities for an identityref, each tagged with its module.
|
|
74
|
+
|
|
75
|
+
Best-effort against pyang internals (``i_identity.i_derived``).
|
|
76
|
+
"""
|
|
77
|
+
result = []
|
|
78
|
+
base = type_stmt.search_one("base")
|
|
79
|
+
identity = getattr(base, "i_identity", None) if base is not None else None
|
|
80
|
+
derived = getattr(identity, "i_derived", None) if identity is not None else None
|
|
81
|
+
for ident in derived or []:
|
|
82
|
+
result.append({"name": ident.arg, "module": ident.i_module.i_modulename})
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def module_name(stmt):
|
|
87
|
+
mod = getattr(stmt, "i_module", None)
|
|
88
|
+
return mod.i_modulename if mod is not None else stmt.main_module().arg
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def is_config(stmt):
|
|
92
|
+
# pyang sets i_config on data nodes after validation.
|
|
93
|
+
val = getattr(stmt, "i_config", None)
|
|
94
|
+
return True if val is None else bool(val)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def walk(stmt):
|
|
98
|
+
kw = stmt.keyword
|
|
99
|
+
common = {"name": stmt.arg, "module": module_name(stmt), "config": is_config(stmt)}
|
|
100
|
+
|
|
101
|
+
if kw == "leaf":
|
|
102
|
+
node = {"kind": "leaf", **common, "type": build_type(stmt)}
|
|
103
|
+
if stmt.search_one("mandatory", "true") is not None:
|
|
104
|
+
node["mandatory"] = True
|
|
105
|
+
default = stmt.search_one("default")
|
|
106
|
+
if default is not None:
|
|
107
|
+
node["default"] = default.arg
|
|
108
|
+
return node
|
|
109
|
+
|
|
110
|
+
if kw == "leaf-list":
|
|
111
|
+
node = {"kind": "leaf-list", **common, "type": build_type(stmt)}
|
|
112
|
+
return node
|
|
113
|
+
|
|
114
|
+
if kw == "container":
|
|
115
|
+
node = {"kind": "container", **common, "children": walk_children(stmt)}
|
|
116
|
+
if stmt.search_one("presence") is not None:
|
|
117
|
+
node["presence"] = True
|
|
118
|
+
return node
|
|
119
|
+
|
|
120
|
+
if kw == "list":
|
|
121
|
+
key = stmt.search_one("key")
|
|
122
|
+
node = {
|
|
123
|
+
"kind": "list",
|
|
124
|
+
**common,
|
|
125
|
+
"keys": key.arg.split() if key is not None else [],
|
|
126
|
+
"children": walk_children(stmt),
|
|
127
|
+
}
|
|
128
|
+
return node
|
|
129
|
+
|
|
130
|
+
if kw == "choice":
|
|
131
|
+
cases = []
|
|
132
|
+
for case in getattr(stmt, "i_children", []):
|
|
133
|
+
if case.keyword == "case":
|
|
134
|
+
cases.append({"name": case.arg, "children": walk_children(case)})
|
|
135
|
+
else:
|
|
136
|
+
# shorthand case: a single data node directly under the choice
|
|
137
|
+
child = walk(case)
|
|
138
|
+
if child is not None:
|
|
139
|
+
cases.append({"name": case.arg, "children": [child]})
|
|
140
|
+
node = {"kind": "choice", "name": stmt.arg, "module": module_name(stmt), "cases": cases}
|
|
141
|
+
default = stmt.search_one("default")
|
|
142
|
+
if default is not None:
|
|
143
|
+
node["default"] = default.arg
|
|
144
|
+
if stmt.search_one("mandatory", "true") is not None:
|
|
145
|
+
node["mandatory"] = True
|
|
146
|
+
return node
|
|
147
|
+
|
|
148
|
+
return None # anydata/anyxml are not emitted
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def walk_children(stmt):
|
|
152
|
+
out = []
|
|
153
|
+
for child in getattr(stmt, "i_children", []):
|
|
154
|
+
node = walk(child)
|
|
155
|
+
if node is not None:
|
|
156
|
+
out.append(node)
|
|
157
|
+
return out
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def main():
|
|
161
|
+
ap = argparse.ArgumentParser()
|
|
162
|
+
ap.add_argument("--entry", required=True)
|
|
163
|
+
ap.add_argument("--source", required=True)
|
|
164
|
+
ap.add_argument("--path", action="append", default=[])
|
|
165
|
+
ap.add_argument("--datastore", default="config", choices=["config", "operational"])
|
|
166
|
+
args = ap.parse_args()
|
|
167
|
+
|
|
168
|
+
repo = repository.FileRepository(":".join([args.source, *args.path]))
|
|
169
|
+
ctx = context.Context(repo)
|
|
170
|
+
module = ctx.search_module(0, args.entry)
|
|
171
|
+
if module is None:
|
|
172
|
+
with open_module(ctx, args.entry, args.source) as text:
|
|
173
|
+
module = ctx.add_module(args.entry, text)
|
|
174
|
+
statements.validate_module(ctx, module)
|
|
175
|
+
if ctx.errors:
|
|
176
|
+
for _pos, tag, arg in ctx.errors:
|
|
177
|
+
print(f"pyang: {tag} {arg}", file=sys.stderr)
|
|
178
|
+
sys.exit(1)
|
|
179
|
+
|
|
180
|
+
roots = walk_children(module)
|
|
181
|
+
if args.datastore == "config":
|
|
182
|
+
roots = strip_state(roots)
|
|
183
|
+
|
|
184
|
+
model = {
|
|
185
|
+
"modules": [{"name": module.i_modulename, "namespace": namespace(module)}],
|
|
186
|
+
"roots": roots,
|
|
187
|
+
}
|
|
188
|
+
json.dump(model, sys.stdout)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def strip_state(nodes):
|
|
192
|
+
kept = []
|
|
193
|
+
for n in nodes:
|
|
194
|
+
if n.get("config") is False:
|
|
195
|
+
continue
|
|
196
|
+
if "children" in n:
|
|
197
|
+
n["children"] = strip_state(n["children"])
|
|
198
|
+
for case in n.get("cases", []):
|
|
199
|
+
case["children"] = strip_state(case["children"])
|
|
200
|
+
kept.append(n)
|
|
201
|
+
return kept
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def namespace(module):
|
|
205
|
+
ns = module.search_one("namespace")
|
|
206
|
+
return ns.arg if ns is not None else ""
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def open_module(ctx, name, source):
|
|
210
|
+
import glob
|
|
211
|
+
import os
|
|
212
|
+
from contextlib import contextmanager
|
|
213
|
+
|
|
214
|
+
@contextmanager
|
|
215
|
+
def _open():
|
|
216
|
+
matches = glob.glob(os.path.join(source, f"{name}*.yang"))
|
|
217
|
+
if not matches:
|
|
218
|
+
print(f"module '{name}' not found in {source}", file=sys.stderr)
|
|
219
|
+
sys.exit(1)
|
|
220
|
+
with open(matches[0], "r", encoding="utf-8") as fh:
|
|
221
|
+
yield fh.read()
|
|
222
|
+
|
|
223
|
+
return _open()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
if __name__ == "__main__":
|
|
227
|
+
main()
|