Merge "fix oauth code"
[ccsdk/features.git] / sdnr / wt-odlux / odlux / apps / configurationApp / src / yang / yangParser.ts
1 /* eslint-disable @typescript-eslint/no-loss-of-precision */
2 /* eslint-disable @typescript-eslint/no-unused-expressions */
3 /* eslint-disable @typescript-eslint/naming-convention */
4 /**
5  * ============LICENSE_START========================================================================
6  * ONAP : ccsdk feature sdnr wt odlux
7  * =================================================================================================
8  * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved.
9  * =================================================================================================
10  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
11  * in compliance with the License. You may obtain a copy of the License at
12  *
13  * http://www.apache.org/licenses/LICENSE-2.0
14  *
15  * Unless required by applicable law or agreed to in writing, software distributed under the License
16  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
17  * or implied. See the License for the specific language governing permissions and limitations under
18  * the License.
19  * ============LICENSE_END==========================================================================
20  */
21 import { Token, Statement, Module, Identity, ModuleState } from '../models/yang';
22 import {
23   Expression,
24   ViewElement,
25   ViewElementBase,
26   ViewSpecification,
27   ViewElementNumber,
28   ViewElementString,
29   ViewElementChoice,
30   ViewElementUnion,
31   ViewElementRpc,
32   isViewElementObjectOrList,
33   isViewElementNumber,
34   isViewElementString,
35   isViewElementRpc,
36   ResolveFunction,
37   YangRange,
38 } from '../models/uiModels';
39 import { yangService } from '../services/yangService';
40
41 const LOGLEVEL = +(localStorage.getItem('log.odlux.app.configuration.yang.yangParser') || 0);
42
43 import { LogLevel } from '../../../../framework/src/utilities/logLevel';
44 import { parseWhen, WhenAST, WhenTokenType } from './whenParser';
45
46 export const splitVPath = (vPath: string, vPathParser: RegExp): RegExpMatchArray[] => {
47   const pathParts: RegExpMatchArray[] = [];
48   let partMatch: RegExpExecArray | null;
49   if (vPath) do {
50     partMatch = vPathParser.exec(vPath);
51     if (partMatch) {
52       pathParts.push(partMatch);
53     }
54   } while (partMatch);
55   return pathParts;
56 };
57
58 class YangLexer {
59
60   private pos: number = 0;
61
62   private buf: string = '';
63
64   constructor(input: string) {
65     this.pos = 0;
66     this.buf = input;
67   }
68
69   private _opTable: { [key: string]: string } = {
70     ';': 'SEMI',
71     '{': 'L_BRACE',
72     '}': 'R_BRACE',
73   };
74
75   private _isNewline(char: string): boolean {
76     return char === '\r' || char === '\n';
77   }
78
79   private _isWhitespace(char: string): boolean {
80     return char === ' ' || char === '\t' || this._isNewline(char);
81   }
82
83   private _isDigit(char: string): boolean {
84     return char >= '0' && char <= '9';
85   }
86
87   private _isAlpha(char: string): boolean {
88     return (char >= 'a' && char <= 'z') ||
89       (char >= 'A' && char <= 'Z');
90   }
91
92   private _isAlphanum(char: string): boolean {
93     return this._isAlpha(char) || this._isDigit(char) ||
94       char === '_' || char === '-' || char === '.';
95   }
96
97   private _skipNonTokens() {
98     while (this.pos < this.buf.length) {
99       const char = this.buf.charAt(this.pos);
100       if (this._isWhitespace(char)) {
101         this.pos++;
102       } else {
103         break;
104       }
105     }
106   }
107
108   private _processString(terminator: string | null): Token {
109     // this.pos points at the opening quote. Find the ending quote.
110     let end_index = this.pos + 1;
111     while (end_index < this.buf.length) {
112       const char = this.buf.charAt(end_index);
113       if (char === '\\') {
114         end_index += 2;
115         continue;
116       }
117       if (terminator === null && (this._isWhitespace(char) || this._opTable[char] !== undefined) || char === terminator) {
118         break;
119       }
120       end_index++;
121     }
122
123     if (end_index >= this.buf.length) {
124       throw Error('Unterminated quote at ' + this.pos);
125     } else {
126       const start = this.pos + (terminator ? 1 : 0);
127       const end = end_index;
128       const tok = {
129         name: 'STRING',
130         value: this.buf.substring(start, end),
131         start,
132         end,
133       };
134       this.pos = terminator ? end + 1 : end;
135       return tok;
136     }
137   }
138
139   private _processIdentifier(): Token {
140     let endpos = this.pos + 1;
141     while (endpos < this.buf.length && this._isAlphanum(this.buf.charAt(endpos))) {
142       ++endpos;
143     }
144
145     let name = 'IDENTIFIER';
146     if (this.buf.charAt(endpos) === ':') {
147       name = 'IDENTIFIERREF';
148       ++endpos;
149       while (endpos < this.buf.length && this._isAlphanum(this.buf.charAt(endpos))) {
150         ++endpos;
151       }
152     }
153
154     const tok = {
155       name: name,
156       value: this.buf.substring(this.pos, endpos),
157       start: this.pos,
158       end: endpos,
159     };
160
161     this.pos = endpos;
162     return tok;
163   }
164
165   private _processNumber(): Token {
166     let endpos = this.pos + 1;
167     while (endpos < this.buf.length &&
168       this._isDigit(this.buf.charAt(endpos))) {
169       endpos++;
170     }
171
172     const tok = {
173       name: 'NUMBER',
174       value: this.buf.substring(this.pos, endpos),
175       start: this.pos,
176       end: endpos,
177     };
178     this.pos = endpos;
179     return tok;
180   }
181
182   private _processLineComment() {
183     var endpos = this.pos + 2;
184     // Skip until the end of the line
185     while (endpos < this.buf.length && !this._isNewline(this.buf.charAt(endpos))) {
186       endpos++;
187     }
188     this.pos = endpos + 1;
189   }
190
191   private _processBlockComment() {
192     var endpos = this.pos + 2;
193     // Skip until the end of the line
194     while (endpos < this.buf.length && !((this.buf.charAt(endpos) === '/' && this.buf.charAt(endpos - 1) === '*'))) {
195       endpos++;
196     }
197     this.pos = endpos + 1;
198   }
199
200   public tokenize(): Token[] {
201     const result: Token[] = [];
202     this._skipNonTokens();
203     while (this.pos < this.buf.length) {
204
205       const char = this.buf.charAt(this.pos);
206       const op = this._opTable[char];
207
208       if (op !== undefined) {
209         result.push({ name: op, value: char, start: this.pos, end: ++this.pos });
210       } else if (this._isAlpha(char)) {
211         result.push(this._processIdentifier());
212         this._skipNonTokens();
213         const peekChar = this.buf.charAt(this.pos);
214         if (this._opTable[peekChar] === undefined) {
215           result.push((peekChar !== '\'' && peekChar !== '"')
216             ? this._processString(null)
217             : this._processString(peekChar));
218         }
219       } else if (char === '/' && this.buf.charAt(this.pos + 1) === '/') {
220         this._processLineComment();
221       } else if (char === '/' && this.buf.charAt(this.pos + 1) === '*') {
222         this._processBlockComment();
223       } else {
224         throw Error('Token error at ' + this.pos + ' ' + this.buf[this.pos]);
225       }
226       this._skipNonTokens();
227     }
228     return result;
229   }
230
231   public tokenize2(): Statement {
232     let stack: Statement[] = [{ key: 'ROOT', sub: [] }];
233     let current: Statement | null = null;
234
235     this._skipNonTokens();
236     while (this.pos < this.buf.length) {
237
238       const char = this.buf.charAt(this.pos);
239       const op = this._opTable[char];
240
241       if (op !== undefined) {
242         if (op === 'L_BRACE') {
243           current && stack.unshift(current);
244           current = null;
245         } else if (op === 'R_BRACE') {
246           current = stack.shift() || null;
247         }
248         this.pos++;
249       } else if (this._isAlpha(char) || char === '_') {
250         const key = this._processIdentifier().value;
251         this._skipNonTokens();
252         let peekChar = this.buf.charAt(this.pos);
253         let arg = undefined;
254         if (this._opTable[peekChar] === undefined) {
255           arg = (peekChar === '"' || peekChar === '\'')
256             ? this._processString(peekChar).value
257             : this._processString(null).value;
258         }
259         do {
260           this._skipNonTokens();
261           peekChar = this.buf.charAt(this.pos);
262           if (peekChar !== '+') break;
263           this.pos++;
264           this._skipNonTokens();
265           peekChar = this.buf.charAt(this.pos);
266           arg += (peekChar === '"' || peekChar === '\'')
267             ? this._processString(peekChar).value
268             : this._processString(null).value;
269         } while (true);
270         current = { key, arg, sub: [] };
271         stack[0].sub!.push(current);
272       } else if (char === '/' && this.buf.charAt(this.pos + 1) === '/') {
273         this._processLineComment();
274       } else if (char === '/' && this.buf.charAt(this.pos + 1) === '*') {
275         this._processBlockComment();
276       } else {
277         throw Error('Token error at ' + this.pos + ' ' + this.buf.slice(this.pos - 10, this.pos + 10));
278       }
279       this._skipNonTokens();
280     }
281     if (stack[0].key !== 'ROOT' || !stack[0].sub![0]) {
282       throw new Error('Internal Perser Error');
283     }
284     return stack[0].sub![0];
285   }
286 }
287
288 export class YangParser {
289   private _groupingsToResolve: ViewSpecification[] = [];
290
291   private _typeRefToResolve: (() => void)[] = [];
292
293   private _identityToResolve: (() => void)[] = [];
294
295   private _unionsToResolve: (() => void)[] = [];
296
297   private _modulesToResolve: (() => void)[] = [];
298
299   private _modules: { [name: string]: Module } = {};
300
301   private _views: ViewSpecification[] = [{
302     id: '0',
303     name: 'root',
304     language: 'en-US',
305     canEdit: false,
306     config: true,
307     parentView: '0',
308     title: 'root',
309     elements: {},
310   }];
311
312   public static ResolveStack = Symbol('ResolveStack');
313
314   constructor(
315     private nodeId: string,
316     private _capabilityRevisionMap: { [capability: string]: string } = {},
317     private _unavailableCapabilities: { failureReason: string; capability: string }[] = [],
318     private _importOnlyModules: { name: string; revision: string }[] = [],
319   ) {
320
321   }
322
323   public get modules() {
324     return this._modules;
325   }
326
327   public get views() {
328     return this._views;
329   }
330
331   public async addCapability(capability: string, version?: string, parentImportOnlyModule?: boolean) {
332     // do not add twice
333     const existingCapability = this._modules[capability];
334     const latestVersionExisting = existingCapability && Object.keys(existingCapability.revisions).sort().reverse()[0];
335     if ((latestVersionExisting && version) && (version <= latestVersionExisting)) {
336       if (LOGLEVEL == LogLevel.Warning) {
337         console.warn(`Skipped capability: ${capability}:${version || ''} since already contained.`);
338       }
339       return;
340     }
341
342     // // do not add unavailable capabilities
343     // if (this._unavailableCapabilities.some(c => c.capability === capability)) {
344     //   // console.warn(`Skipped capability: ${capability} since it is marked as unavailable.` );
345     //   return;
346     // }
347
348     const data = await yangService.getCapability(capability, this.nodeId, version);
349     if (!data) {
350       throw new Error(`Could not load yang file for ${capability}:${version || ''}.`);
351     }
352
353     const rootStatement = new YangLexer(data).tokenize2();
354
355     if (rootStatement.key !== 'module') {
356       throw new Error(`Root element of ${capability} is not a module.`);
357     }
358     if (rootStatement.arg !== capability) {
359       throw new Error(`Root element capability ${rootStatement.arg} does not requested ${capability}.`);
360     }
361
362     const isUnavailable = this._unavailableCapabilities.some(c => c.capability === capability);
363     const isImportOnly = parentImportOnlyModule === true || this._importOnlyModules.some(c => c.name === capability);
364
365     // extract revisions
366     const revisions = this.extractNodes(rootStatement, 'revision').reduce<{ [version: string]: {} }>((acc, revision) => {
367       if (!revision.arg) {
368         throw new Error(`Module [${rootStatement.arg}] has a version w/o version number.`);
369       }
370       const description = this.extractValue(revision, 'description');
371       const reference = this.extractValue(revision, 'reference');
372       acc[revision.arg] = {
373         description,
374         reference,
375       };
376       return acc;
377     }, {});
378
379     const latestVersionLoaded = Object.keys(revisions).sort().reverse()[0];
380     if (existingCapability && latestVersionExisting >= latestVersionLoaded) {
381       if (LOGLEVEL == LogLevel.Warning) {
382         console.warn(`Skipped capability: ${capability}:${latestVersionLoaded} since ${capability}:${latestVersionExisting} already contained.`);
383       }
384       return;
385     }
386     
387     const module = this._modules[capability] = {
388       name: rootStatement.arg,
389       revisions,
390       imports: {},
391       features: {},
392       identities: {},
393       augments: {},
394       groupings: {},
395       typedefs: {},
396       views: {},
397       elements: {},
398       state: isUnavailable
399         ? ModuleState.unavailable
400         : isImportOnly
401           ? ModuleState.importOnly
402           : ModuleState.stable,
403     };
404
405     await this.handleModule(module, rootStatement, capability);
406   }
407
408   private async handleModule(module: Module, rootStatement: Statement, capability: string) {
409
410     // extract namespace && prefix
411     module.namespace = this.extractValue(rootStatement, 'namespace');
412     module.prefix = this.extractValue(rootStatement, 'prefix');
413     if (module.prefix) {
414       module.imports[module.prefix] = capability;
415     }
416
417     // extract features
418     const features = this.extractNodes(rootStatement, 'feature');
419     module.features = {
420       ...module.features,
421       ...features.reduce<{ [version: string]: {} }>((acc, feature) => {
422         if (!feature.arg) {
423           throw new Error(`Module [${module.name}] has a feature w/o name.`);
424         }
425         const description = this.extractValue(feature, 'description');
426         acc[feature.arg] = {
427           description,
428         };
429         return acc;
430       }, {}),
431     };
432
433     // extract imports
434     const imports = this.extractNodes(rootStatement, 'import');
435     module.imports = {
436       ...module.imports,
437       ...imports.reduce<{ [key: string]: string }>((acc, imp) => {
438         const prefix = imp.sub && imp.sub.filter(s => s.key === 'prefix');
439         if (!imp.arg) {
440           throw new Error(`Module [${module.name}] has an import with neither name nor prefix.`);
441         }
442         acc[prefix && prefix.length === 1 && prefix[0].arg || imp.arg] = imp.arg;
443         return acc;
444       }, {}),
445     };
446
447     // import all required files and set module state 
448     if (imports) for (let ind = 0; ind < imports.length; ++ind) {
449       const moduleName = imports[ind].arg!;
450
451       const revision = this._capabilityRevisionMap[moduleName] || undefined;
452       await this.addCapability(moduleName, revision, module.state === ModuleState.importOnly);
453       const importedModule = this._modules[imports[ind].arg!];
454       if (importedModule && importedModule.state > ModuleState.stable) {
455         module.state = Math.max(module.state, ModuleState.instable);
456       }
457     }
458
459     this.extractTypeDefinitions(rootStatement, module, '');
460
461     this.extractIdentities(rootStatement, 0, module, '');
462
463     const groupings = this.extractGroupings(rootStatement, 0, module, '');
464     this._views.push(...groupings);
465
466     const augments = this.extractAugments(rootStatement, 0, module, '');
467     this._views.push(...augments);
468
469     // the default for config on module level is config = true;
470     const [currentView, subViews] = this.extractSubViews(rootStatement, 0, module, '');
471     this._views.push(currentView, ...subViews);
472
473     // create the root elements for this module
474     module.elements = currentView.elements;
475     this._modulesToResolve.push(() => {
476       Object.keys(module.elements).forEach(key => {
477         const viewElement = module.elements[key];
478         if (!(isViewElementObjectOrList(viewElement) || isViewElementRpc(viewElement))) {
479           console.error(new Error(`Module: [${module}]. Only Object, List or RPC are allowed on root level.`));
480         }
481         if (isViewElementObjectOrList(viewElement)) {
482           const viewIdIndex = Number(viewElement.viewId);
483           module.views[key] = this._views[viewIdIndex];
484         }
485
486         // add only the UI View if the module is available
487         if (module.state === ModuleState.stable || module.state === ModuleState.instable) this._views[0].elements[key] = module.elements[key];
488       });
489     });
490     return module;
491   }
492
493   public postProcess() {
494     // process all type refs
495     this._typeRefToResolve.forEach(cb => {
496       try { cb(); } catch (error) {
497         console.warn(error.message);
498       }
499     });
500     /**
501      * This is to fix the issue for sequential execution of modules based on their child and parent relationship
502      * We are sorting the module object based on their augment status
503      */
504     Object.keys(this.modules)
505       .map(elem => {
506         if (this.modules[elem].augments && Object.keys(this.modules[elem].augments).length > 0) {
507           const { augments, ..._rest } = this.modules[elem];
508           const partsOfKeys = Object.keys(augments).map((key) => (key.split('/').length - 1));
509           this.modules[elem].executionOrder = Math.max(...partsOfKeys);
510         } else {
511           this.modules[elem].executionOrder = 0;
512         }
513       });
514
515     // process all augmentations / sort by namespace changes to ensure proper order 
516     Object.keys(this.modules).sort((a, b) => this.modules[a].executionOrder! - this.modules[b].executionOrder!).forEach(modKey => {
517       const module = this.modules[modKey];
518       const augmentKeysWithCounter = Object.keys(module.augments).map((key) => {
519         const pathParts = splitVPath(key, /(?:(?:([^\/\:]+):)?([^\/]+))/g);  // 1 = opt: namespace / 2 = property 
520         let nameSpaceChangeCounter = 0;
521         let currentNS = module.name; // init namespace
522         pathParts.forEach(([ns, _]) => {
523           if (ns === currentNS) {
524             currentNS = ns;
525             nameSpaceChangeCounter++;
526           }
527         });
528         return {
529           key,
530           nameSpaceChangeCounter,
531         };
532       });
533
534       const augmentKeys = augmentKeysWithCounter
535         .sort((a, b) => a.nameSpaceChangeCounter > b.nameSpaceChangeCounter ? 1 : a.nameSpaceChangeCounter === b.nameSpaceChangeCounter ? 0 : -1)
536         .map((a) => a.key);
537
538       augmentKeys.forEach(augKey => {
539         const augments = module.augments[augKey];
540         const viewSpec = this.resolveView(augKey);
541         if (!viewSpec) console.warn(`Could not find view to augment [${augKey}] in [${module.name}].`);
542         if (augments && viewSpec) {
543           augments.forEach(augment => Object.keys(augment.elements).forEach(key => {
544             const elm = augment.elements[key];
545             
546             const when = elm.when && augment.when
547               ? {
548                 type: WhenTokenType.AND,
549                 left: elm.when,
550                 right: augment.when,
551               }
552               : elm.when || augment.when;
553             
554             const ifFeature = elm.ifFeature
555               ? `(${augment.ifFeature}) and (${elm.ifFeature})`
556               : augment.ifFeature;
557             
558             viewSpec.elements[key] = {
559               ...augment.elements[key],
560               when,
561               ifFeature,
562             };
563           }));
564         }
565       });
566     });
567
568     // process Identities
569     const traverseIdentity = (identities: Identity[]) => {
570       const result: Identity[] = [];
571       for (let identity of identities) {
572         if (identity.children && identity.children.length > 0) {
573           result.push(...traverseIdentity(identity.children));
574         } else {
575           result.push(identity);
576         }
577       }
578       return result;
579     };
580
581     const baseIdentities: Identity[] = [];
582     Object.keys(this.modules).forEach(modKey => {
583       const module = this.modules[modKey];
584       Object.keys(module.identities).forEach(idKey => {
585         const identity = module.identities[idKey];
586         if (identity.base != null) {
587           const base = this.resolveIdentity(identity.base, module);
588           base?.children?.push(identity);
589         } else {
590           baseIdentities.push(identity);
591         }
592       });
593     });
594     baseIdentities.forEach(identity => {
595       identity.values = identity.children && traverseIdentity(identity.children) || [];
596     });
597
598     this._identityToResolve.forEach(cb => {
599       try { cb(); } catch (error) {
600         console.warn(error.message);
601       }
602     });
603
604     this._modulesToResolve.forEach(cb => {
605       try { cb(); } catch (error) {
606         console.warn(error.message);
607       }
608     });
609
610     // execute all post processes like resolving in proper order
611     this._unionsToResolve.forEach(cb => {
612       try { cb(); } catch (error) {
613         console.warn(error.message);
614       }
615     });
616
617     // process all groupings
618     this._groupingsToResolve.filter(vs => vs.uses && vs.uses[ResolveFunction]).forEach(vs => {
619       try { vs.uses![ResolveFunction] !== undefined && vs.uses![ResolveFunction]!('|'); } catch (error) {
620         console.warn(`Error resolving: [${vs.name}] [${error.message}]`);
621       }
622     });
623
624     const knownViews: ViewSpecification[] = [];
625     // resolve readOnly
626     const resolveReadOnly = (view: ViewSpecification, parentConfig: boolean) => {
627       if (knownViews.includes(view)) return;
628       knownViews.push(view);
629
630       // update view config
631       view.config = view.config && parentConfig;
632
633       Object.keys(view.elements).forEach((key) => {
634         const elm = view.elements[key];
635
636         // update element config
637         elm.config = elm.config && view.config;
638
639         // update all sub-elements of objects
640         if (elm.uiType === 'object') {
641           resolveReadOnly(this.views[+elm.viewId], elm.config);
642         }
643
644       });
645     };
646
647     const dump = resolveReadOnly(this.views[0], true);
648     if (LOGLEVEL > 2) {
649       console.log('Resolved views:', dump);
650     }
651   }
652
653   private _nextId = 1;
654
655   private get nextId() {
656     return this._nextId++;
657   }
658
659   private extractNodes(statement: Statement, key: string): Statement[] {
660     return statement.sub && statement.sub.filter(s => s.key === key) || [];
661   }
662
663   private extractValue(statement: Statement, key: string): string | undefined;
664   private extractValue(statement: Statement, key: string, parser: RegExp): RegExpExecArray | undefined;
665   private extractValue(statement: Statement, key: string, parser?: RegExp): string | RegExpExecArray | undefined {
666     const typeNodes = this.extractNodes(statement, key);
667     const rawValue = typeNodes.length > 0 && typeNodes[0].arg || undefined;
668     return parser
669       ? rawValue && parser.exec(rawValue) || undefined
670       : rawValue;
671   }
672
673   private extractTypeDefinitions(statement: Statement, module: Module, currentPath: string): void {
674     const typedefs = this.extractNodes(statement, 'typedef');
675     typedefs && typedefs.forEach(def => {
676       if (!def.arg) {
677         throw new Error(`Module: [${module.name}]. Found typedef without name.`);
678       }
679       module.typedefs[def.arg] = this.getViewElement(def, module, 0, currentPath, false);
680     });
681   }
682
683   /** Handles groupings like named Container */
684   private extractGroupings(statement: Statement, parentId: number, module: Module, currentPath: string): ViewSpecification[] {
685     const subViews: ViewSpecification[] = [];
686     const groupings = this.extractNodes(statement, 'grouping');
687     if (groupings && groupings.length > 0) {
688       subViews.push(...groupings.reduce<ViewSpecification[]>((acc, cur) => {
689         if (!cur.arg) {
690           throw new Error(`Module: [${module.name}][${currentPath}]. Found grouping without name.`);
691         }
692         const grouping = cur.arg;
693
694         // the default for config on module level is config = true;
695         const [currentView, currentSubViews] = this.extractSubViews(cur, /* parentId */ -1, module, currentPath);
696         grouping && (module.groupings[grouping] = currentView);
697         acc.push(currentView, ...currentSubViews);
698         return acc;
699       }, []));
700     }
701
702     return subViews;
703   }
704
705   /** Handles augments also like named container */
706   private extractAugments(statement: Statement, parentId: number, module: Module, currentPath: string): ViewSpecification[] {
707     const subViews: ViewSpecification[] = [];
708     const augments = this.extractNodes(statement, 'augment');
709     if (augments && augments.length > 0) {
710       subViews.push(...augments.reduce<ViewSpecification[]>((acc, cur) => {
711         if (!cur.arg) {
712           throw new Error(`Module: [${module.name}][${currentPath}]. Found augment without path.`);
713         }
714         const augment = this.resolveReferencePath(cur.arg, module);
715
716         // the default for config on module level is config = true;
717         const [currentView, currentSubViews] = this.extractSubViews(cur, parentId, module, currentPath);
718         if (augment) {
719           module.augments[augment] = module.augments[augment] || [];
720           module.augments[augment].push(currentView);
721         }
722         acc.push(currentView, ...currentSubViews);
723         return acc;
724       }, []));
725     }
726
727     return subViews;
728   }
729
730   /** Handles identities  */
731   private extractIdentities(statement: Statement, parentId: number, module: Module, currentPath: string) {
732     const identities = this.extractNodes(statement, 'identity');
733     module.identities = identities.reduce<{ [name: string]: Identity }>((acc, cur) => {
734       if (!cur.arg) {
735         throw new Error(`Module: [${module.name}][${currentPath}]. Found identity without name.`);
736       }
737       acc[cur.arg] = {
738         id: `${module.name}:${cur.arg}`,
739         label: cur.arg,
740         base: this.extractValue(cur, 'base'),
741         description: this.extractValue(cur, 'description'),
742         reference: this.extractValue(cur, 'reference'),
743         children: [],
744       };
745       return acc;
746     }, {});
747   }
748
749   // Hint: use 0 as parentId for rootElements and -1 for rootGroupings.
750   private extractSubViews(statement: Statement, parentId: number, module: Module, currentPath: string): [ViewSpecification, ViewSpecification[]] {
751     // used for scoped definitions
752     const context: Module = {
753       ...module,
754       typedefs: {
755         ...module.typedefs,
756       },
757     };
758
759     const currentId = this.nextId;
760     const subViews: ViewSpecification[] = [];
761     let elements: ViewElement[] = [];
762
763     const configValue = this.extractValue(statement, 'config');
764     const config = configValue == null ? true : configValue.toLocaleLowerCase() !== 'false';
765
766     // extract conditions
767     const ifFeature = this.extractValue(statement, 'if-feature');
768     const whenCondition = this.extractValue(statement, 'when');
769     if (whenCondition) console.warn('Found in [' + context.name + ']' + currentPath + ' when: ' + whenCondition);
770
771     // extract all scoped typedefs
772     this.extractTypeDefinitions(statement, context, currentPath);
773
774     // extract all scoped groupings
775     subViews.push(
776       ...this.extractGroupings(statement, parentId, context, currentPath),
777     );
778
779     // extract all container
780     const container = this.extractNodes(statement, 'container');
781     if (container && container.length > 0) {
782       subViews.push(...container.reduce<ViewSpecification[]>((acc, cur) => {
783         if (!cur.arg) {
784           throw new Error(`Module: [${context.name}]${currentPath}. Found container without name.`);
785         }
786         const [currentView, currentSubViews] = this.extractSubViews(cur, currentId, context, `${currentPath}/${context.name}:${cur.arg}`);
787         elements.push({
788           id: parentId === 0 ? `${context.name}:${cur.arg}` : cur.arg,
789           label: cur.arg,
790           path: currentPath,
791           module: context.name || module.name || '',
792           uiType: 'object',
793           viewId: currentView.id,
794           config: currentView.config,
795         });
796         acc.push(currentView, ...currentSubViews);
797         return acc;
798       }, []));
799     }
800
801     // process all lists
802     // a list is a list of containers with the leafs contained in the list
803     const lists = this.extractNodes(statement, 'list');
804     if (lists && lists.length > 0) {
805       subViews.push(...lists.reduce<ViewSpecification[]>((acc, cur) => {
806         let elmConfig = config;
807         if (!cur.arg) {
808           throw new Error(`Module: [${context.name}]${currentPath}. Found list without name.`);
809         }
810         const key = this.extractValue(cur, 'key') || undefined;
811         if (elmConfig && !key) {
812           console.warn(`Module: [${context.name}]${currentPath}. Found configurable list without key. Assume config shell be false.`);
813           elmConfig = false;
814         }
815         const [currentView, currentSubViews] = this.extractSubViews(cur, currentId, context, `${currentPath}/${context.name}:${cur.arg}`);
816         elements.push({
817           id: parentId === 0 ? `${context.name}:${cur.arg}` : cur.arg,
818           label: cur.arg,
819           path: currentPath,
820           module: context.name || module.name || '',
821           isList: true,
822           uiType: 'object',
823           viewId: currentView.id,
824           key: key,
825           config: elmConfig && currentView.config,
826         });
827         acc.push(currentView, ...currentSubViews);
828         return acc;
829       }, []));
830     }
831
832     // process all leaf-lists
833     // a leaf-list is a list of some type
834     const leafLists = this.extractNodes(statement, 'leaf-list');
835     if (leafLists && leafLists.length > 0) {
836       elements.push(...leafLists.reduce<ViewElement[]>((acc, cur) => {
837         const element = this.getViewElement(cur, context, parentId, currentPath, true);
838         element && acc.push(element);
839         return acc;
840       }, []));
841     }
842
843     // process all leafs
844     // a leaf is mainly a property of an object
845     const leafs = this.extractNodes(statement, 'leaf');
846     if (leafs && leafs.length > 0) {
847       elements.push(...leafs.reduce<ViewElement[]>((acc, cur) => {
848         const element = this.getViewElement(cur, context, parentId, currentPath, false);
849         element && acc.push(element);
850         return acc;
851       }, []));
852     }
853
854
855     const choiceStms = this.extractNodes(statement, 'choice');
856     if (choiceStms && choiceStms.length > 0) {
857       elements.push(...choiceStms.reduce<ViewElementChoice[]>((accChoice, curChoice) => {
858         if (!curChoice.arg) {
859           throw new Error(`Module: [${context.name}]${currentPath}. Found choise without name.`);
860         }
861         // extract all cases like containers
862         const cases: { id: string; label: string; description?: string; elements: { [name: string]: ViewElement } }[] = [];
863         const caseStms = this.extractNodes(curChoice, 'case');
864         if (caseStms && caseStms.length > 0) {
865           cases.push(...caseStms.reduce((accCase, curCase) => {
866             if (!curCase.arg) {
867               throw new Error(`Module: [${context.name}]${currentPath}/${curChoice.arg}. Found case without name.`);
868             }
869             const description = this.extractValue(curCase, 'description') || undefined;
870             const [caseView, caseSubViews] = this.extractSubViews(curCase, parentId, context, `${currentPath}/${context.name}:${curChoice.arg}`);
871             subViews.push(caseView, ...caseSubViews);
872
873             const caseDef: { id: string; label: string; description?: string; elements: { [name: string]: ViewElement } } = {
874               id: parentId === 0 ? `${context.name}:${curCase.arg}` : curCase.arg,
875               label: curCase.arg,
876               description: description,
877               elements: caseView.elements,
878             };
879             accCase.push(caseDef);
880             return accCase;
881           }, [] as { id: string; label: string; description?: string; elements: { [name: string]: ViewElement } }[]));
882         }
883
884         // extract all simple cases (one case per leaf, container, etc.)
885         const [choiceView, choiceSubViews] = this.extractSubViews(curChoice, parentId, context, `${currentPath}/${context.name}:${curChoice.arg}`);
886         subViews.push(choiceView, ...choiceSubViews);
887         cases.push(...Object.keys(choiceView.elements).reduce((accElm, curElm) => {
888           const elm = choiceView.elements[curElm];
889           const caseDef: { id: string; label: string; description?: string; elements: { [name: string]: ViewElement } } = {
890             id: elm.id,
891             label: elm.label,
892             description: elm.description,
893             elements: { [elm.id]: elm },
894           };
895           accElm.push(caseDef);
896           return accElm;
897         }, [] as { id: string; label: string; description?: string; elements: { [name: string]: ViewElement } }[]));
898
899         const choiceDescription = this.extractValue(curChoice, 'description') || undefined;
900         const choiceConfigValue = this.extractValue(curChoice, 'config');
901         const choiceConfig = choiceConfigValue == null ? true : choiceConfigValue.toLocaleLowerCase() !== 'false';
902
903         const mandatory = this.extractValue(curChoice, 'mandatory') === 'true' || false;
904
905         const element: ViewElementChoice = {
906           uiType: 'choice',
907           id: parentId === 0 ? `${context.name}:${curChoice.arg}` : curChoice.arg,
908           label: curChoice.arg,
909           path: currentPath,
910           module: context.name || module.name || '',
911           config: choiceConfig,
912           mandatory: mandatory,
913           description: choiceDescription,
914           cases: cases.reduce((acc, cur) => {
915             acc[cur.id] = cur;
916             return acc;
917           }, {} as { [name: string]: { id: string; label: string; description?: string; elements: { [name: string]: ViewElement } } }),
918         };
919
920         accChoice.push(element);
921         return accChoice;
922       }, []));
923     }
924
925     const rpcStms = this.extractNodes(statement, 'rpc');
926     if (rpcStms && rpcStms.length > 0) {
927       elements.push(...rpcStms.reduce<ViewElementRpc[]>((accRpc, curRpc) => {
928         if (!curRpc.arg) {
929           throw new Error(`Module: [${context.name}]${currentPath}. Found rpc without name.`);
930         }
931
932         const rpcDescription = this.extractValue(curRpc, 'description') || undefined;
933         const rpcConfigValue = this.extractValue(curRpc, 'config');
934         const rpcConfig = rpcConfigValue == null ? true : rpcConfigValue.toLocaleLowerCase() !== 'false';
935
936         let inputViewId: string | undefined = undefined;
937         let outputViewId: string | undefined = undefined;
938
939         const input = this.extractNodes(curRpc, 'input') || undefined;
940         const output = this.extractNodes(curRpc, 'output') || undefined;
941
942         if (input && input.length > 0) {
943           const [inputView, inputSubViews] = this.extractSubViews(input[0], parentId, context, `${currentPath}/${context.name}:${curRpc.arg}`);
944           subViews.push(inputView, ...inputSubViews);
945           inputViewId = inputView.id;
946         }
947
948         if (output && output.length > 0) {
949           const [outputView, outputSubViews] = this.extractSubViews(output[0], parentId, context, `${currentPath}/${context.name}:${curRpc.arg}`);
950           subViews.push(outputView, ...outputSubViews);
951           outputViewId = outputView.id;
952         }
953
954         const element: ViewElementRpc = {
955           uiType: 'rpc',
956           id: parentId === 0 ? `${context.name}:${curRpc.arg}` : curRpc.arg,
957           label: curRpc.arg,
958           path: currentPath,
959           module: context.name || module.name || '',
960           config: rpcConfig,
961           description: rpcDescription,
962           inputViewId: inputViewId,
963           outputViewId: outputViewId,
964         };
965
966         accRpc.push(element);
967
968         return accRpc;
969       }, []));
970     }
971
972     if (!statement.arg) {
973       console.error(new Error(`Module: [${context.name}]. Found statement without name.`));
974     }
975
976     let whenParsed: WhenAST | undefined = undefined;
977     try {
978       whenParsed = whenCondition && parseWhen(whenCondition) || undefined;
979     } catch (e) {
980       console.error(new Error(`Module: [${context.name}]. Found invalid when condition: ${whenCondition}`));
981     }
982
983     const viewSpec: ViewSpecification = {
984       id: String(currentId),
985       parentView: String(parentId),
986       ns: context.name,
987       name: statement.arg != null ? statement.arg : undefined,
988       title: statement.arg != null ? statement.arg : undefined,
989       language: 'en-us',
990       canEdit: false,
991       config: config,
992       ifFeature: ifFeature,
993       when: whenParsed,
994       elements: elements.reduce<{ [name: string]: ViewElement }>((acc, cur) => {
995         acc[cur.id] = cur;
996         return acc;
997       }, {}),
998     };
999
1000     // evaluate canEdit depending on all conditions
1001     Object.defineProperty(viewSpec, 'canEdit', {
1002       get: () => {
1003         return Object.keys(viewSpec.elements).some(key => {
1004           const elm = viewSpec.elements[key];
1005           return (!isViewElementObjectOrList(elm) && elm.config);
1006         });
1007       },
1008     });
1009
1010     // merge in all uses references and resolve groupings
1011     const usesRefs = this.extractNodes(statement, 'uses');
1012     if (usesRefs && usesRefs.length > 0) {
1013
1014       viewSpec.uses = (viewSpec.uses || []);
1015       const resolveFunctions: ((parentElementPath: string) => void)[] = [];
1016
1017       for (let i = 0; i < usesRefs.length; ++i) {
1018         const groupingName = usesRefs[i].arg;
1019         if (!groupingName) {
1020           throw new Error(`Module: [${context.name}]. Found an uses statement without a grouping name.`);
1021         }
1022
1023         viewSpec.uses.push(this.resolveReferencePath(groupingName, context));
1024
1025         resolveFunctions.push((parentElementPath: string) => {
1026           const groupingViewSpec = this.resolveGrouping(groupingName, context);
1027           if (groupingViewSpec) {
1028
1029             // resolve recursive
1030             const resolveFunc = groupingViewSpec.uses && groupingViewSpec.uses[ResolveFunction];
1031             resolveFunc && resolveFunc(parentElementPath);
1032
1033             Object.keys(groupingViewSpec.elements).forEach(key => {
1034               const elm = groupingViewSpec.elements[key];
1035               // a useRef on root level need a namespace
1036               const resolvedWhen = elm.when && groupingViewSpec.when
1037                 ? {
1038                   type: WhenTokenType.AND,
1039                   left: elm.when,
1040                   right: groupingViewSpec.when,
1041                 }
1042                 : elm.when || groupingViewSpec.when;  
1043               
1044               const resolvedIfFeature = elm.ifFeature
1045                 ? `(${groupingViewSpec.ifFeature}) and (${elm.ifFeature})`
1046                 : groupingViewSpec.ifFeature;
1047
1048               viewSpec.elements[parentId === 0 ? `${module.name}:${key}` : key] = {
1049                 ...elm,
1050                 when: resolvedWhen,
1051                 ifFeature: resolvedIfFeature,
1052               };
1053             });
1054           }
1055         });
1056       }
1057
1058       viewSpec.uses[ResolveFunction] = (parentElementPath: string) => {
1059         const currentElementPath = `${parentElementPath} -> ${viewSpec.ns}:${viewSpec.name}`;
1060         resolveFunctions.forEach(resolve => {
1061           try {
1062             resolve(currentElementPath);
1063           } catch (error) {
1064             console.error(error);
1065           }
1066         });
1067         // console.log("Resolved "+currentElementPath, viewSpec);
1068         if (viewSpec?.uses) {
1069           viewSpec.uses[ResolveFunction] = undefined;
1070         }
1071       };
1072
1073       this._groupingsToResolve.push(viewSpec);
1074     }
1075
1076     return [viewSpec, subViews];
1077   }
1078
1079   // https://tools.ietf.org/html/rfc7950#section-9.3.4
1080   private static decimalRange = [
1081     { min: -9223372036854775808, max: 9223372036854775807 },
1082     { min: -922337203685477580.8, max: 922337203685477580.7 },
1083     { min: -92233720368547758.08, max: 92233720368547758.07 },
1084     { min: -9223372036854775.808, max: 9223372036854775.807 },
1085     { min: -922337203685477.5808, max: 922337203685477.5807 },
1086     { min: -92233720368547.75808, max: 92233720368547.75807 },
1087     { min: -9223372036854.775808, max: 9223372036854.775807 },
1088     { min: -922337203685.4775808, max: 922337203685.4775807 },
1089     { min: -92233720368.54775808, max: 92233720368.54775807 },
1090     { min: -9223372036.854775808, max: 9223372036.854775807 },
1091     { min: -922337203.6854775808, max: 922337203.6854775807 },
1092     { min: -92233720.36854775808, max: 92233720.36854775807 },
1093     { min: -9223372.036854775808, max: 9223372.036854775807 },
1094     { min: -922337.2036854775808, max: 922337.2036854775807 },
1095     { min: -92233.72036854775808, max: 92233.72036854775807 },
1096     { min: -9223.372036854775808, max: 9223.372036854775807 },
1097     { min: -922.3372036854775808, max: 922.3372036854775807 },
1098     { min: -92.23372036854775808, max: 92.23372036854775807 },
1099     { min: -9.223372036854775808, max: 9.223372036854775807 },
1100   ];
1101
1102   /** Extracts the UI View from the type in the cur statement. */
1103   private getViewElement(cur: Statement, module: Module, parentId: number, currentPath: string, isList: boolean): ViewElement {
1104
1105     const type = this.extractValue(cur, 'type');
1106     const defaultVal = this.extractValue(cur, 'default') || undefined;
1107     const description = this.extractValue(cur, 'description') || undefined;
1108
1109     const configValue = this.extractValue(cur, 'config');
1110     const config = configValue == null ? true : configValue.toLocaleLowerCase() !== 'false';
1111
1112     const extractRange = (min: number, max: number, property: string = 'range'): { expression: Expression<YangRange> | undefined; min: number; max: number } => {
1113       const ranges = this.extractValue(this.extractNodes(cur, 'type')[0]!, property) || undefined;
1114       const range = ranges?.replace(/min/i, String(min)).replace(/max/i, String(max)).split('|').map(r => {
1115         let minValue: number;
1116         let maxValue: number;
1117
1118         if (r.indexOf('..') > -1) {
1119           const [minStr, maxStr] = r.split('..');
1120           minValue = Number(minStr);
1121           maxValue = Number(maxStr);
1122         } else if (!isNaN(maxValue = Number(r && r.trim()))) {
1123           minValue = maxValue;
1124         } else {
1125           minValue = min,
1126           maxValue = max;
1127         }
1128
1129         if (minValue > min) min = minValue;
1130         if (maxValue < max) max = maxValue;
1131
1132         return {
1133           min: minValue,
1134           max: maxValue,
1135         };
1136       });
1137       return {
1138         min: min,
1139         max: max,
1140         expression: range && range.length === 1
1141           ? range[0]
1142           : range && range.length > 1
1143             ? { operation: 'OR', arguments: range }
1144             : undefined,
1145       };
1146     };
1147
1148     const extractPattern = (): Expression<RegExp> | undefined => {
1149     // 2023.01.26 decision MF & SKO: we will no longer remove the backslashes from the pattern, seems to be a bug in the original code
1150       const pattern = this.extractNodes(this.extractNodes(cur, 'type')[0]!, 'pattern').map(p => p.arg!).filter(p => !!p).map(p => `^${p/*.replace(/(?:\\(.))/g, '$1')*/}$`);
1151       return pattern && pattern.length == 1
1152         ? new RegExp(pattern[0])
1153         : pattern && pattern.length > 1
1154           ? { operation: 'AND', arguments: pattern.map(p => new RegExp(p)) }
1155           : undefined;
1156     };
1157
1158     const mandatory = this.extractValue(cur, 'mandatory') === 'true' || false;
1159
1160     if (!cur.arg) {
1161       throw new Error(`Module: [${module.name}]. Found element without name.`);
1162     }
1163
1164     if (!type) {
1165       throw new Error(`Module: [${module.name}].[${cur.arg}]. Found element without type.`);
1166     }
1167
1168     const element: ViewElementBase = {
1169       id: parentId === 0 ? `${module.name}:${cur.arg}` : cur.arg,
1170       label: cur.arg,
1171       path: currentPath,
1172       module: module.name || '',
1173       config: config,
1174       mandatory: mandatory,
1175       isList: isList,
1176       default: defaultVal,
1177       description: description,
1178     };
1179
1180     if (type === 'string') {
1181       const length = extractRange(0, +18446744073709551615, 'length');
1182       return ({
1183         ...element,
1184         uiType: 'string',
1185         length: length.expression,
1186         pattern: extractPattern(),
1187       });
1188     } else if (type === 'boolean') {
1189       return ({
1190         ...element,
1191         uiType: 'boolean',
1192       });
1193     } else if (type === 'uint8') {
1194       const range = extractRange(0, +255);
1195       return ({
1196         ...element,
1197         uiType: 'number',
1198         range: range.expression,
1199         min: range.min,
1200         max: range.max,
1201         units: this.extractValue(cur, 'units') || undefined,
1202         format: this.extractValue(cur, 'format') || undefined,
1203       });
1204     } else if (type === 'uint16') {
1205       const range = extractRange(0, +65535);
1206       return ({
1207         ...element,
1208         uiType: 'number',
1209         range: range.expression,
1210         min: range.min,
1211         max: range.max,
1212         units: this.extractValue(cur, 'units') || undefined,
1213         format: this.extractValue(cur, 'format') || undefined,
1214       });
1215     } else if (type === 'uint32') {
1216       const range = extractRange(0, +4294967295);
1217       return ({
1218         ...element,
1219         uiType: 'number',
1220         range: range.expression,
1221         min: range.min,
1222         max: range.max,
1223         units: this.extractValue(cur, 'units') || undefined,
1224         format: this.extractValue(cur, 'format') || undefined,
1225       });
1226     } else if (type === 'uint64') {
1227       const range = extractRange(0, +18446744073709551615);
1228       return ({
1229         ...element,
1230         uiType: 'number',
1231         range: range.expression,
1232         min: range.min,
1233         max: range.max,
1234         units: this.extractValue(cur, 'units') || undefined,
1235         format: this.extractValue(cur, 'format') || undefined,
1236       });
1237     } else if (type === 'int8') {
1238       const range = extractRange(-128, +127);
1239       return ({
1240         ...element,
1241         uiType: 'number',
1242         range: range.expression,
1243         min: range.min,
1244         max: range.max,
1245         units: this.extractValue(cur, 'units') || undefined,
1246         format: this.extractValue(cur, 'format') || undefined,
1247       });
1248     } else if (type === 'int16') {
1249       const range = extractRange(-32768, +32767);
1250       return ({
1251         ...element,
1252         uiType: 'number',
1253         range: range.expression,
1254         min: range.min,
1255         max: range.max,
1256         units: this.extractValue(cur, 'units') || undefined,
1257         format: this.extractValue(cur, 'format') || undefined,
1258       });
1259     } else if (type === 'int32') {
1260       const range = extractRange(-2147483648, +2147483647);
1261       return ({
1262         ...element,
1263         uiType: 'number',
1264         range: range.expression,
1265         min: range.min,
1266         max: range.max,
1267         units: this.extractValue(cur, 'units') || undefined,
1268         format: this.extractValue(cur, 'format') || undefined,
1269       });
1270     } else if (type === 'int64') {
1271       const range = extractRange(-9223372036854775808, +9223372036854775807);
1272       return ({
1273         ...element,
1274         uiType: 'number',
1275         range: range.expression,
1276         min: range.min,
1277         max: range.max,
1278         units: this.extractValue(cur, 'units') || undefined,
1279         format: this.extractValue(cur, 'format') || undefined,
1280       });
1281     } else if (type === 'decimal64') {
1282       // decimalRange
1283       const fDigits = Number(this.extractValue(this.extractNodes(cur, 'type')[0]!, 'fraction-digits')) || -1;
1284       if (fDigits === -1) {
1285         throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found decimal64 with invalid fraction-digits.`);
1286       }
1287       const range = extractRange(YangParser.decimalRange[fDigits].min, YangParser.decimalRange[fDigits].max);
1288       return ({
1289         ...element,
1290         uiType: 'number',
1291         fDigits: fDigits,
1292         range: range.expression,
1293         min: range.min,
1294         max: range.max,
1295         units: this.extractValue(cur, 'units') || undefined,
1296         format: this.extractValue(cur, 'format') || undefined,
1297       });
1298     } else if (type === 'enumeration') {
1299       const typeNode = this.extractNodes(cur, 'type')[0]!;
1300       const enumNodes = this.extractNodes(typeNode, 'enum');
1301       return ({
1302         ...element,
1303         uiType: 'selection',
1304         options: enumNodes.reduce<{ key: string; value: string; description?: string }[]>((acc, enumNode) => {
1305           if (!enumNode.arg) {
1306             throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found option without name.`);
1307           }
1308           // const ifClause = this.extractValue(enumNode, 'if-feature');
1309           const value = this.extractValue(enumNode, 'value');
1310           const enumOption = {
1311             key: enumNode.arg,
1312             value: value != null ? value : enumNode.arg,
1313             description: this.extractValue(enumNode, 'description') || undefined,
1314           };
1315           // todo: ❗ handle the if clause ⚡
1316           acc.push(enumOption);
1317           return acc;
1318         }, []),
1319       });
1320     } else if (type === 'leafref') {
1321       const typeNode = this.extractNodes(cur, 'type')[0]!;
1322       const vPath = this.extractValue(typeNode, 'path');
1323       if (!vPath) {
1324         throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found leafref without path.`);
1325       }
1326       const refPath = this.resolveReferencePath(vPath, module);
1327       const resolve = this.resolveReference.bind(this);
1328       const res: ViewElement = {
1329         ...element,
1330         uiType: 'reference',
1331         referencePath: refPath,
1332         ref(this: ViewElement, basePath: string) {
1333           const elementPath = `${basePath}/${cur.arg}`;
1334
1335           const result = resolve(refPath, elementPath);
1336           if (!result) return undefined;
1337
1338           const [resolvedElement, resolvedPath] = result;
1339           return resolvedElement && [{
1340             ...resolvedElement,
1341             id: this.id,
1342             label: this.label,
1343             config: this.config,
1344             mandatory: this.mandatory,
1345             isList: this.isList,
1346             default: this.default,
1347             description: this.description,
1348           } as ViewElement, resolvedPath] || undefined;
1349         },
1350       };
1351       return res;
1352     } else if (type === 'identityref') {
1353       const typeNode = this.extractNodes(cur, 'type')[0]!;
1354       const base = this.extractValue(typeNode, 'base');
1355       if (!base) {
1356         throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found identityref without base.`);
1357       }
1358       const res: ViewElement = {
1359         ...element,
1360         uiType: 'selection',
1361         options: [],
1362       };
1363       this._identityToResolve.push(() => {
1364         const identity: Identity = this.resolveIdentity(base, module);
1365         if (!identity) {
1366           throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Could not resolve identity [${base}].`);
1367         }
1368         if (!identity.values || identity.values.length === 0) {
1369           throw new Error(`Identity: [${base}] has no values.`);
1370         }
1371         res.options = identity.values.map(val => ({
1372           key: val.id,
1373           value: val.id,
1374           description: val.description,
1375         }));
1376       });
1377       return res;
1378     } else if (type === 'empty') {
1379       // todo: ❗ handle empty ⚡
1380       /*  9.11.  The empty Built-In Type
1381           The empty built-in type represents a leaf that does not have any
1382           value, it conveys information by its presence or absence. */
1383       return {
1384         ...element,
1385         uiType: 'empty',
1386       };
1387     } else if (type === 'union') {
1388       // todo: ❗ handle union ⚡
1389       /* 9.12.  The union Built-In Type */
1390       const typeNode = this.extractNodes(cur, 'type')[0]!;
1391       const typeNodes = this.extractNodes(typeNode, 'type');
1392
1393       const resultingElement = {
1394         ...element,
1395         uiType: 'union',
1396         elements: [],
1397       } as ViewElementUnion;
1398
1399       const resolveUnion = () => {
1400         resultingElement.elements.push(...typeNodes.map(node => {
1401           const stm: Statement = {
1402             ...cur,
1403             sub: [
1404               ...(cur.sub?.filter(s => s.key !== 'type') || []),
1405               node,
1406             ],
1407           };
1408           return {
1409             ...this.getViewElement(stm, module, parentId, currentPath, isList),
1410             id: node.arg!,
1411           };
1412         }));
1413       };
1414
1415       this._unionsToResolve.push(resolveUnion);
1416
1417       return resultingElement;
1418     } else if (type === 'bits') {
1419       const typeNode = this.extractNodes(cur, 'type')[0]!;
1420       const bitNodes = this.extractNodes(typeNode, 'bit');
1421       return {
1422         ...element,
1423         uiType: 'bits',
1424         flags: bitNodes.reduce<{ [name: string]: number | undefined }>((acc, bitNode) => {
1425           if (!bitNode.arg) {
1426             throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found bit without name.`);
1427           }
1428           // const ifClause = this.extractValue(bitNode, 'if-feature');
1429           const pos = Number(this.extractValue(bitNode, 'position'));
1430           acc[bitNode.arg] = pos === pos ? pos : undefined;
1431           return acc;
1432         }, {}),
1433       };
1434     } else if (type === 'binary') {
1435       return {
1436         ...element,
1437         uiType: 'binary',
1438         length: extractRange(0, +18446744073709551615, 'length'),
1439       };
1440     } else if (type === 'instance-identifier') {
1441       // https://tools.ietf.org/html/rfc7950#page-168
1442       return {
1443         ...element,
1444         uiType: 'string',
1445         length: extractRange(0, +18446744073709551615, 'length'),
1446       };
1447     } else {
1448       // not a build in type, need to resolve type
1449       let typeRef = this.resolveType(type, module);
1450       if (typeRef == null) console.error(new Error(`Could not resolve type ${type} in [${module.name}][${currentPath}].`));
1451
1452       if (isViewElementString(typeRef)) {
1453         typeRef = this.resolveStringType(typeRef, extractPattern(), extractRange(0, +18446744073709551615));
1454       } else if (isViewElementNumber(typeRef)) {
1455         typeRef = this.resolveNumberType(typeRef, extractRange(typeRef.min, typeRef.max));
1456       }
1457
1458       const res = {
1459         id: element.id,
1460       } as ViewElement;
1461
1462       this._typeRefToResolve.push(() => {
1463         // spoof date type here from special string type
1464         if ((type === 'date-and-time' || type.endsWith(':date-and-time')) && typeRef.module === 'ietf-yang-types') {
1465           Object.assign(res, {
1466             ...typeRef,
1467             ...element,
1468             description: description,
1469             uiType: 'date',
1470           });
1471         } else {
1472           Object.assign(res, {
1473             ...typeRef,
1474             ...element,
1475             description: description,
1476           });
1477         }
1478       });
1479       
1480       return res;
1481     }
1482   }
1483
1484   private resolveStringType(parentElement: ViewElementString, pattern: Expression<RegExp> | undefined, length: { expression: Expression<YangRange> | undefined; min: number; max: number }) {
1485     return {
1486       ...parentElement,
1487       pattern: pattern != null && parentElement.pattern
1488         ? { operation: 'AND', arguments: [pattern, parentElement.pattern] }
1489         : parentElement.pattern
1490           ? parentElement.pattern
1491           : pattern,
1492       length: length.expression != null && parentElement.length
1493         ? { operation: 'AND', arguments: [length.expression, parentElement.length] }
1494         : parentElement.length
1495           ? parentElement.length
1496           : length?.expression,
1497     } as ViewElementString;
1498   }
1499
1500   private resolveNumberType(parentElement: ViewElementNumber, range: { expression: Expression<YangRange> | undefined; min: number; max: number }) {
1501     return {
1502       ...parentElement,
1503       range: range.expression != null && parentElement.range
1504         ? { operation: 'AND', arguments: [range.expression, parentElement.range] }
1505         : parentElement.range
1506           ? parentElement.range
1507           : range,
1508       min: range.min,
1509       max: range.max,
1510     } as ViewElementNumber;
1511   }
1512
1513   private resolveReferencePath(vPath: string, module: Module) {
1514     const vPathParser = /(?:(?:([^\/\:]+):)?([^\/]+))/g; // 1 = opt: namespace / 2 = property
1515     return vPath.replace(vPathParser, (_, ns, property) => {
1516       const nameSpace = ns && module.imports[ns] || module.name;
1517       return `${nameSpace}:${property}`;
1518     });
1519   }
1520
1521   private resolveReference(vPath: string, currentPath: string) {
1522     const vPathParser = /(?:(?:([^\/\[\]\:]+):)?([^\/\[\]]+)(\[[^\]]+\])?)/g; // 1 = opt: namespace / 2 = property / 3 = opt: indexPath
1523     let element: ViewElement | null = null;
1524     let moduleName = '';
1525
1526     const vPathParts = splitVPath(vPath, vPathParser).map(p => ({ ns: p[1], property: p[2], ind: p[3] }));
1527     const resultPathParts = !vPath.startsWith('/')
1528       ? splitVPath(currentPath, vPathParser).map(p => { moduleName = p[1] || moduleName; return { ns: moduleName, property: p[2], ind: p[3] }; })
1529       : [];
1530
1531     for (let i = 0; i < vPathParts.length; ++i) {
1532       const vPathPart = vPathParts[i];
1533       if (vPathPart.property === '..') {
1534         resultPathParts.pop();
1535       } else if (vPathPart.property !== '.') {
1536         resultPathParts.push(vPathPart);
1537       }
1538     }
1539
1540     // resolve element by path
1541     for (let j = 0; j < resultPathParts.length; ++j) {
1542       const pathPart = resultPathParts[j];
1543       if (j === 0) {
1544         moduleName = pathPart.ns;
1545         const rootModule = this._modules[moduleName];
1546         if (!rootModule) throw new Error('Could not resolve module [' + moduleName + '].\r\n' + vPath);
1547         element = rootModule.elements[`${pathPart.ns}:${pathPart.property}`];
1548       } else if (element && isViewElementObjectOrList(element)) {
1549         const view: ViewSpecification = this._views[+element.viewId];
1550         if (moduleName !== pathPart.ns) {
1551           moduleName = pathPart.ns;
1552         }
1553         element = view.elements[pathPart.property] || view.elements[`${moduleName}:${pathPart.property}`];
1554       } else {
1555         throw new Error('Could not resolve reference.\r\n' + vPath);
1556       }
1557       if (!element) throw new Error('Could not resolve path [' + pathPart.property + '] in [' + currentPath + '] \r\n' + vPath);
1558     }
1559
1560     moduleName = ''; // create the vPath for the resolved element, do not add the element itself this will be done later in the res(...) function
1561     return [element, resultPathParts.slice(0, -1).map(p => `${moduleName !== p.ns ? `${moduleName = p.ns}:` : ''}${p.property}${p.ind || ''}`).join('/')];
1562   }
1563
1564   private resolveView(vPath: string) {
1565     const vPathParser = /(?:(?:([^\/\[\]\:]+):)?([^\/\[\]]+)(\[[^\]]+\])?)/g; // 1 = opt: namespace / 2 = property / 3 = opt: indexPath
1566     let element: ViewElement | null = null;
1567     let partMatch: RegExpExecArray | null;
1568     let view: ViewSpecification | null = null;
1569     let moduleName = '';
1570     if (vPath) do {
1571       partMatch = vPathParser.exec(vPath);
1572       if (partMatch) {
1573         if (element === null) {
1574           moduleName = partMatch[1]!;
1575           const rootModule = this._modules[moduleName];
1576           if (!rootModule) return null;
1577           element = rootModule.elements[`${moduleName}:${partMatch[2]!}`];
1578         } else if (isViewElementObjectOrList(element)) {
1579           view = this._views[+element.viewId];
1580           if (moduleName !== partMatch[1]) {
1581             moduleName = partMatch[1];
1582             element = view.elements[`${moduleName}:${partMatch[2]}`];
1583           } else {
1584             element = view.elements[partMatch[2]];
1585           }
1586         } else {
1587           return null;
1588         }
1589         if (!element) return null;
1590       }
1591     } while (partMatch);
1592     return element && isViewElementObjectOrList(element) && this._views[+element.viewId] || null;
1593   }
1594
1595   private resolveType(type: string, module: Module) {
1596     const colonInd = type.indexOf(':');
1597     const preFix = colonInd > -1 ? type.slice(0, colonInd) : '';
1598     const typeName = colonInd > -1 ? type.slice(colonInd + 1) : type;
1599
1600     const res = preFix
1601       ? this._modules[module.imports[preFix]].typedefs[typeName]
1602       : module.typedefs[typeName];
1603     return res;
1604   }
1605
1606   private resolveGrouping(grouping: string, module: Module) {
1607     const collonInd = grouping.indexOf(':');
1608     const preFix = collonInd > -1 ? grouping.slice(0, collonInd) : '';
1609     const groupingName = collonInd > -1 ? grouping.slice(collonInd + 1) : grouping;
1610
1611     return preFix
1612       ? this._modules[module.imports[preFix]].groupings[groupingName]
1613       : module.groupings[groupingName];
1614
1615   }
1616
1617   private resolveIdentity(identity: string, module: Module) {
1618     const collonInd = identity.indexOf(':');
1619     const preFix = collonInd > -1 ? identity.slice(0, collonInd) : '';
1620     const identityName = collonInd > -1 ? identity.slice(collonInd + 1) : identity;
1621
1622     return preFix
1623       ? this._modules[module.imports[preFix]].identities[identityName]
1624       : module.identities[identityName];
1625
1626   }
1627 }