2 * ============LICENSE_START========================================================================
3 * ONAP : ccsdk feature sdnr wt odlux
4 * =================================================================================================
5 * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved.
6 * =================================================================================================
7 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
8 * in compliance with the License. You may obtain a copy of the License at
10 * http://www.apache.org/licenses/LICENSE-2.0
12 * Unless required by applicable law or agreed to in writing, software distributed under the License
13 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
14 * or implied. See the License for the specific language governing permissions and limitations under
16 * ============LICENSE_END==========================================================================
18 import { Token, Statement, Module, Identity, ModuleState } from "../models/yang";
20 ViewSpecification, ViewElement, isViewElementObjectOrList, ViewElementBase,
21 isViewElementReference, ViewElementChoise, ViewElementBinary, ViewElementString, isViewElementString,
22 isViewElementNumber, ViewElementNumber, Expression, YangRange, ViewElementUnion, ViewElementRpc, isViewElementRpc, ResolveFunction, ViewElementDate
23 } from "../models/uiModels";
24 import { yangService } from "../services/yangService";
27 export const splitVPath = (vPath: string, vPathParser: RegExp): RegExpMatchArray[] => {
28 const pathParts: RegExpMatchArray[] = [];
29 let partMatch: RegExpExecArray | null;
31 partMatch = vPathParser.exec(vPath);
33 pathParts.push(partMatch);
41 private pos: number = 0;
42 private buf: string = "";
44 constructor(input: string) {
49 private _optable: { [key: string]: string } = {
55 private _isNewline(char: string): boolean {
56 return char === '\r' || char === '\n';
59 private _isWhitespace(char: string): boolean {
60 return char === ' ' || char === '\t' || this._isNewline(char);
63 private _isDigit(char: string): boolean {
64 return char >= '0' && char <= '9';
67 private _isAlpha(char: string): boolean {
68 return (char >= 'a' && char <= 'z') ||
69 (char >= 'A' && char <= 'Z')
72 private _isAlphanum(char: string): boolean {
73 return this._isAlpha(char) || this._isDigit(char) ||
74 char === '_' || char === '-' || char === '.';
77 private _skipNontokens() {
78 while (this.pos < this.buf.length) {
79 const char = this.buf.charAt(this.pos);
80 if (this._isWhitespace(char)) {
88 private _processString(terminator: string | null): Token {
89 // this.pos points at the opening quote. Find the ending quote.
90 let end_index = this.pos + 1;
91 while (end_index < this.buf.length) {
92 const char = this.buf.charAt(end_index);
97 if (terminator === null && (this._isWhitespace(char) || this._optable[char] !== undefined) || char === terminator) {
103 if (end_index >= this.buf.length) {
104 throw Error('Unterminated quote at ' + this.pos);
106 const start = this.pos + (terminator ? 1 : 0);
107 const end = end_index;
110 value: this.buf.substring(start, end),
114 this.pos = terminator ? end + 1 : end;
119 private _processIdentifier(): Token {
120 let endpos = this.pos + 1;
121 while (endpos < this.buf.length && this._isAlphanum(this.buf.charAt(endpos))) {
125 let name = 'IDENTIFIER'
126 if (this.buf.charAt(endpos) === ":") {
127 name = 'IDENTIFIERREF';
129 while (endpos < this.buf.length && this._isAlphanum(this.buf.charAt(endpos))) {
136 value: this.buf.substring(this.pos, endpos),
145 private _processNumber(): Token {
146 let endpos = this.pos + 1;
147 while (endpos < this.buf.length &&
148 this._isDigit(this.buf.charAt(endpos))) {
154 value: this.buf.substring(this.pos, endpos),
162 private _processLineComment() {
163 var endpos = this.pos + 2;
164 // Skip until the end of the line
165 while (endpos < this.buf.length && !this._isNewline(this.buf.charAt(endpos))) {
168 this.pos = endpos + 1;
171 private _processBlockComment() {
172 var endpos = this.pos + 2;
173 // Skip until the end of the line
174 while (endpos < this.buf.length && !((this.buf.charAt(endpos) === "/" && this.buf.charAt(endpos - 1) === "*"))) {
177 this.pos = endpos + 1;
180 public tokenize(): Token[] {
181 const result: Token[] = [];
182 this._skipNontokens();
183 while (this.pos < this.buf.length) {
185 const char = this.buf.charAt(this.pos);
186 const op = this._optable[char];
188 if (op !== undefined) {
189 result.push({ name: op, value: char, start: this.pos, end: ++this.pos });
190 } else if (this._isAlpha(char)) {
191 result.push(this._processIdentifier());
192 this._skipNontokens();
193 const peekChar = this.buf.charAt(this.pos);
194 if (this._optable[peekChar] === undefined) {
195 result.push((peekChar !== "'" && peekChar !== '"')
196 ? this._processString(null)
197 : this._processString(peekChar));
199 } else if (char === '/' && this.buf.charAt(this.pos + 1) === "/") {
200 this._processLineComment();
201 } else if (char === '/' && this.buf.charAt(this.pos + 1) === "*") {
202 this._processBlockComment();
204 throw Error('Token error at ' + this.pos + " " + this.buf[this.pos]);
206 this._skipNontokens();
211 public tokenize2(): Statement {
212 let stack: Statement[] = [{ key: "ROOT", sub: [] }];
213 let current: Statement | null = null;
215 this._skipNontokens();
216 while (this.pos < this.buf.length) {
218 const char = this.buf.charAt(this.pos);
219 const op = this._optable[char];
221 if (op !== undefined) {
222 if (op === "L_BRACE") {
223 current && stack.unshift(current);
225 } else if (op === "R_BRACE") {
226 current = stack.shift() || null;
229 } else if (this._isAlpha(char) || char === "_") {
230 const key = this._processIdentifier().value;
231 this._skipNontokens();
232 let peekChar = this.buf.charAt(this.pos);
234 if (this._optable[peekChar] === undefined) {
235 arg = (peekChar === '"' || peekChar === "'")
236 ? this._processString(peekChar).value
237 : this._processString(null).value;
240 this._skipNontokens();
241 peekChar = this.buf.charAt(this.pos);
242 if (peekChar !== "+") break;
244 this._skipNontokens();
245 peekChar = this.buf.charAt(this.pos);
246 arg += (peekChar === '"' || peekChar === "'")
247 ? this._processString(peekChar).value
248 : this._processString(null).value;
250 current = { key, arg, sub: [] };
251 stack[0].sub!.push(current);
252 } else if (char === '/' && this.buf.charAt(this.pos + 1) === "/") {
253 this._processLineComment();
254 } else if (char === '/' && this.buf.charAt(this.pos + 1) === "*") {
255 this._processBlockComment();
257 throw Error('Token error at ' + this.pos + " " + this.buf.slice(this.pos - 10, this.pos + 10));
259 this._skipNontokens();
261 if (stack[0].key !== "ROOT" || !stack[0].sub![0]) {
262 throw new Error("Internal Perser Error");
264 return stack[0].sub![0];
268 export class YangParser {
269 private _groupingsToResolve: ViewSpecification[] = [];
271 private _identityToResolve: (() => void)[] = [];
272 private _unionsToResolve: (() => void)[] = [];
273 private _modulesToResolve: (() => void)[] = [];
275 private _modules: { [name: string]: Module } = {};
276 private _views: ViewSpecification[] = [{
287 public static ResolveStack = Symbol("ResolveStack");
289 constructor(private _unavailableCapabilities: { failureReason: string; capability: string; }[] = [], private _importOnlyModules: { name: string; revision: string; }[] = [], private nodeId: string) {
293 public get modules() {
294 return this._modules;
301 public async addCapability(capability: string, version?: string, parentImportOnlyModule?: boolean) {
303 if (this._modules[capability]) {
304 // console.warn(`Skipped capability: ${capability} since already contained.` );
308 // // do not add unavailable capabilities
309 // if (this._unavailableCapabilities.some(c => c.capability === capability)) {
310 // // console.warn(`Skipped capability: ${capability} since it is marked as unavailable.` );
313 const data = await yangService.getCapability(capability, this.nodeId, version);
315 throw new Error(`Could not load yang file for ${capability}.`);
318 const rootStatement = new YangLexer(data).tokenize2();
320 if (rootStatement.key !== "module") {
321 throw new Error(`Root element of ${capability} is not a module.`);
323 if (rootStatement.arg !== capability) {
324 throw new Error(`Root element capability ${rootStatement.arg} does not requested ${capability}.`);
327 const isUnavailable = this._unavailableCapabilities.some(c => c.capability === capability);
328 const isImportOnly = parentImportOnlyModule === true || this._importOnlyModules.some(c => c.name === capability);
330 const module = this._modules[capability] = {
331 name: rootStatement.arg,
342 ? ModuleState.unavailable
344 ? ModuleState.importOnly
345 : ModuleState.stable,
348 await this.handleModule(module, rootStatement, capability);
351 private async handleModule(module: Module, rootStatement: Statement, capability: string) {
353 // extract namespace && prefix
354 module.namespace = this.extractValue(rootStatement, "namespace");
355 module.prefix = this.extractValue(rootStatement, "prefix");
357 module.imports[module.prefix] = capability;
361 const revisions = this.extractNodes(rootStatement, "revision");
364 ...revisions.reduce<{ [version: string]: {} }>((acc, version) => {
366 throw new Error(`Module [${module.name}] has a version w/o version number.`);
368 const description = this.extractValue(version, "description");
369 const reference = this.extractValue(version, "reference");
379 const features = this.extractNodes(rootStatement, "feature");
382 ...features.reduce<{ [version: string]: {} }>((acc, feature) => {
384 throw new Error(`Module [${module.name}] has a feature w/o name.`);
386 const description = this.extractValue(feature, "description");
395 const imports = this.extractNodes(rootStatement, "import");
398 ...imports.reduce<{ [key: string]: string }>((acc, imp) => {
399 const prefix = imp.sub && imp.sub.filter(s => s.key === "prefix");
401 throw new Error(`Module [${module.name}] has an import with neither name nor prefix.`);
403 acc[prefix && prefix.length === 1 && prefix[0].arg || imp.arg] = imp.arg;
408 // import all required files and set module state
409 if (imports) for (let ind = 0; ind < imports.length; ++ind) {
410 const moduleName = imports[ind].arg!;
412 //TODO: Fix imports getting loaded without revision
413 await this.addCapability(moduleName, undefined, module.state === ModuleState.importOnly);
414 const importedModule = this._modules[imports[ind].arg!];
415 if (importedModule && importedModule.state > ModuleState.stable) {
416 module.state = Math.max(module.state, ModuleState.instable);
420 this.extractTypeDefinitions(rootStatement, module, "");
422 this.extractIdentities(rootStatement, 0, module, "");
424 const groupings = this.extractGroupings(rootStatement, 0, module, "");
425 this._views.push(...groupings);
427 const augments = this.extractAugments(rootStatement, 0, module, "");
428 this._views.push(...augments);
430 // the default for config on module level is config = true;
431 const [currentView, subViews] = this.extractSubViews(rootStatement, 0, module, "");
432 this._views.push(currentView, ...subViews);
434 // create the root elements for this module
435 module.elements = currentView.elements;
436 this._modulesToResolve.push(() => {
437 Object.keys(module.elements).forEach(key => {
438 const viewElement = module.elements[key];
439 if (!(isViewElementObjectOrList(viewElement) || isViewElementRpc(viewElement))) {
440 console.error(new Error(`Module: [${module}]. Only Object, List or RPC are allowed on root level.`));
442 if (isViewElementObjectOrList(viewElement)) {
443 const viewIdIndex = Number(viewElement.viewId);
444 module.views[key] = this._views[viewIdIndex];
447 // add only the UI View if the module is available
448 if (module.state === ModuleState.stable || module.state === ModuleState.instable) this._views[0].elements[key] = module.elements[key];
454 public postProcess() {
456 // execute all post processes like resolving in proper order
457 this._unionsToResolve.forEach(cb => {
458 try { cb(); } catch (error) {
459 console.warn(error.message);
463 // process all groupings
464 this._groupingsToResolve.filter(vs => vs.uses && vs.uses[ResolveFunction]).forEach(vs => {
465 try { vs.uses![ResolveFunction] !== undefined && vs.uses![ResolveFunction]!("|"); } catch (error) {
466 console.warn(`Error resolving: [${vs.name}] [${error.message}]`);
471 * This is to fix the issue for sequential execution of modules based on their child and parent relationship
472 * We are sorting the module object based on their augment status
474 Object.keys(this.modules)
476 if(this.modules[elem].augments && Object.keys(this.modules[elem].augments).length > 0) {
477 const {augments, ...rest} = this.modules[elem];
478 const partsOfKeys = Object.keys(augments).map((key) => (key.split("/").length - 1))
479 this.modules[elem].executionOrder= Math.max(...partsOfKeys)
481 this.modules[elem].executionOrder=0;
485 // process all augmentations / sort by namespace changes to ensure proper order
486 Object.keys(this.modules).sort((a, b) => this.modules[a].executionOrder! - this.modules[b].executionOrder!).forEach(modKey => {
487 const module = this.modules[modKey];
488 const augmentKeysWithCounter = Object.keys(module.augments).map((key) => {
489 const pathParts = splitVPath(key, /(?:(?:([^\/\:]+):)?([^\/]+))/g); // 1 = opt: namespace / 2 = property
490 let nameSpaceChangeCounter = 0;
491 let currentNS = module.name; // init namespace
492 pathParts.forEach(([ns, _])=> {
493 if (ns === currentNS){
495 nameSpaceChangeCounter++;
500 nameSpaceChangeCounter,
504 const augmentKeys = augmentKeysWithCounter
505 .sort((a,b) => a.nameSpaceChangeCounter > b.nameSpaceChangeCounter ? 1 : a.nameSpaceChangeCounter === b.nameSpaceChangeCounter ? 0 : -1 )
508 augmentKeys.forEach(augKey => {
509 const augments = module.augments[augKey];
510 const viewSpec = this.resolveView(augKey);
511 if (!viewSpec) console.warn(`Could not find view to augment [${augKey}] in [${module.name}].`);
512 if (augments && viewSpec) {
513 augments.forEach(augment => Object.keys(augment.elements).forEach(key => {
514 const elm = augment.elements[key];
515 viewSpec.elements[key] = {
516 ...augment.elements[key],
518 when: elm.when ? `(${augment.when}) and (${elm.when})` : augment.when,
519 ifFeature: elm.ifFeature ? `(${augment.ifFeature}) and (${elm.ifFeature})` : augment.ifFeature,
526 // process Identities
527 const traverseIdentity = (identities: Identity[]) => {
528 const result: Identity[] = [];
529 for (let identity of identities) {
530 if (identity.children && identity.children.length > 0) {
531 result.push(...traverseIdentity(identity.children));
533 result.push(identity);
539 const baseIdentities: Identity[] = [];
540 Object.keys(this.modules).forEach(modKey => {
541 const module = this.modules[modKey];
542 Object.keys(module.identities).forEach(idKey => {
543 const identity = module.identities[idKey];
544 if (identity.base != null) {
545 const base = this.resolveIdentity(identity.base, module);
546 base.children?.push(identity);
548 baseIdentities.push(identity);
552 baseIdentities.forEach(identity => {
553 identity.values = identity.children && traverseIdentity(identity.children) || [];
556 this._identityToResolve.forEach(cb => {
557 try { cb(); } catch (error) {
558 console.warn(error.message);
562 this._modulesToResolve.forEach(cb => {
563 try { cb(); } catch (error) {
564 console.warn(error.message);
569 const resolveReadOnly = (view: ViewSpecification, parentConfig: boolean) => {
571 // update view config
572 view.config = view.config && parentConfig;
574 Object.keys(view.elements).forEach((key) => {
575 const elm = view.elements[key];
577 // update element config
578 elm.config = elm.config && view.config;
580 // update all sub-elements of objects
581 if (elm.uiType === "object") {
582 resolveReadOnly(this.views[+elm.viewId], elm.config);
588 const dump = resolveReadOnly(this.views[0], true);
592 private get nextId() {
593 return this._nextId++;
596 private extractNodes(statement: Statement, key: string): Statement[] {
597 return statement.sub && statement.sub.filter(s => s.key === key) || [];
600 private extractValue(statement: Statement, key: string): string | undefined;
601 private extractValue(statement: Statement, key: string, parser: RegExp): RegExpExecArray | undefined;
602 private extractValue(statement: Statement, key: string, parser?: RegExp): string | RegExpExecArray | undefined {
603 const typeNodes = this.extractNodes(statement, key);
604 const rawValue = typeNodes.length > 0 && typeNodes[0].arg || undefined;
606 ? rawValue && parser.exec(rawValue) || undefined
610 private extractTypeDefinitions(statement: Statement, module: Module, currentPath: string): void {
611 const typedefs = this.extractNodes(statement, "typedef");
612 typedefs && typedefs.forEach(def => {
614 throw new Error(`Module: [${module.name}]. Found typefed without name.`);
616 module.typedefs[def.arg] = this.getViewElement(def, module, 0, currentPath, false);
620 /** Handles groupings like named Container */
621 private extractGroupings(statement: Statement, parentId: number, module: Module, currentPath: string): ViewSpecification[] {
622 const subViews: ViewSpecification[] = [];
623 const groupings = this.extractNodes(statement, "grouping");
624 if (groupings && groupings.length > 0) {
625 subViews.push(...groupings.reduce<ViewSpecification[]>((acc, cur) => {
627 throw new Error(`Module: [${module.name}][${currentPath}]. Found grouping without name.`);
629 const grouping = cur.arg;
631 // the default for config on module level is config = true;
632 const [currentView, subViews] = this.extractSubViews(cur, /* parentId */ -1, module, currentPath);
633 grouping && (module.groupings[grouping] = currentView);
634 acc.push(currentView, ...subViews);
642 /** Handles augments also like named container */
643 private extractAugments(statement: Statement, parentId: number, module: Module, currentPath: string): ViewSpecification[] {
644 const subViews: ViewSpecification[] = [];
645 const augments = this.extractNodes(statement, "augment");
646 if (augments && augments.length > 0) {
647 subViews.push(...augments.reduce<ViewSpecification[]>((acc, cur) => {
649 throw new Error(`Module: [${module.name}][${currentPath}]. Found augment without path.`);
651 const augment = this.resolveReferencePath(cur.arg, module);
653 // the default for config on module level is config = true;
654 const [currentView, subViews] = this.extractSubViews(cur, parentId, module, currentPath);
656 module.augments[augment] = module.augments[augment] || [];
657 module.augments[augment].push(currentView);
659 acc.push(currentView, ...subViews);
667 /** Handles identities */
668 private extractIdentities(statement: Statement, parentId: number, module: Module, currentPath: string) {
669 const identities = this.extractNodes(statement, "identity");
670 module.identities = identities.reduce<{ [name: string]: Identity }>((acc, cur) => {
672 throw new Error(`Module: [${module.name}][${currentPath}]. Found identiy without name.`);
675 id: `${module.name}:${cur.arg}`,
677 base: this.extractValue(cur, "base"),
678 description: this.extractValue(cur, "description"),
679 reference: this.extractValue(cur, "reference"),
686 // Hint: use 0 as parentId for rootElements and -1 for rootGroupings.
687 private extractSubViews(statement: Statement, parentId: number, module: Module, currentPath: string): [ViewSpecification, ViewSpecification[]] {
688 // used for scoped definitions
689 const context: Module = {
696 const currentId = this.nextId;
697 const subViews: ViewSpecification[] = [];
698 let elements: ViewElement[] = [];
700 const configValue = this.extractValue(statement, "config");
701 const config = configValue == null ? true : configValue.toLocaleLowerCase() !== "false";
703 // extract conditions
704 const ifFeature = this.extractValue(statement, "if-feature");
705 const whenCondition = this.extractValue(statement, "when");
706 if (whenCondition) console.warn("Found in [" + context.name + "]" + currentPath + " when: " + whenCondition);
708 // extract all scoped typedefs
709 this.extractTypeDefinitions(statement, context, currentPath);
711 // extract all scoped groupings
713 ...this.extractGroupings(statement, parentId, context, currentPath)
716 // extract all container
717 const container = this.extractNodes(statement, "container");
718 if (container && container.length > 0) {
719 subViews.push(...container.reduce<ViewSpecification[]>((acc, cur) => {
721 throw new Error(`Module: [${context.name}]${currentPath}. Found container without name.`);
723 const [currentView, subViews] = this.extractSubViews(cur, currentId, context, `${currentPath}/${context.name}:${cur.arg}`);
725 id: parentId === 0 ? `${context.name}:${cur.arg}` : cur.arg,
728 module: context.name || module.name || '',
730 viewId: currentView.id,
731 config: currentView.config,
733 acc.push(currentView, ...subViews);
739 // a list is a list of containers with the leafs contained in the list
740 const lists = this.extractNodes(statement, "list");
741 if (lists && lists.length > 0) {
742 subViews.push(...lists.reduce<ViewSpecification[]>((acc, cur) => {
743 let elmConfig = config;
745 throw new Error(`Module: [${context.name}]${currentPath}. Found list without name.`);
747 const key = this.extractValue(cur, "key") || undefined;
748 if (elmConfig && !key) {
749 console.warn(`Module: [${context.name}]${currentPath}. Found configurable list without key. Assume config shell be false.`);
752 const [currentView, subViews] = this.extractSubViews(cur, currentId, context, `${currentPath}/${context.name}:${cur.arg}`);
754 id: parentId === 0 ? `${context.name}:${cur.arg}` : cur.arg,
757 module: context.name || module.name || '',
760 viewId: currentView.id,
762 config: elmConfig && currentView.config,
764 acc.push(currentView, ...subViews);
769 // process all leaf-lists
770 // a leaf-list is a list of some type
771 const leafLists = this.extractNodes(statement, "leaf-list");
772 if (leafLists && leafLists.length > 0) {
773 elements.push(...leafLists.reduce<ViewElement[]>((acc, cur) => {
774 const element = this.getViewElement(cur, context, parentId, currentPath, true);
775 element && acc.push(element);
781 // a leaf is mainly a property of an object
782 const leafs = this.extractNodes(statement, "leaf");
783 if (leafs && leafs.length > 0) {
784 elements.push(...leafs.reduce<ViewElement[]>((acc, cur) => {
785 const element = this.getViewElement(cur, context, parentId, currentPath, false);
786 element && acc.push(element);
792 const choiceStms = this.extractNodes(statement, "choice");
793 if (choiceStms && choiceStms.length > 0) {
794 elements.push(...choiceStms.reduce<ViewElementChoise[]>((accChoise, curChoise) => {
795 if (!curChoise.arg) {
796 throw new Error(`Module: [${context.name}]${currentPath}. Found choise without name.`);
798 // extract all cases like containers
799 const cases: { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } }[] = [];
800 const caseStms = this.extractNodes(curChoise, "case");
801 if (caseStms && caseStms.length > 0) {
802 cases.push(...caseStms.reduce((accCase, curCase) => {
804 throw new Error(`Module: [${context.name}]${currentPath}/${curChoise.arg}. Found case without name.`);
806 const description = this.extractValue(curCase, "description") || undefined;
807 const [caseView, caseSubViews] = this.extractSubViews(curCase, parentId, context, `${currentPath}/${context.name}:${curChoise.arg}`);
808 subViews.push(caseView, ...caseSubViews);
810 const caseDef: { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } } = {
811 id: parentId === 0 ? `${context.name}:${curCase.arg}` : curCase.arg,
813 description: description,
814 elements: caseView.elements
816 accCase.push(caseDef);
818 }, [] as { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } }[]));
821 // extract all simple cases (one case per leaf, container, etc.)
822 const [choiseView, choiseSubViews] = this.extractSubViews(curChoise, parentId, context, `${currentPath}/${context.name}:${curChoise.arg}`);
823 subViews.push(choiseView, ...choiseSubViews);
824 cases.push(...Object.keys(choiseView.elements).reduce((accElm, curElm) => {
825 const elm = choiseView.elements[curElm];
826 const caseDef: { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } } = {
829 description: elm.description,
830 elements: { [elm.id]: elm }
832 accElm.push(caseDef);
834 }, [] as { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } }[]));
836 const description = this.extractValue(curChoise, "description") || undefined;
837 const configValue = this.extractValue(curChoise, "config");
838 const config = configValue == null ? true : configValue.toLocaleLowerCase() !== "false";
840 const mandatory = this.extractValue(curChoise, "mandatory") === "true" || false;
842 const element: ViewElementChoise = {
844 id: parentId === 0 ? `${context.name}:${curChoise.arg}` : curChoise.arg,
845 label: curChoise.arg,
847 module: context.name || module.name || '',
849 mandatory: mandatory,
850 description: description,
851 cases: cases.reduce((acc, cur) => {
854 }, {} as { [name: string]: { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } } })
857 accChoise.push(element);
862 const rpcStms = this.extractNodes(statement, "rpc");
863 if (rpcStms && rpcStms.length > 0) {
864 elements.push(...rpcStms.reduce<ViewElementRpc[]>((accRpc, curRpc) => {
866 throw new Error(`Module: [${context.name}]${currentPath}. Found rpc without name.`);
869 const description = this.extractValue(curRpc, "description") || undefined;
870 const configValue = this.extractValue(curRpc, "config");
871 const config = configValue == null ? true : configValue.toLocaleLowerCase() !== "false";
873 let inputViewId: string | undefined = undefined;
874 let outputViewId: string | undefined = undefined;
876 const input = this.extractNodes(curRpc, "input") || undefined;
877 const output = this.extractNodes(curRpc, "output") || undefined;
879 if (input && input.length > 0) {
880 const [inputView, inputSubViews] = this.extractSubViews(input[0], parentId, context, `${currentPath}/${context.name}:${curRpc.arg}`);
881 subViews.push(inputView, ...inputSubViews);
882 inputViewId = inputView.id;
885 if (output && output.length > 0) {
886 const [outputView, outputSubViews] = this.extractSubViews(output[0], parentId, context, `${currentPath}/${context.name}:${curRpc.arg}`);
887 subViews.push(outputView, ...outputSubViews);
888 outputViewId = outputView.id;
891 const element: ViewElementRpc = {
893 id: parentId === 0 ? `${context.name}:${curRpc.arg}` : curRpc.arg,
896 module: context.name || module.name || '',
898 description: description,
899 inputViewId: inputViewId,
900 outputViewId: outputViewId,
903 accRpc.push(element);
909 // if (!statement.arg) {
910 // throw new Error(`Module: [${context.name}]. Found statement without name.`);
913 const viewSpec: ViewSpecification = {
914 id: String(currentId),
915 parentView: String(parentId),
917 name: statement.arg != null ? statement.arg : undefined,
918 title: statement.arg != null ? statement.arg : undefined,
922 ifFeature: ifFeature,
924 elements: elements.reduce<{ [name: string]: ViewElement }>((acc, cur) => {
930 // evaluate canEdit depending on all conditions
931 Object.defineProperty(viewSpec, "canEdit", {
933 return Object.keys(viewSpec.elements).some(key => {
934 const elm = viewSpec.elements[key];
935 return (!isViewElementObjectOrList(elm) && elm.config);
940 // merge in all uses references and resolve groupings
941 const usesRefs = this.extractNodes(statement, "uses");
942 if (usesRefs && usesRefs.length > 0) {
944 viewSpec.uses = (viewSpec.uses || []);
945 const resolveFunctions : ((parentElementPath: string)=>void)[] = [];
947 for (let i = 0; i < usesRefs.length; ++i) {
948 const groupingName = usesRefs[i].arg;
950 throw new Error(`Module: [${context.name}]. Found an uses statement without a grouping name.`);
953 viewSpec.uses.push(this.resolveReferencePath(groupingName, context));
955 resolveFunctions.push((parentElementPath: string) => {
956 const groupingViewSpec = this.resolveGrouping(groupingName, context);
957 if (groupingViewSpec) {
960 const resolveFunc = groupingViewSpec.uses && groupingViewSpec.uses[ResolveFunction];
961 resolveFunc && resolveFunc(parentElementPath);
963 Object.keys(groupingViewSpec.elements).forEach(key => {
964 const elm = groupingViewSpec.elements[key];
965 // a useRef on root level need a namespace
966 viewSpec.elements[parentId === 0 ? `${module.name}:${key}` : key] = {
968 when: elm.when ? `(${groupingViewSpec.when}) and (${elm.when})` : groupingViewSpec.when,
969 ifFeature: elm.ifFeature ? `(${groupingViewSpec.ifFeature}) and (${elm.ifFeature})` : groupingViewSpec.ifFeature,
976 viewSpec.uses[ResolveFunction] = (parentElementPath: string) => {
977 const currentElementPath = `${parentElementPath} -> ${viewSpec.ns}:${viewSpec.name}`;
978 resolveFunctions.forEach(resolve => {
980 resolve(currentElementPath);
982 console.error(error);
985 // console.log("Resolved "+currentElementPath, viewSpec);
986 if (viewSpec?.uses) {
987 viewSpec.uses[ResolveFunction] = undefined;
991 this._groupingsToResolve.push(viewSpec);
994 return [viewSpec, subViews];
997 // https://tools.ietf.org/html/rfc7950#section-9.3.4
998 private static decimalRange = [
999 { min: -9223372036854775808, max: 9223372036854775807 },
1000 { min: -922337203685477580.8, max: 922337203685477580.7 },
1001 { min: -92233720368547758.08, max: 92233720368547758.07 },
1002 { min: -9223372036854775.808, max: 9223372036854775.807 },
1003 { min: -922337203685477.5808, max: 922337203685477.5807 },
1004 { min: -92233720368547.75808, max: 92233720368547.75807 },
1005 { min: -9223372036854.775808, max: 9223372036854.775807 },
1006 { min: -922337203685.4775808, max: 922337203685.4775807 },
1007 { min: -92233720368.54775808, max: 92233720368.54775807 },
1008 { min: -9223372036.854775808, max: 9223372036.854775807 },
1009 { min: -922337203.6854775808, max: 922337203.6854775807 },
1010 { min: -92233720.36854775808, max: 92233720.36854775807 },
1011 { min: -9223372.036854775808, max: 9223372.036854775807 },
1012 { min: -922337.2036854775808, max: 922337.2036854775807 },
1013 { min: -92233.72036854775808, max: 92233.72036854775807 },
1014 { min: -9223.372036854775808, max: 9223.372036854775807 },
1015 { min: -922.3372036854775808, max: 922.3372036854775807 },
1016 { min: -92.23372036854775808, max: 92.23372036854775807 },
1017 { min: -9.223372036854775808, max: 9.223372036854775807 },
1020 /** Extracts the UI View from the type in the cur statement. */
1021 private getViewElement(cur: Statement, module: Module, parentId: number, currentPath: string, isList: boolean): ViewElement {
1023 const type = this.extractValue(cur, "type");
1024 const defaultVal = this.extractValue(cur, "default") || undefined;
1025 const description = this.extractValue(cur, "description") || undefined;
1027 const configValue = this.extractValue(cur, "config");
1028 const config = configValue == null ? true : configValue.toLocaleLowerCase() !== "false";
1030 const extractRange = (min: number, max: number, property: string = "range"): { expression: Expression<YangRange> | undefined, min: number, max: number } => {
1031 const ranges = this.extractValue(this.extractNodes(cur, "type")[0]!, property) || undefined;
1032 const range = ranges ?.replace(/min/i, String(min)).replace(/max/i, String(max)).split("|").map(r => {
1033 let minValue: number;
1034 let maxValue: number;
1036 if (r.indexOf('..') > -1) {
1037 const [minStr, maxStr] = r.split('..');
1038 minValue = Number(minStr);
1039 maxValue = Number(maxStr);
1040 } else if (!isNaN(maxValue = Number(r && r.trim() )) ) {
1041 minValue = maxValue;
1047 if (minValue > min) min = minValue;
1048 if (maxValue < max) max = maxValue;
1058 expression: range && range.length === 1
1060 : range && range.length > 1
1061 ? { operation: "OR", arguments: range }
1066 const extractPattern = (): Expression<RegExp> | undefined => {
1067 const pattern = this.extractNodes(this.extractNodes(cur, "type")[0]!, "pattern").map(p => p.arg!).filter(p => !!p).map(p => `^${p.replace(/(?:\\(.))/g, '$1')}$`);
1068 return pattern && pattern.length == 1
1069 ? new RegExp(pattern[0])
1070 : pattern && pattern.length > 1
1071 ? { operation: "AND", arguments: pattern.map(p => new RegExp(p)) }
1075 const mandatory = this.extractValue(cur, "mandatory") === "true" || false;
1078 throw new Error(`Module: [${module.name}]. Found element without name.`);
1082 throw new Error(`Module: [${module.name}].[${cur.arg}]. Found element without type.`);
1085 const element: ViewElementBase = {
1086 id: parentId === 0 ? `${module.name}:${cur.arg}` : cur.arg,
1089 module: module.name || "",
1091 mandatory: mandatory,
1093 default: defaultVal,
1094 description: description
1097 if (type === "string") {
1098 const length = extractRange(0, +18446744073709551615, "length");
1102 length: length.expression,
1103 pattern: extractPattern(),
1105 } else if (type === "boolean") {
1110 } else if (type === "uint8") {
1111 const range = extractRange(0, +255);
1115 range: range.expression,
1118 units: this.extractValue(cur, "units") || undefined,
1119 format: this.extractValue(cur, "format") || undefined,
1121 } else if (type === "uint16") {
1122 const range = extractRange(0, +65535);
1126 range: range.expression,
1129 units: this.extractValue(cur, "units") || undefined,
1130 format: this.extractValue(cur, "format") || undefined,
1132 } else if (type === "uint32") {
1133 const range = extractRange(0, +4294967295);
1137 range: range.expression,
1140 units: this.extractValue(cur, "units") || undefined,
1141 format: this.extractValue(cur, "format") || undefined,
1143 } else if (type === "uint64") {
1144 const range = extractRange(0, +18446744073709551615);
1148 range: range.expression,
1151 units: this.extractValue(cur, "units") || undefined,
1152 format: this.extractValue(cur, "format") || undefined,
1154 } else if (type === "int8") {
1155 const range = extractRange(-128, +127);
1159 range: range.expression,
1162 units: this.extractValue(cur, "units") || undefined,
1163 format: this.extractValue(cur, "format") || undefined,
1165 } else if (type === "int16") {
1166 const range = extractRange(-32768, +32767);
1170 range: range.expression,
1173 units: this.extractValue(cur, "units") || undefined,
1174 format: this.extractValue(cur, "format") || undefined,
1176 } else if (type === "int32") {
1177 const range = extractRange(-2147483648, +2147483647);
1181 range: range.expression,
1184 units: this.extractValue(cur, "units") || undefined,
1185 format: this.extractValue(cur, "format") || undefined,
1187 } else if (type === "int64") {
1188 const range = extractRange(-9223372036854775808, +9223372036854775807);
1192 range: range.expression,
1195 units: this.extractValue(cur, "units") || undefined,
1196 format: this.extractValue(cur, "format") || undefined,
1198 } else if (type === "decimal64") {
1200 const fDigits = Number(this.extractValue(this.extractNodes(cur, "type")[0]!, "fraction-digits")) || -1;
1201 if (fDigits === -1) {
1202 throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found decimal64 with invalid fraction-digits.`);
1204 const range = extractRange(YangParser.decimalRange[fDigits].min, YangParser.decimalRange[fDigits].max);
1209 range: range.expression,
1212 units: this.extractValue(cur, "units") || undefined,
1213 format: this.extractValue(cur, "format") || undefined,
1215 } else if (type === "enumeration") {
1216 const typeNode = this.extractNodes(cur, "type")[0]!;
1217 const enumNodes = this.extractNodes(typeNode, "enum");
1220 uiType: "selection",
1221 options: enumNodes.reduce<{ key: string; value: string; description?: string }[]>((acc, enumNode) => {
1222 if (!enumNode.arg) {
1223 throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found option without name.`);
1225 const ifClause = this.extractValue(enumNode, "if-feature");
1226 const value = this.extractValue(enumNode, "value");
1227 const enumOption = {
1229 value: value != null ? value : enumNode.arg,
1230 description: this.extractValue(enumNode, "description") || undefined
1232 // todo: ❗ handle the if clause ⚡
1233 acc.push(enumOption);
1237 } else if (type === "leafref") {
1238 const typeNode = this.extractNodes(cur, "type")[0]!;
1239 const vPath = this.extractValue(typeNode, "path");
1241 throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found leafref without path.`);
1243 const refPath = this.resolveReferencePath(vPath, module);
1244 const resolve = this.resolveReference.bind(this);
1245 const res: ViewElement = {
1247 uiType: "reference",
1248 referencePath: refPath,
1249 ref(this: ViewElement, currentPath: string) {
1250 const elementPath = `${currentPath}/${cur.arg}`;
1252 const result = resolve(refPath, elementPath);
1253 if (!result) return undefined;
1255 const [resolvedElement, resolvedPath] = result;
1256 return resolvedElement && [{
1260 config: this.config,
1261 mandatory: this.mandatory,
1262 isList: this.isList,
1263 default: this.default,
1264 description: this.description,
1265 } as ViewElement , resolvedPath] || undefined;
1269 } else if (type === "identityref") {
1270 const typeNode = this.extractNodes(cur, "type")[0]!;
1271 const base = this.extractValue(typeNode, "base");
1273 throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found identityref without base.`);
1275 const res: ViewElement = {
1277 uiType: "selection",
1280 this._identityToResolve.push(() => {
1281 const identity: Identity = this.resolveIdentity(base, module);
1283 throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Could not resolve identity [${base}].`);
1285 if (!identity.values || identity.values.length === 0) {
1286 throw new Error(`Identity: [${base}] has no values.`);
1288 res.options = identity.values.map(val => ({
1291 description: val.description
1295 } else if (type === "empty") {
1296 // todo: ❗ handle empty ⚡
1297 /* 9.11. The empty Built-In Type
1298 The empty built-in type represents a leaf that does not have any
1299 value, it conveys information by its presence or absence. */
1304 } else if (type === "union") {
1305 // todo: ❗ handle union ⚡
1306 /* 9.12. The union Built-In Type */
1307 const typeNode = this.extractNodes(cur, "type")[0]!;
1308 const typeNodes = this.extractNodes(typeNode, "type");
1310 const resultingElement = {
1314 } as ViewElementUnion;
1316 const resolveUnion = () => {
1317 resultingElement.elements.push(...typeNodes.map(node => {
1318 const stm: Statement = {
1321 ...(cur.sub ?.filter(s => s.key !== "type") || []),
1326 ...this.getViewElement(stm, module, parentId, currentPath, isList),
1332 this._unionsToResolve.push(resolveUnion);
1334 return resultingElement;
1335 } else if (type === "bits") {
1336 const typeNode = this.extractNodes(cur, "type")[0]!;
1337 const bitNodes = this.extractNodes(typeNode, "bit");
1341 flags: bitNodes.reduce<{ [name: string]: number | undefined; }>((acc, bitNode) => {
1343 throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found bit without name.`);
1345 const ifClause = this.extractValue(bitNode, "if-feature");
1346 const pos = Number(this.extractValue(bitNode, "position"));
1347 acc[bitNode.arg] = pos === pos ? pos : undefined;
1351 } else if (type === "binary") {
1355 length: extractRange(0, +18446744073709551615, "length"),
1357 } else if (type === "instance-identifier") {
1358 // https://tools.ietf.org/html/rfc7950#page-168
1362 length: extractRange(0, +18446744073709551615, "length"),
1365 // not a build in type, need to resolve type
1366 let typeRef = this.resolveType(type, module);
1367 if (typeRef == null) console.error(new Error(`Could not resolve type ${type} in [${module.name}][${currentPath}].`));
1370 if (isViewElementString(typeRef)) {
1371 typeRef = this.resolveStringType(typeRef, extractPattern(), extractRange(0, +18446744073709551615));
1372 } else if (isViewElementNumber(typeRef)) {
1373 typeRef = this.resolveNumberType(typeRef, extractRange(typeRef.min, typeRef.max));
1376 // spoof date type here from special string type
1377 if ((type === 'date-and-time' || type.endsWith(':date-and-time') ) && typeRef.module === "ietf-yang-types") {
1381 description: description,
1389 description: description,
1394 private resolveStringType(parentElement: ViewElementString, pattern: Expression<RegExp> | undefined, length: { expression: Expression<YangRange> | undefined, min: number, max: number }) {
1397 pattern: pattern != null && parentElement.pattern
1398 ? { operation: "AND", arguments: [pattern, parentElement.pattern] }
1399 : parentElement.pattern
1400 ? parentElement.pattern
1402 length: length.expression != null && parentElement.length
1403 ? { operation: "AND", arguments: [length.expression, parentElement.length] }
1404 : parentElement.length
1405 ? parentElement.length
1406 : length ?.expression,
1407 } as ViewElementString;
1410 private resolveNumberType(parentElement: ViewElementNumber, range: { expression: Expression<YangRange> | undefined, min: number, max: number }) {
1413 range: range.expression != null && parentElement.range
1414 ? { operation: "AND", arguments: [range.expression, parentElement.range] }
1415 : parentElement.range
1416 ? parentElement.range
1420 } as ViewElementNumber;
1423 private resolveReferencePath(vPath: string, module: Module) {
1424 const vPathParser = /(?:(?:([^\/\:]+):)?([^\/]+))/g // 1 = opt: namespace / 2 = property
1425 return vPath.replace(vPathParser, (_, ns, property) => {
1426 const nameSpace = ns && module.imports[ns] || module.name;
1427 return `${nameSpace}:${property}`;
1431 private resolveReference(vPath: string, currentPath: string) {
1432 const vPathParser = /(?:(?:([^\/\[\]\:]+):)?([^\/\[\]]+)(\[[^\]]+\])?)/g // 1 = opt: namespace / 2 = property / 3 = opt: indexPath
1433 let element: ViewElement | null = null;
1434 let moduleName = "";
1436 const vPathParts = splitVPath(vPath, vPathParser).map(p => ({ ns: p[1], property: p[2], ind: p[3] }));
1437 const resultPathParts = !vPath.startsWith("/")
1438 ? splitVPath(currentPath, vPathParser).map(p => { moduleName = p[1] || moduleName ; return { ns: moduleName, property: p[2], ind: p[3] } })
1441 for (let i = 0; i < vPathParts.length; ++i) {
1442 const vPathPart = vPathParts[i];
1443 if (vPathPart.property === "..") {
1444 resultPathParts.pop();
1445 } else if (vPathPart.property !== ".") {
1446 resultPathParts.push(vPathPart);
1450 // resolve element by path
1451 for (let j = 0; j < resultPathParts.length; ++j) {
1452 const pathPart = resultPathParts[j];
1454 moduleName = pathPart.ns;
1455 const rootModule = this._modules[moduleName];
1456 if (!rootModule) throw new Error("Could not resolve module [" + moduleName + "].\r\n" + vPath);
1457 element = rootModule.elements[`${pathPart.ns}:${pathPart.property}`];
1458 } else if (element && isViewElementObjectOrList(element)) {
1459 const view: ViewSpecification = this._views[+element.viewId];
1460 if (moduleName !== pathPart.ns) {
1461 moduleName = pathPart.ns;
1463 element = view.elements[pathPart.property] || view.elements[`${moduleName}:${pathPart.property}`];
1465 throw new Error("Could not resolve reference.\r\n" + vPath);
1467 if (!element) throw new Error("Could not resolve path [" + pathPart.property + "] in [" + currentPath + "] \r\n" + vPath);
1470 moduleName = ""; // create the vPath for the resolved element, do not add the element itself this will be done later in the res(...) function
1471 return [element, resultPathParts.slice(0,-1).map(p => `${moduleName !== p.ns ? `${moduleName=p.ns}:` : ""}${p.property}${p.ind || ''}`).join("/")];
1474 private resolveView(vPath: string) {
1475 const vPathParser = /(?:(?:([^\/\[\]\:]+):)?([^\/\[\]]+)(\[[^\]]+\])?)/g // 1 = opt: namespace / 2 = property / 3 = opt: indexPath
1476 let element: ViewElement | null = null;
1477 let partMatch: RegExpExecArray | null;
1478 let view: ViewSpecification | null = null;
1479 let moduleName = "";
1481 partMatch = vPathParser.exec(vPath);
1483 if (element === null) {
1484 moduleName = partMatch[1]!;
1485 const rootModule = this._modules[moduleName];
1486 if (!rootModule) return null;
1487 element = rootModule.elements[`${moduleName}:${partMatch[2]!}`];
1488 } else if (isViewElementObjectOrList(element)) {
1489 view = this._views[+element.viewId];
1490 if (moduleName !== partMatch[1]) {
1491 moduleName = partMatch[1];
1492 element = view.elements[`${moduleName}:${partMatch[2]}`];
1494 element = view.elements[partMatch[2]];
1499 if (!element) return null;
1502 return element && isViewElementObjectOrList(element) && this._views[+element.viewId] || null;
1505 private resolveType(type: string, module: Module) {
1506 const collonInd = type.indexOf(":");
1507 const preFix = collonInd > -1 ? type.slice(0, collonInd) : "";
1508 const typeName = collonInd > -1 ? type.slice(collonInd + 1) : type;
1511 ? this._modules[module.imports[preFix]].typedefs[typeName]
1512 : module.typedefs[typeName];
1516 private resolveGrouping(grouping: string, module: Module) {
1517 const collonInd = grouping.indexOf(":");
1518 const preFix = collonInd > -1 ? grouping.slice(0, collonInd) : "";
1519 const groupingName = collonInd > -1 ? grouping.slice(collonInd + 1) : grouping;
1522 ? this._modules[module.imports[preFix]].groupings[groupingName]
1523 : module.groupings[groupingName];
1527 private resolveIdentity(identity: string, module: Module) {
1528 const collonInd = identity.indexOf(":");
1529 const preFix = collonInd > -1 ? identity.slice(0, collonInd) : "";
1530 const identityName = collonInd > -1 ? identity.slice(collonInd + 1) : identity;
1533 ? this._modules[module.imports[preFix]].identities[identityName]
1534 : module.identities[identityName];