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