Merge "SDN-R add updated featureaggregator"
[ccsdk/features.git] / sdnr / wt / odlux / apps / configurationApp / src / yang / yangParser.ts
1 /**
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
9  *
10  * http://www.apache.org/licenses/LICENSE-2.0
11  *
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
15  * the License.
16  * ============LICENSE_END==========================================================================
17  */
18
19
20 import { Token, Statement, Module, Identity } from "../models/yang";
21 import { ViewSpecification, ViewElement, isViewElementObjectOrList, ViewElementBase, isViewElementReference, ViewElementChoise, ViewElementBinary, ViewElementString, isViewElementString, isViewElementNumber, ViewElementNumber, Expression, YangRange, ViewElementUnion } from "../models/uiModels";
22 import { yangService } from "../services/yangService";
23
24 export const splitVPath = (vPath: string, vPathParser: RegExp): RegExpMatchArray[] => {
25   const pathParts: RegExpMatchArray[] = [];
26   let partMatch: RegExpExecArray | null;
27   if (vPath) do {
28     partMatch = vPathParser.exec(vPath);
29     if (partMatch) {
30       pathParts.push(partMatch);
31     }
32   } while (partMatch)
33   return pathParts;
34 }
35
36 class YangLexer {
37
38   private pos: number = 0;
39   private buf: string = "";
40
41   constructor(input: string) {
42     this.pos = 0;
43     this.buf = input;
44   }
45
46   private _optable: { [key: string]: string } = {
47     ';': 'SEMI',
48     '{': 'L_BRACE',
49     '}': 'R_BRACE',
50   };
51
52   private _isNewline(char: string): boolean {
53     return char === '\r' || char === '\n';
54   }
55
56   private _isWhitespace(char: string): boolean {
57     return char === ' ' || char === '\t' || this._isNewline(char);
58   }
59
60   private _isDigit(char: string): boolean {
61     return char >= '0' && char <= '9';
62   }
63
64   private _isAlpha(char: string): boolean {
65     return (char >= 'a' && char <= 'z') ||
66       (char >= 'A' && char <= 'Z')
67   }
68
69   private _isAlphanum(char: string): boolean {
70     return this._isAlpha(char) || this._isDigit(char) ||
71       char === '_' || char === '-' || char === '.';
72   }
73
74   private _skipNontokens() {
75     while (this.pos < this.buf.length) {
76       const char = this.buf.charAt(this.pos);
77       if (this._isWhitespace(char)) {
78         this.pos++;
79       } else {
80         break;
81       }
82     }
83   }
84
85   private _processString(terminator: string | null): Token {
86     // this.pos points at the opening quote. Find the ending quote.
87     let end_index = this.pos + 1;
88     while (end_index < this.buf.length) {
89       const char = this.buf.charAt(end_index);
90       if (char === "\\") {
91         end_index += 2;
92         continue;
93       };
94       if (terminator === null && (this._isWhitespace(char) || this._optable[char] !== undefined) || char === terminator) {
95         break;
96       }
97       end_index++;
98     }
99
100     if (end_index >= this.buf.length) {
101       throw Error('Unterminated quote at ' + this.pos);
102     } else {
103       const start = this.pos + (terminator ? 1 : 0);
104       const end = end_index;
105       const tok = {
106         name: 'STRING',
107         value: this.buf.substring(start, end),
108         start,
109         end
110       };
111       this.pos = terminator ? end + 1 : end;
112       return tok;
113     }
114   }
115
116   private _processIdentifier(): Token {
117     let endpos = this.pos + 1;
118     while (endpos < this.buf.length &&
119       this._isAlphanum(this.buf.charAt(endpos))) {
120       endpos++;
121     }
122
123     const tok = {
124       name: 'IDENTIFIER',
125       value: this.buf.substring(this.pos, endpos),
126       start: this.pos,
127       end: endpos
128     };
129     this.pos = endpos;
130     return tok;
131   }
132
133   private _processNumber(): Token {
134     let endpos = this.pos + 1;
135     while (endpos < this.buf.length &&
136       this._isDigit(this.buf.charAt(endpos))) {
137       endpos++;
138     }
139
140     const tok = {
141       name: 'NUMBER',
142       value: this.buf.substring(this.pos, endpos),
143       start: this.pos,
144       end: endpos
145     };
146     this.pos = endpos;
147     return tok;
148   }
149
150   private _processLineComment() {
151     var endpos = this.pos + 2;
152     // Skip until the end of the line
153     while (endpos < this.buf.length && !this._isNewline(this.buf.charAt(endpos))) {
154       endpos++;
155     }
156     this.pos = endpos + 1;
157   }
158
159   private _processBlockComment() {
160     var endpos = this.pos + 2;
161     // Skip until the end of the line
162     while (endpos < this.buf.length && !((this.buf.charAt(endpos) === "/" && this.buf.charAt(endpos - 1) === "*"))) {
163       endpos++;
164     }
165     this.pos = endpos + 1;
166   }
167
168   public tokenize(): Token[] {
169     const result: Token[] = [];
170     this._skipNontokens();
171     while (this.pos < this.buf.length) {
172
173       const char = this.buf.charAt(this.pos);
174       const op = this._optable[char];
175
176       if (op !== undefined) {
177         result.push({ name: op, value: char, start: this.pos, end: ++this.pos });
178       } else if (this._isAlpha(char)) {
179         result.push(this._processIdentifier());
180         this._skipNontokens();
181         const peekChar = this.buf.charAt(this.pos);
182         if (this._optable[peekChar] === undefined) {
183           result.push((peekChar !== "'" && peekChar !== '"')
184             ? this._processString(null)
185             : this._processString(peekChar));
186         }
187       } else if (char === '/' && this.buf.charAt(this.pos + 1) === "/") {
188         this._processLineComment();
189       } else if (char === '/' && this.buf.charAt(this.pos + 1) === "*") {
190         this._processBlockComment();
191       } else {
192         throw Error('Token error at ' + this.pos + " " + this.buf[this.pos]);
193       }
194       this._skipNontokens();
195     }
196     return result;
197   }
198
199   public tokenize2(): Statement {
200     let stack: Statement[] = [{ key: "ROOT", sub: [] }];
201     let current: Statement | null = null;
202
203     this._skipNontokens();
204     while (this.pos < this.buf.length) {
205
206       const char = this.buf.charAt(this.pos);
207       const op = this._optable[char];
208
209       if (op !== undefined) {
210         if (op === "L_BRACE") {
211           current && stack.unshift(current);
212           current = null;
213         } else if (op === "R_BRACE") {
214           current = stack.shift() || null;
215         }
216         this.pos++;
217       } else if (this._isAlpha(char)) {
218         const key = this._processIdentifier().value;
219         this._skipNontokens();
220         let peekChar = this.buf.charAt(this.pos);
221         let arg = undefined;
222         if (this._optable[peekChar] === undefined) {
223           arg = (peekChar === '"' || peekChar === "'")
224             ? this._processString(peekChar).value
225             : this._processString(null).value;
226         }
227         do {
228           this._skipNontokens();
229           peekChar = this.buf.charAt(this.pos);
230           if (peekChar !== "+") break;
231           this.pos++;
232           this._skipNontokens();
233           peekChar = this.buf.charAt(this.pos);
234           arg += (peekChar === '"' || peekChar === "'")
235             ? this._processString(peekChar).value
236             : this._processString(null).value;
237         } while (true);
238         current = { key, arg, sub: [] };
239         stack[0].sub!.push(current);
240       } else if (char === '/' && this.buf.charAt(this.pos + 1) === "/") {
241         this._processLineComment();
242       } else if (char === '/' && this.buf.charAt(this.pos + 1) === "*") {
243         this._processBlockComment();
244       } else {
245         throw Error('Token error at ' + this.pos + " " + this.buf.slice(this.pos - 10, this.pos + 10));
246       }
247       this._skipNontokens();
248     }
249     if (stack[0].key !== "ROOT" || !stack[0].sub![0]) {
250       throw new Error("Internal Perser Error");
251     }
252     return stack[0].sub![0];
253   }
254 }
255
256 export class YangParser {
257   private _groupingsToResolve: (() => void)[] = [];
258   private _identityToResolve: (() => void)[] = [];
259   private _unionsToResolve: (() => void)[] = [];
260
261
262   private _modules: { [name: string]: Module } = {};
263   private _views: ViewSpecification[] = [{
264     id: "0",
265     name: "root",
266     language: "en-US",
267     canEdit: false,
268     parentView: "0",
269     title: "root",
270     elements: {},
271   }];
272
273   public static ResolveStack = Symbol("ResolveStack");
274
275   constructor() {
276
277   }
278
279   public get modules() {
280     return this._modules;
281   }
282
283   public get views() {
284     return this._views;
285   }
286
287   public async addCapability(capability: string, version?: string) {
288     // do not add twice
289     if (this._modules[capability]) {
290       return;
291     }
292
293     const data = await yangService.getCapability(capability, version);
294     if (!data) {
295       throw new Error(`Could not load yang file for ${capability}.`);
296     }
297
298     const rootStatement = new YangLexer(data).tokenize2();
299
300     if (rootStatement.key !== "module") {
301       throw new Error(`Root element of ${capability} is not a module.`);
302     }
303     if (rootStatement.arg !== capability) {
304       throw new Error(`Root element capability ${rootStatement.arg} does not requested ${capability}.`);
305     }
306
307     const module = this._modules[capability] = {
308       name: rootStatement.arg,
309       revisions: {},
310       imports: {},
311       features: {},
312       identities: {},
313       augments: {},
314       groupings: {},
315       typedefs: {},
316       views: {},
317       elements: {}
318     };
319
320     await this.handleModule(module, rootStatement, capability);
321   }
322
323   private async handleModule(module: Module, rootStatement: Statement, capability: string) {
324
325     // extract namespace && prefix
326     module.namespace = this.extractValue(rootStatement, "namespace");
327     module.prefix = this.extractValue(rootStatement, "prefix");
328     if (module.prefix) {
329       module.imports[module.prefix] = capability;
330     }
331
332     // extract revisions
333     const revisions = this.extractNodes(rootStatement, "revision");
334     module.revisions = {
335       ...module.revisions,
336       ...revisions.reduce<{ [version: string]: {} }>((acc, version) => {
337         if (!version.arg) {
338           throw new Error(`Module [${module.name}] has a version w/o version number.`);
339         }
340         const description = this.extractValue(version, "description");
341         const reference = this.extractValue(version, "reference");
342         acc[version.arg] = {
343           description,
344           reference,
345         };
346         return acc;
347       }, {})
348     };
349
350     // extract features
351     const features = this.extractNodes(rootStatement, "feature");
352     module.features = {
353       ...module.features,
354       ...features.reduce<{ [version: string]: {} }>((acc, feature) => {
355         if (!feature.arg) {
356           throw new Error(`Module [${module.name}] has a feature w/o name.`);
357         }
358         const description = this.extractValue(feature, "description");
359         acc[feature.arg] = {
360           description,
361         };
362         return acc;
363       }, {})
364     };
365
366     // extract imports
367     const imports = this.extractNodes(rootStatement, "import");
368     module.imports = {
369       ...module.imports,
370       ...imports.reduce<{ [key: string]: string }>((acc, imp) => {
371         const prefix = imp.sub && imp.sub.filter(s => s.key === "prefix");
372         if (!imp.arg) {
373           throw new Error(`Module [${module.name}] has an import with neither name nor prefix.`);
374         }
375         acc[prefix && prefix.length === 1 && prefix[0].arg || imp.arg] = imp.arg;
376         return acc;
377       }, {})
378     };
379
380     // import all required files
381     if (imports) for (let ind = 0; ind < imports.length; ++ind) {
382       await this.addCapability(imports[ind].arg!);
383     }
384
385     this.extractTypeDefinitions(rootStatement, module, "");
386
387     this.extractIdentites(rootStatement, 0, module, "");
388
389     const groupings = this.extractGroupings(rootStatement, 0, module, "");
390     this._views.push(...groupings);
391
392     const augments = this.extractAugments(rootStatement, 0, module, "");
393     this._views.push(...augments);
394
395     // the default for config on module level is config = true;
396     const [currentView, subViews] = this.extractSubViews(rootStatement, 0, module, "");
397     this._views.push(currentView, ...subViews);
398
399     // create the root elements for this module
400     module.elements = currentView.elements;
401     Object.keys(module.elements).forEach(key => {
402       const viewElement = module.elements[key];
403       if (!isViewElementObjectOrList(viewElement)) {
404         throw new Error(`Module: [${module}]. Only List or Object allowed on root level.`);
405       }
406       const viewIdIndex = Number(viewElement.viewId);
407       module.views[key] = this._views[viewIdIndex];
408       this._views[0].elements[key] = module.elements[key];
409     });
410     return module;
411   }
412
413   public postProcess() {
414
415     // execute all post processes like resolving in propper order
416     this._unionsToResolve.forEach(cb => {
417       try { cb(); } catch (error) {
418         console.warn(error.message);
419       }
420     });
421
422     // process all groupings
423     this._groupingsToResolve.forEach(cb => {
424       try { cb(); } catch (error) {
425         console.warn(`Error resolving: [${error.message}]`);
426       }
427     });
428
429     // process all augmentations
430     Object.keys(this.modules).forEach(modKey => {
431       const module = this.modules[modKey];
432       Object.keys(module.augments).forEach(augKey => {
433         const augments = module.augments[augKey];
434         const viewSpec = this.resolveView(augKey);
435         if (!viewSpec) console.warn(`Could not find view to augment [${augKey}] in [${module.name}].`);
436         if (augments && viewSpec) {
437           augments.forEach(augment => Object.keys(augment.elements).forEach(key => {
438             const elm = augment.elements[key];
439             viewSpec.elements[key] = {
440               ...augment.elements[key],
441               when: elm.when ? `(${augment.when}) and (${elm.when})` : augment.when,
442               ifFeature: elm.ifFeature ? `(${augment.ifFeature}) and (${elm.ifFeature})` : augment.ifFeature,
443             };
444           }));
445         }
446       });
447     });
448
449     // process Identities
450     const traverseIdentity = (identities: Identity[]) => {
451       const result: Identity[] = [];
452       for (let identity of identities) {
453         if (identity.children && identity.children.length > 0) {
454           result.push(...traverseIdentity(identity.children));
455         } else {
456           result.push(identity);
457         }
458       }
459       return result;
460     }
461
462
463     const baseIdentites: Identity[] = [];
464     Object.keys(this.modules).forEach(modKey => {
465       const module = this.modules[modKey];
466       Object.keys(module.identities).forEach(idKey => {
467         const identity = module.identities[idKey];
468         if (identity.base != null) {
469           const base = this.resolveIdentity(identity.base, module);
470           base.children ?.push(identity);
471         } else {
472           baseIdentites.push(identity);
473         }
474       });
475     });
476     baseIdentites.forEach(identity => {
477       identity.values = identity.children && traverseIdentity(identity.children) || [];
478     });
479
480     this._identityToResolve.forEach(cb => {
481       try { cb(); } catch (error) {
482         console.warn(error.message);
483       }
484     });
485   };
486
487   private _nextId = 1;
488   private get nextId() {
489     return this._nextId++;
490   }
491
492   private extractNodes(statement: Statement, key: string): Statement[] {
493     return statement.sub && statement.sub.filter(s => s.key === key) || [];
494   }
495
496   private extractValue(statement: Statement, key: string): string | undefined;
497   private extractValue(statement: Statement, key: string, parser: RegExp): RegExpExecArray | undefined;
498   private extractValue(statement: Statement, key: string, parser?: RegExp): string | RegExpExecArray | undefined {
499     const typeNodes = this.extractNodes(statement, key);
500     const rawValue = typeNodes.length > 0 && typeNodes[0].arg || undefined;
501     return parser
502       ? rawValue && parser.exec(rawValue) || undefined
503       : rawValue;
504   }
505
506   private extractTypeDefinitions(statement: Statement, module: Module, currentPath: string): void {
507     const typedefs = this.extractNodes(statement, "typedef");
508     typedefs && typedefs.forEach(def => {
509       if (!def.arg) {
510         throw new Error(`Module: [${module.name}]. Found typefed without name.`);
511       }
512       module.typedefs[def.arg] = this.getViewElement(def, module, 0, currentPath, false);
513     });
514   }
515
516   /** Handles Goupings like named Container */
517   private extractGroupings(statement: Statement, parentId: number, module: Module, currentPath: string): ViewSpecification[] {
518     const subViews: ViewSpecification[] = [];
519     const groupings = this.extractNodes(statement, "grouping");
520     if (groupings && groupings.length > 0) {
521       subViews.push(...groupings.reduce<ViewSpecification[]>((acc, cur) => {
522         if (!cur.arg) {
523           throw new Error(`Module: [${module.name}][${currentPath}]. Found grouping without name.`);
524         }
525         const grouping = cur.arg;
526
527         // the default for config on module level is config = true;
528         const [currentView, subViews] = this.extractSubViews(cur, parentId, module, currentPath);
529         grouping && (module.groupings[grouping] = currentView);
530         acc.push(currentView, ...subViews);
531         return acc;
532       }, []));
533     }
534
535     return subViews;
536   }
537
538   /** Handles Augmants also like named Container */
539   private extractAugments(statement: Statement, parentId: number, module: Module, currentPath: string): ViewSpecification[] {
540     const subViews: ViewSpecification[] = [];
541     const augments = this.extractNodes(statement, "augment");
542     if (augments && augments.length > 0) {
543       subViews.push(...augments.reduce<ViewSpecification[]>((acc, cur) => {
544         if (!cur.arg) {
545           throw new Error(`Module: [${module.name}][${currentPath}]. Found augment without path.`);
546         }
547         const augment = this.resolveReferencePath(cur.arg, module);
548
549         // the default for config on module level is config = true;
550         const [currentView, subViews] = this.extractSubViews(cur, parentId, module, currentPath);
551         if (augment) {
552           module.augments[augment] = module.augments[augment] || [];
553           module.augments[augment].push(currentView);
554         }
555         acc.push(currentView, ...subViews);
556         return acc;
557       }, []));
558     }
559
560     return subViews;
561   }
562
563   /** Handles Identities  */
564   private extractIdentites(statement: Statement, parentId: number, module: Module, currentPath: string) {
565     const identities = this.extractNodes(statement, "identity");
566     module.identities = identities.reduce<{ [name: string]: Identity }>((acc, cur) => {
567       if (!cur.arg) {
568         throw new Error(`Module: [${module.name}][${currentPath}]. Found identiy without name.`);
569       }
570       acc[cur.arg] = {
571         id: `${module.name}:${cur.arg}`,
572         label: cur.arg,
573         base: this.extractValue(cur, "base"),
574         description: this.extractValue(cur, "description"),
575         reference: this.extractValue(cur, "reference"),
576         children: []
577       }
578       return acc;
579     }, {});
580   }
581
582   private extractSubViews(statement: Statement, parentId: number, module: Module, currentPath: string): [ViewSpecification, ViewSpecification[]] {
583     const subViews: ViewSpecification[] = [];
584     const currentId = this.nextId;
585     let elements: ViewElement[] = [];
586
587     const configValue = this.extractValue(statement, "config");
588     const config = configValue == null ? true : configValue.toLocaleLowerCase() !== "false";
589
590     // extract conditions
591     const ifFeature = this.extractValue(statement, "if-feature");
592     const whenCondition = this.extractValue(statement, "when");
593     if (whenCondition) console.warn("Found in [" + module.name + "]" + currentPath + " when: " + whenCondition);
594
595     // extract all container
596     const container = this.extractNodes(statement, "container");
597     if (container && container.length > 0) {
598       subViews.push(...container.reduce<ViewSpecification[]>((acc, cur) => {
599         if (!cur.arg) {
600           throw new Error(`Module: [${module.name}]${currentPath}. Found container without name.`);
601         }
602         const [currentView, subViews] = this.extractSubViews(cur, currentId, module, `${currentPath}/${module.name}:${cur.arg}`);
603         elements.push({
604           id: parentId === 0 ? `${module.name}:${cur.arg}` : cur.arg,
605           label: cur.arg,
606           uiType: "object",
607           viewId: currentView.id,
608           config: config
609         });
610         acc.push(currentView, ...subViews);
611         return acc;
612       }, []));
613     }
614
615     // process all lists
616     // a list is a list of containers with the leafs contained in the list
617     const lists = this.extractNodes(statement, "list");
618     if (lists && lists.length > 0) {
619       subViews.push(...lists.reduce<ViewSpecification[]>((acc, cur) => {
620         if (!cur.arg) {
621           throw new Error(`Module: [${module.name}]${currentPath}. Found list without name.`);
622         }
623         const key = this.extractValue(cur, "key") || undefined;
624         if (config && !key) {
625           throw new Error(`Module: [${module.name}]${currentPath}. Found configurable list without key.`);
626         }
627         const [currentView, subViews] = this.extractSubViews(cur, currentId, module, `${currentPath}/${module.name}:${cur.arg}`);
628         elements.push({
629           id: parentId === 0 ? `${module.name}:${cur.arg}` : cur.arg,
630           label: cur.arg,
631           isList: true,
632           uiType: "object",
633           viewId: currentView.id,
634           key: key,
635           config: config
636         });
637         acc.push(currentView, ...subViews);
638         return acc;
639       }, []));
640     }
641
642     // process all leaf-lists
643     // a leaf-list is a list of some type
644     const leafLists = this.extractNodes(statement, "leaf-list");
645     if (leafLists && leafLists.length > 0) {
646       elements.push(...leafLists.reduce<ViewElement[]>((acc, cur) => {
647         const element = this.getViewElement(cur, module, parentId, currentPath, true);
648         element && acc.push(element);
649         return acc;
650       }, []));
651     }
652
653     // process all leafs
654     // a leaf is mainly a property of an object
655     const leafs = this.extractNodes(statement, "leaf");
656     if (leafs && leafs.length > 0) {
657       elements.push(...leafs.reduce<ViewElement[]>((acc, cur) => {
658         const element = this.getViewElement(cur, module, parentId, currentPath, false);
659         element && acc.push(element);
660         return acc;
661       }, []));
662     }
663
664
665     const choiceStms = this.extractNodes(statement, "choice");
666     if (choiceStms && choiceStms.length > 0) {
667       elements.push(...choiceStms.reduce<ViewElementChoise[]>((accChoise, curChoise) => {
668         if (!curChoise.arg) {
669           throw new Error(`Module: [${module.name}]${currentPath}. Found choise without name.`);
670         }
671         // extract all cases like containers
672         const cases: { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } }[] = [];
673         const caseStms = this.extractNodes(curChoise, "case");
674         if (caseStms && caseStms.length > 0) {
675           cases.push(...caseStms.reduce((accCase, curCase) => {
676             if (!curCase.arg) {
677               throw new Error(`Module: [${module.name}]${currentPath}/${curChoise.arg}. Found case without name.`);
678             }
679             const description = this.extractValue(curCase, "description") || undefined;
680             const [caseView, caseSubViews] = this.extractSubViews(curCase, parentId, module, `${currentPath}/${module.name}:${curChoise.arg}`);
681             subViews.push(caseView, ...caseSubViews);
682
683             const caseDef: { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } } = {
684               id: parentId === 0 ? `${module.name}:${curCase.arg}` : curCase.arg,
685               label: curCase.arg,
686               description: description,
687               elements: caseView.elements
688             };
689             accCase.push(caseDef);
690             return accCase;
691           }, [] as { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } }[]));
692         }
693
694         // extract all simple cases (one case per leaf, container, etc.)
695         const [choiseView, choiseSubViews] = this.extractSubViews(curChoise, parentId, module, `${currentPath}/${module.name}:${curChoise.arg}`);
696         subViews.push(choiseView, ...choiseSubViews);
697         cases.push(...Object.keys(choiseView.elements).reduce((accElm, curElm) => {
698           const elm = choiseView.elements[curElm];
699           const caseDef: { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } } = {
700             id: elm.id,
701             label: elm.label,
702             description: elm.description,
703             elements: { [elm.id]: elm }
704           };
705           accElm.push(caseDef);
706           return accElm;
707         }, [] as { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } }[]));
708
709         const description = this.extractValue(curChoise, "description") || undefined;
710         const configValue = this.extractValue(curChoise, "config");
711         const config = configValue == null ? true : configValue.toLocaleLowerCase() !== "false";
712
713         const mandatory = this.extractValue(curChoise, "mandatory") === "true" || false;
714
715         const element: ViewElementChoise = {
716           uiType: "choise",
717           id: parentId === 0 ? `${module.name}:${curChoise.arg}` : curChoise.arg,
718           label: curChoise.arg,
719           config: config,
720           mandatory: mandatory,
721           description: description,
722           cases: cases.reduce((acc, cur) => {
723             acc[cur.id] = cur;
724             return acc;
725           }, {} as { [name: string]: { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } } })
726         };
727
728         accChoise.push(element);
729         return accChoise;
730       }, []));
731     }
732
733     const rpcs = this.extractNodes(statement, "rpc");
734     if (rpcs && rpcs.length > 0) {
735       // todo:
736     }
737
738     if (!statement.arg) {
739       throw new Error(`Module: [${module.name}]. Found statement without name.`);
740     }
741
742     const viewSpec: ViewSpecification = {
743       id: String(currentId),
744       parentView: String(parentId),
745       name: statement.arg,
746       title: statement.arg,
747       language: "en-us",
748       canEdit: false,
749       ifFeature: ifFeature,
750       when: whenCondition,
751       elements: elements.reduce<{ [name: string]: ViewElement }>((acc, cur) => {
752         acc[cur.id] = cur;
753         return acc;
754       }, {}),
755     };
756
757     // evaluate canEdit depending on all conditions
758     Object.defineProperty(viewSpec, "canEdit", {
759       get: () => {
760         return Object.keys(viewSpec.elements).some(key => {
761           const elm = viewSpec.elements[key];
762           return (!isViewElementObjectOrList(elm) && elm.config);
763         });
764       }
765     });
766
767     // merge in all uses references and resolve groupings
768     const usesRefs = this.extractNodes(statement, "uses");
769     if (usesRefs && usesRefs.length > 0) {
770
771       viewSpec.uses = (viewSpec.uses || []);
772       for (let i = 0; i < usesRefs.length; ++i) {
773         const groupingName = usesRefs[i].arg;
774         if (!groupingName) {
775           throw new Error(`Module: [${module.name}]. Found an uses statement without a grouping name.`);
776         }
777
778         viewSpec.uses.push(this.resolveReferencePath(groupingName, module));
779
780         this._groupingsToResolve.push(() => {
781           const groupingViewSpec = this.resolveGrouping(groupingName, module);
782           if (groupingViewSpec) {
783             Object.keys(groupingViewSpec.elements).forEach(key => {
784               const elm = groupingViewSpec.elements[key];
785               viewSpec.elements[key] = {
786                 ...groupingViewSpec.elements[key],
787                 when: elm.when ? `(${groupingViewSpec.when}) and (${elm.when})` : groupingViewSpec.when,
788                 ifFeature: elm.ifFeature ? `(${groupingViewSpec.ifFeature}) and (${elm.ifFeature})` : groupingViewSpec.ifFeature,
789               };
790             });
791           }
792         });
793       }
794     }
795
796     return [viewSpec, subViews];
797   }
798
799   // https://tools.ietf.org/html/rfc7950#section-9.3.4
800   private static decimalRange = [
801     { min: -9223372036854775808, max: 9223372036854775807 },
802     { min: -922337203685477580.8, max: 922337203685477580.7 },
803     { min: -92233720368547758.08, max: 92233720368547758.07 },
804     { min: -9223372036854775.808, max: 9223372036854775.807 },
805     { min: -922337203685477.5808, max: 922337203685477.5807 },
806     { min: -92233720368547.75808, max: 92233720368547.75807 },
807     { min: -9223372036854.775808, max: 9223372036854.775807 },
808     { min: -922337203685.4775808, max: 922337203685.4775807 },
809     { min: -92233720368.54775808, max: 92233720368.54775807 },
810     { min: -9223372036.854775808, max: 9223372036.854775807 },
811     { min: -922337203.6854775808, max: 922337203.6854775807 },
812     { min: -92233720.36854775808, max: 92233720.36854775807 },
813     { min: -9223372.036854775808, max: 9223372.036854775807 },
814     { min: -922337.2036854775808, max: 922337.2036854775807 },
815     { min: -92233.72036854775808, max: 92233.72036854775807 },
816     { min: -9223.372036854775808, max: 9223.372036854775807 },
817     { min: -922.3372036854775808, max: 922.3372036854775807 },
818     { min: -92.23372036854775808, max: 92.23372036854775807 },
819     { min: -9.223372036854775808, max: 9.223372036854775807 },
820   ];
821
822   /** Extracts the UI View from the type in the cur statement. */
823   private getViewElement(cur: Statement, module: Module, parentId: number, currentPath: string, isList: boolean): ViewElement {
824
825     const type = this.extractValue(cur, "type");
826     const defaultVal = this.extractValue(cur, "default") || undefined;
827     const description = this.extractValue(cur, "description") || undefined;
828
829     const configValue = this.extractValue(cur, "config");
830     const config = configValue == null ? true : configValue.toLocaleLowerCase() !== "false";
831
832     const extractRange = (min: number, max: number, property: string = "range"): { expression: Expression<YangRange> | undefined, min: number, max: number } => {
833       const ranges = this.extractValue(this.extractNodes(cur, "type")[0]!, property) || undefined;
834       const range = ranges ?.replace(/min/i, String(min)).replace(/max/i, String(max)).split("|").map(r => {
835         const [minStr, maxStr] = r.split('..');
836         const minValue = Number(minStr);
837         const maxValue = Number(maxStr);
838
839         if (minValue > min) min = minValue;
840         if (maxValue < max) max = maxValue;
841
842         return {
843           min: minValue,
844           max: maxValue
845         };
846       });
847       return {
848         min: min,
849         max: max,
850         expression: range && range.length === 1
851           ? range[0]
852           : range && range.length > 1
853             ? { operation: "OR", arguments: range }
854             : undefined
855       }
856     };
857
858     const extractPattern = (): Expression<RegExp> | undefined => {
859       const pattern = this.extractNodes(this.extractNodes(cur, "type")[0]!, "pattern").map(p => p.arg!).filter(p => !!p).map(p => `^${p}$`);
860       return pattern && pattern.length == 1
861         ? new RegExp(pattern[0])
862         : pattern && pattern.length > 1
863           ? { operation: "AND", arguments: pattern.map(p => new RegExp(p)) }
864           : undefined;
865     }
866
867     const mandatory = this.extractValue(cur, "mandatory") === "true" || false;
868
869     if (!cur.arg) {
870       throw new Error(`Module: [${module.name}]. Found element without name.`);
871     }
872
873     if (!type) {
874       throw new Error(`Module: [${module.name}].[${cur.arg}]. Found element without type.`);
875     }
876
877     const element: ViewElementBase = {
878       id: parentId === 0 ? `${module.name}:${cur.arg}` : cur.arg,
879       label: cur.arg,
880       config: config,
881       mandatory: mandatory,
882       isList: isList,
883       default: defaultVal,
884       description: description
885     };
886
887     if (type === "string") {
888       const length = extractRange(0, +18446744073709551615, "length");
889       return ({
890         ...element,
891         uiType: "string",
892         length: length.expression,
893         pattern: extractPattern(),
894       });
895     } else if (type === "boolean") {
896       return ({
897         ...element,
898         uiType: "boolean"
899       });
900     } else if (type === "uint8") {
901       const range = extractRange(0, +255);
902       return ({
903         ...element,
904         uiType: "number",
905         range: range.expression,
906         min: range.min,
907         max: range.max,
908         units: this.extractValue(cur, "units") || undefined,
909         format: this.extractValue(cur, "format") || undefined,
910       });
911     } else if (type === "uint16") {
912       const range = extractRange(0, +65535);
913       return ({
914         ...element,
915         uiType: "number",
916         range: range.expression,
917         min: range.min,
918         max: range.max,
919         units: this.extractValue(cur, "units") || undefined,
920         format: this.extractValue(cur, "format") || undefined,
921       });
922     } else if (type === "uint32") {
923       const range = extractRange(0, +4294967295);
924       return ({
925         ...element,
926         uiType: "number",
927         range: range.expression,
928         min: range.min,
929         max: range.max,
930         units: this.extractValue(cur, "units") || undefined,
931         format: this.extractValue(cur, "format") || undefined,
932       });
933     } else if (type === "uint64") {
934       const range = extractRange(0, +18446744073709551615);
935       return ({
936         ...element,
937         uiType: "number",
938         range: range.expression,
939         min: range.min,
940         max: range.max,
941         units: this.extractValue(cur, "units") || undefined,
942         format: this.extractValue(cur, "format") || undefined,
943       });
944     } else if (type === "int8") {
945       const range = extractRange(-128, +127);
946       return ({
947         ...element,
948         uiType: "number",
949         range: range.expression,
950         min: range.min,
951         max: range.max,
952         units: this.extractValue(cur, "units") || undefined,
953         format: this.extractValue(cur, "format") || undefined,
954       });
955     } else if (type === "int16") {
956       const range = extractRange(-32768, +32767);
957       return ({
958         ...element,
959         uiType: "number",
960         range: range.expression,
961         min: range.min,
962         max: range.max,
963         units: this.extractValue(cur, "units") || undefined,
964         format: this.extractValue(cur, "format") || undefined,
965       });
966     } else if (type === "int32") {
967       const range = extractRange(-2147483648, +2147483647);
968       return ({
969         ...element,
970         uiType: "number",
971         range: range.expression,
972         min: range.min,
973         max: range.max,
974         units: this.extractValue(cur, "units") || undefined,
975         format: this.extractValue(cur, "format") || undefined,
976       });
977     } else if (type === "int64") {
978       const range = extractRange(-9223372036854775808, +9223372036854775807);
979       return ({
980         ...element,
981         uiType: "number",
982         range: range.expression,
983         min: range.min,
984         max: range.max,
985         units: this.extractValue(cur, "units") || undefined,
986         format: this.extractValue(cur, "format") || undefined,
987       });
988     } else if (type === "decimal64") {
989       // decimalRange
990       const fDigits = Number(this.extractValue(this.extractNodes(cur, "type")[0]!, "fraction-digits")) || -1;
991       if (fDigits === -1) {
992         throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found decimal64 with invalid fraction-digits.`);
993       }
994       const range = extractRange(YangParser.decimalRange[fDigits].min, YangParser.decimalRange[fDigits].max);
995       return ({
996         ...element,
997         uiType: "number",
998         fDigits: fDigits,
999         range: range.expression,
1000         min: range.min,
1001         max: range.max,
1002         units: this.extractValue(cur, "units") || undefined,
1003         format: this.extractValue(cur, "format") || undefined,
1004       });
1005     } else if (type === "enumeration") {
1006       const typeNode = this.extractNodes(cur, "type")[0]!;
1007       const enumNodes = this.extractNodes(typeNode, "enum");
1008       return ({
1009         ...element,
1010         uiType: "selection",
1011         options: enumNodes.reduce<{ key: string; value: string; description?: string }[]>((acc, enumNode) => {
1012           if (!enumNode.arg) {
1013             throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found option without name.`);
1014           }
1015           const ifClause = this.extractValue(enumNode, "if-feature");
1016           const value = this.extractValue(enumNode, "value");
1017           const enumOption = {
1018             key: enumNode.arg,
1019             value: value != null ? value : enumNode.arg,
1020             description: this.extractValue(enumNode, "description") || undefined
1021           };
1022           // todo: ❗ handle the if clause ⚡
1023           acc.push(enumOption);
1024           return acc;
1025         }, [])
1026       });
1027     } else if (type === "leafref") {
1028       const typeNode = this.extractNodes(cur, "type")[0]!;
1029       const vPath = this.extractValue(typeNode, "path");
1030       if (!vPath) {
1031         throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found leafref without path.`);
1032       }
1033       const refPath = this.resolveReferencePath(vPath, module);
1034       const resolve = this.resolveReference.bind(this);
1035       const res: ViewElement = {
1036         ...element,
1037         uiType: "reference",
1038         referencePath: refPath,
1039         ref(this: ViewElement, currentPath: string) {
1040           const resolved = resolve(refPath, currentPath);
1041           return resolved && {
1042             ...resolved,
1043             id: this.id,
1044             label: this.label,
1045             config: this.config,
1046             mandatory: this.mandatory,
1047             isList: this.isList,
1048             default: this.default,
1049             description: this.description,
1050           } as ViewElement;
1051         }
1052       };
1053       return res;
1054     } else if (type === "identityref") {
1055       const typeNode = this.extractNodes(cur, "type")[0]!;
1056       const base = this.extractValue(typeNode, "base");
1057       if (!base) {
1058         throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found identityref without base.`);
1059       }
1060       const res: ViewElement = {
1061         ...element,
1062         uiType: "selection",
1063         options: []
1064       };
1065       this._identityToResolve.push(() => {
1066         const identity: Identity = this.resolveIdentity(base, module);
1067         if (!identity) {
1068           throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Could not resolve identity [${base}].`);
1069         }
1070         if (!identity.values || identity.values.length === 0) {
1071           throw new Error(`Identity: [${base}] has no values.`);
1072         }
1073         res.options = identity.values.map(val => ({
1074           key: val.id,
1075           value: val.id,
1076           description: val.description
1077         }));
1078       });
1079       return res;
1080     } else if (type === "empty") {
1081       // todo: ❗ handle empty ⚡
1082       /*  9.11.  The empty Built-In Type
1083           The empty built-in type represents a leaf that does not have any
1084           value, it conveys information by its presence or absence. */
1085       console.warn(`found type: empty in [${module.name}][${currentPath}][${element.label}]`);
1086       return {
1087         ...element,
1088         uiType: "string",
1089       };
1090     } else if (type === "union") {
1091       // todo: ❗ handle union ⚡
1092       /* 9.12.  The union Built-In Type */
1093       const typeNode = this.extractNodes(cur, "type")[0]!;
1094       const typeNodes = this.extractNodes(typeNode, "type");
1095
1096       const resultingElement = {
1097         ...element,
1098         uiType: "union",
1099         elements: []
1100       } as ViewElementUnion;
1101
1102       const resolveUnion = () => {
1103         resultingElement.elements.push(...typeNodes.map(node => {
1104           const stm: Statement = {
1105             ...cur,
1106             sub: [
1107               ...(cur.sub ?.filter(s => s.key !== "type") || []),
1108               node
1109             ]
1110           };
1111           return {
1112             ...this.getViewElement(stm, module, parentId, currentPath, isList),
1113             id: node.arg!
1114           };
1115         }));
1116       };
1117
1118       this._unionsToResolve.push(resolveUnion);
1119
1120       return resultingElement;
1121     } else if (type === "bits") {
1122       const typeNode = this.extractNodes(cur, "type")[0]!;
1123       const bitNodes = this.extractNodes(typeNode, "bit");
1124       return {
1125         ...element,
1126         uiType: "bits",
1127         flags: bitNodes.reduce<{ [name: string]: number | undefined; }>((acc, bitNode) => {
1128           if (!bitNode.arg) {
1129             throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found bit without name.`);
1130           }
1131           const ifClause = this.extractValue(bitNode, "if-feature");
1132           const pos = Number(this.extractValue(bitNode, "position"));
1133           acc[bitNode.arg] = pos === pos ? pos : undefined;
1134           return acc;
1135         }, {})
1136       };
1137     } else if (type === "binary") {
1138       return {
1139         ...element,
1140         uiType: "binary",
1141         length: extractRange(0, +18446744073709551615, "length"),
1142       };
1143     } else {
1144       // not a build in type, have to resolve type
1145       let typeRef = this.resolveType(type, module);
1146       if (typeRef == null) console.error(new Error(`Could not resolve type ${type} in [${module.name}][${currentPath}].`));
1147
1148       if (isViewElementString(typeRef)) {
1149         typeRef = this.resolveStringType(typeRef, extractPattern(), extractRange(0, +18446744073709551615));
1150
1151       } else if (isViewElementNumber(typeRef)) {
1152         typeRef = this.resolveNumberType(typeRef, extractRange(typeRef.min, typeRef.max));
1153       }
1154
1155       return ({
1156         ...typeRef,
1157         ...element,
1158         description: description,
1159       }) as ViewElement;
1160     }
1161   }
1162
1163   private resolveStringType(parentElement: ViewElementString, pattern: Expression<RegExp> | undefined, length: { expression: Expression<YangRange> | undefined, min: number, max: number }) {
1164     return {
1165       ...parentElement,
1166       pattern: pattern != null && parentElement.pattern
1167         ? { operation: "AND", arguments: [pattern, parentElement.pattern] }
1168         : parentElement.pattern
1169           ? parentElement.pattern
1170           : pattern,
1171       length: length.expression != null && parentElement.length
1172         ? { operation: "AND", arguments: [length.expression, parentElement.length] }
1173         : parentElement.length
1174           ? parentElement.length
1175           : length ?.expression,
1176     } as ViewElementString;
1177   }
1178
1179   private resolveNumberType(parentElement: ViewElementNumber, range: { expression: Expression<YangRange> | undefined, min: number, max: number }) {
1180     return {
1181       ...parentElement,
1182       range: range.expression != null && parentElement.range
1183         ? { operation: "AND", arguments: [range.expression, parentElement.range] }
1184         : parentElement.range
1185           ? parentElement.range
1186           : range,
1187       min: range.min,
1188       max: range.max,
1189     } as ViewElementNumber;
1190   }
1191
1192   private resolveReferencePath(vPath: string, module: Module) {
1193     const vPathParser = /(?:(?:([^\/\:]+):)?([^\/]+))/g // 1 = opt: namespace / 2 = property
1194     return vPath.replace(vPathParser, (_, ns, property) => {
1195       const nameSpace = ns && module.imports[ns] || module.name;
1196       return `${nameSpace}:${property}`;
1197     });
1198   }
1199
1200   private resolveReference(vPath: string, currentPath: string) {
1201     const vPathParser = /(?:(?:([^\/\[\]\:]+):)?([^\/\[\]]+)(\[[^\]]+\])?)/g // 1 = opt: namespace / 2 = property / 3 = opt: indexPath
1202     let element: ViewElement | null = null;
1203     let moduleName = "";
1204
1205     const vPathParts = splitVPath(vPath, vPathParser).map(p => ({ ns: p[1], property: p[2], ind: p[3] }));
1206     const resultPathParts = !vPath.startsWith("/")
1207       ? splitVPath(currentPath, vPathParser).map(p => ({ ns: p[1], property: p[2], ind: p[3] }))
1208       : [];
1209
1210     for (let i = 0; i < vPathParts.length; ++i) {
1211       const vPathPart = vPathParts[i];
1212       if (vPathPart.property === "..") {
1213         resultPathParts.pop();
1214       } else if (vPathPart.property !== ".") {
1215         resultPathParts.push(vPathPart);
1216       }
1217     }
1218
1219     // resolve element by path
1220     for (let j = 0; j < resultPathParts.length; ++j) {
1221       const pathPart = resultPathParts[j];
1222       if (j === 0) {
1223         moduleName = pathPart.ns;
1224         const rootModule = this._modules[moduleName];
1225         if (!rootModule) throw new Error("Could not resolve module [" + moduleName + "].\r\n" + vPath);
1226         element = rootModule.elements[`${pathPart.ns}:${pathPart.property}`];
1227       } else if (element && isViewElementObjectOrList(element)) {
1228         const view: ViewSpecification = this._views[+element.viewId];
1229         if (moduleName !== pathPart.ns) {
1230           moduleName = pathPart.ns;
1231           element = view.elements[`${moduleName}:${pathPart.property}`];
1232         } else {
1233           element = view.elements[pathPart.property] || view.elements[`${moduleName}:${pathPart.property}`];
1234         }
1235       } else {
1236         throw new Error("Could not resolve reference.\r\n" + vPath);
1237       }
1238       if (!element) throw new Error("Could not resolve path [" + pathPart.property + "] in [" + currentPath + "] \r\n" + vPath);
1239     }
1240
1241     return element;
1242   }
1243
1244   private resolveView(vPath: string) {
1245     const vPathParser = /(?:(?:([^\/\[\]\:]+):)?([^\/\[\]]+)(\[[^\]]+\])?)/g // 1 = opt: namespace / 2 = property / 3 = opt: indexPath
1246     let element: ViewElement | null = null;
1247     let partMatch: RegExpExecArray | null;
1248     let view: ViewSpecification | null = null;
1249     let moduleName = "";
1250     if (vPath) do {
1251       partMatch = vPathParser.exec(vPath);
1252       if (partMatch) {
1253         if (element === null) {
1254           moduleName = partMatch[1]!;
1255           const rootModule = this._modules[moduleName];
1256           if (!rootModule) return null;
1257           element = rootModule.elements[`${moduleName}:${partMatch[2]!}`];
1258         } else if (isViewElementObjectOrList(element)) {
1259           view = this._views[+element.viewId];
1260           if (moduleName !== partMatch[1]) {
1261             moduleName = partMatch[1];
1262             element = view.elements[`${moduleName}:${partMatch[2]}`];
1263           } else {
1264             element = view.elements[partMatch[2]];
1265           }
1266         } else {
1267           return null;
1268         }
1269         if (!element) return null;
1270       }
1271     } while (partMatch)
1272     return element && isViewElementObjectOrList(element) && this._views[+element.viewId] || null;
1273   }
1274
1275   private resolveType(type: string, module: Module) {
1276     const collonInd = type.indexOf(":");
1277     const preFix = collonInd > -1 ? type.slice(0, collonInd) : "";
1278     const typeName = collonInd > -1 ? type.slice(collonInd + 1) : type;
1279
1280     const res = preFix
1281       ? this._modules[module.imports[preFix]].typedefs[typeName]
1282       : module.typedefs[typeName];
1283     return res;
1284   }
1285
1286   private resolveGrouping(grouping: string, module: Module) {
1287     const collonInd = grouping.indexOf(":");
1288     const preFix = collonInd > -1 ? grouping.slice(0, collonInd) : "";
1289     const groupingName = collonInd > -1 ? grouping.slice(collonInd + 1) : grouping;
1290
1291     return preFix
1292       ? this._modules[module.imports[preFix]].groupings[groupingName]
1293       : module.groupings[groupingName];
1294
1295   }
1296
1297   private resolveIdentity(identity: string, module: Module) {
1298     const collonInd = identity.indexOf(":");
1299     const preFix = collonInd > -1 ? identity.slice(0, collonInd) : "";
1300     const identityName = collonInd > -1 ? identity.slice(collonInd + 1) : identity;
1301
1302     return preFix
1303       ? this._modules[module.imports[preFix]].identities[identityName]
1304       : module.identities[identityName];
1305
1306   }
1307 }