2 * ============LICENSE_START========================================================================
3 * ONAP : ccsdk feature sdnr wt odlux
4 * =================================================================================================
5 * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved.
6 * =================================================================================================
7 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
8 * in compliance with the License. You may obtain a copy of the License at
10 * http://www.apache.org/licenses/LICENSE-2.0
12 * Unless required by applicable law or agreed to in writing, software distributed under the License
13 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
14 * or implied. See the License for the specific language governing permissions and limitations under
16 * ============LICENSE_END==========================================================================
20 import { Token, Statement, Module, Identity } from "../models/yang";
21 import { ViewSpecification, ViewElement, isViewElementObjectOrList, ViewElementBase, isViewElementReference, ViewElementChoise, ViewElementBinary, ViewElementString, isViewElementString, isViewElementNumber, ViewElementNumber, Expression, YangRange, ViewElementUnion } from "../models/uiModels";
22 import { yangService } from "../services/yangService";
24 export const splitVPath = (vPath: string, vPathParser: RegExp): RegExpMatchArray[] => {
25 const pathParts: RegExpMatchArray[] = [];
26 let partMatch: RegExpExecArray | null;
28 partMatch = vPathParser.exec(vPath);
30 pathParts.push(partMatch);
38 private pos: number = 0;
39 private buf: string = "";
41 constructor(input: string) {
46 private _optable: { [key: string]: string } = {
52 private _isNewline(char: string): boolean {
53 return char === '\r' || char === '\n';
56 private _isWhitespace(char: string): boolean {
57 return char === ' ' || char === '\t' || this._isNewline(char);
60 private _isDigit(char: string): boolean {
61 return char >= '0' && char <= '9';
64 private _isAlpha(char: string): boolean {
65 return (char >= 'a' && char <= 'z') ||
66 (char >= 'A' && char <= 'Z')
69 private _isAlphanum(char: string): boolean {
70 return this._isAlpha(char) || this._isDigit(char) ||
71 char === '_' || char === '-' || char === '.';
74 private _skipNontokens() {
75 while (this.pos < this.buf.length) {
76 const char = this.buf.charAt(this.pos);
77 if (this._isWhitespace(char)) {
85 private _processString(terminator: string | null): Token {
86 // this.pos points at the opening quote. Find the ending quote.
87 let end_index = this.pos + 1;
88 while (end_index < this.buf.length) {
89 const char = this.buf.charAt(end_index);
94 if (terminator === null && (this._isWhitespace(char) || this._optable[char] !== undefined) || char === terminator) {
100 if (end_index >= this.buf.length) {
101 throw Error('Unterminated quote at ' + this.pos);
103 const start = this.pos + (terminator ? 1 : 0);
104 const end = end_index;
107 value: this.buf.substring(start, end),
111 this.pos = terminator ? end + 1 : end;
116 private _processIdentifier(): Token {
117 let endpos = this.pos + 1;
118 while (endpos < this.buf.length &&
119 this._isAlphanum(this.buf.charAt(endpos))) {
125 value: this.buf.substring(this.pos, endpos),
133 private _processNumber(): Token {
134 let endpos = this.pos + 1;
135 while (endpos < this.buf.length &&
136 this._isDigit(this.buf.charAt(endpos))) {
142 value: this.buf.substring(this.pos, endpos),
150 private _processLineComment() {
151 var endpos = this.pos + 2;
152 // Skip until the end of the line
153 while (endpos < this.buf.length && !this._isNewline(this.buf.charAt(endpos))) {
156 this.pos = endpos + 1;
159 private _processBlockComment() {
160 var endpos = this.pos + 2;
161 // Skip until the end of the line
162 while (endpos < this.buf.length && !((this.buf.charAt(endpos) === "/" && this.buf.charAt(endpos - 1) === "*"))) {
165 this.pos = endpos + 1;
168 public tokenize(): Token[] {
169 const result: Token[] = [];
170 this._skipNontokens();
171 while (this.pos < this.buf.length) {
173 const char = this.buf.charAt(this.pos);
174 const op = this._optable[char];
176 if (op !== undefined) {
177 result.push({ name: op, value: char, start: this.pos, end: ++this.pos });
178 } else if (this._isAlpha(char)) {
179 result.push(this._processIdentifier());
180 this._skipNontokens();
181 const peekChar = this.buf.charAt(this.pos);
182 if (this._optable[peekChar] === undefined) {
183 result.push((peekChar !== "'" && peekChar !== '"')
184 ? this._processString(null)
185 : this._processString(peekChar));
187 } else if (char === '/' && this.buf.charAt(this.pos + 1) === "/") {
188 this._processLineComment();
189 } else if (char === '/' && this.buf.charAt(this.pos + 1) === "*") {
190 this._processBlockComment();
192 throw Error('Token error at ' + this.pos + " " + this.buf[this.pos]);
194 this._skipNontokens();
199 public tokenize2(): Statement {
200 let stack: Statement[] = [{ key: "ROOT", sub: [] }];
201 let current: Statement | null = null;
203 this._skipNontokens();
204 while (this.pos < this.buf.length) {
206 const char = this.buf.charAt(this.pos);
207 const op = this._optable[char];
209 if (op !== undefined) {
210 if (op === "L_BRACE") {
211 current && stack.unshift(current);
213 } else if (op === "R_BRACE") {
214 current = stack.shift() || null;
217 } else if (this._isAlpha(char)) {
218 const key = this._processIdentifier().value;
219 this._skipNontokens();
220 let peekChar = this.buf.charAt(this.pos);
222 if (this._optable[peekChar] === undefined) {
223 arg = (peekChar === '"' || peekChar === "'")
224 ? this._processString(peekChar).value
225 : this._processString(null).value;
228 this._skipNontokens();
229 peekChar = this.buf.charAt(this.pos);
230 if (peekChar !== "+") break;
232 this._skipNontokens();
233 peekChar = this.buf.charAt(this.pos);
234 arg += (peekChar === '"' || peekChar === "'")
235 ? this._processString(peekChar).value
236 : this._processString(null).value;
238 current = { key, arg, sub: [] };
239 stack[0].sub!.push(current);
240 } else if (char === '/' && this.buf.charAt(this.pos + 1) === "/") {
241 this._processLineComment();
242 } else if (char === '/' && this.buf.charAt(this.pos + 1) === "*") {
243 this._processBlockComment();
245 throw Error('Token error at ' + this.pos + " " + this.buf.slice(this.pos - 10, this.pos + 10));
247 this._skipNontokens();
249 if (stack[0].key !== "ROOT" || !stack[0].sub![0]) {
250 throw new Error("Internal Perser Error");
252 return stack[0].sub![0];
256 export class YangParser {
257 private _groupingsToResolve: (() => void)[] = [];
258 private _identityToResolve: (() => void)[] = [];
259 private _unionsToResolve: (() => void)[] = [];
262 private _modules: { [name: string]: Module } = {};
263 private _views: ViewSpecification[] = [{
273 public static ResolveStack = Symbol("ResolveStack");
279 public get modules() {
280 return this._modules;
287 public async addCapability(capability: string, version?: string) {
289 if (this._modules[capability]) {
293 const data = await yangService.getCapability(capability, version);
295 throw new Error(`Could not load yang file for ${capability}.`);
298 const rootStatement = new YangLexer(data).tokenize2();
300 if (rootStatement.key !== "module") {
301 throw new Error(`Root element of ${capability} is not a module.`);
303 if (rootStatement.arg !== capability) {
304 throw new Error(`Root element capability ${rootStatement.arg} does not requested ${capability}.`);
307 const module = this._modules[capability] = {
308 name: rootStatement.arg,
320 await this.handleModule(module, rootStatement, capability);
323 private async handleModule(module: Module, rootStatement: Statement, capability: string) {
325 // extract namespace && prefix
326 module.namespace = this.extractValue(rootStatement, "namespace");
327 module.prefix = this.extractValue(rootStatement, "prefix");
329 module.imports[module.prefix] = capability;
333 const revisions = this.extractNodes(rootStatement, "revision");
336 ...revisions.reduce<{ [version: string]: {} }>((acc, version) => {
338 throw new Error(`Module [${module.name}] has a version w/o version number.`);
340 const description = this.extractValue(version, "description");
341 const reference = this.extractValue(version, "reference");
351 const features = this.extractNodes(rootStatement, "feature");
354 ...features.reduce<{ [version: string]: {} }>((acc, feature) => {
356 throw new Error(`Module [${module.name}] has a feature w/o name.`);
358 const description = this.extractValue(feature, "description");
367 const imports = this.extractNodes(rootStatement, "import");
370 ...imports.reduce<{ [key: string]: string }>((acc, imp) => {
371 const prefix = imp.sub && imp.sub.filter(s => s.key === "prefix");
373 throw new Error(`Module [${module.name}] has an import with neither name nor prefix.`);
375 acc[prefix && prefix.length === 1 && prefix[0].arg || imp.arg] = imp.arg;
380 // import all required files
381 if (imports) for (let ind = 0; ind < imports.length; ++ind) {
382 await this.addCapability(imports[ind].arg!);
385 this.extractTypeDefinitions(rootStatement, module, "");
387 this.extractIdentites(rootStatement, 0, module, "");
389 const groupings = this.extractGroupings(rootStatement, 0, module, "");
390 this._views.push(...groupings);
392 const augments = this.extractAugments(rootStatement, 0, module, "");
393 this._views.push(...augments);
395 // the default for config on module level is config = true;
396 const [currentView, subViews] = this.extractSubViews(rootStatement, 0, module, "");
397 this._views.push(currentView, ...subViews);
399 // create the root elements for this module
400 module.elements = currentView.elements;
401 Object.keys(module.elements).forEach(key => {
402 const viewElement = module.elements[key];
403 if (!isViewElementObjectOrList(viewElement)) {
404 throw new Error(`Module: [${module}]. Only List or Object allowed on root level.`);
406 const viewIdIndex = Number(viewElement.viewId);
407 module.views[key] = this._views[viewIdIndex];
408 this._views[0].elements[key] = module.elements[key];
413 public postProcess() {
415 // execute all post processes like resolving in propper order
416 this._unionsToResolve.forEach(cb => {
417 try { cb(); } catch (error) {
418 console.warn(error.message);
422 // process all groupings
423 this._groupingsToResolve.forEach(cb => {
424 try { cb(); } catch (error) {
425 console.warn(`Error resolving: [${error.message}]`);
429 // process all augmentations
430 Object.keys(this.modules).forEach(modKey => {
431 const module = this.modules[modKey];
432 Object.keys(module.augments).forEach(augKey => {
433 const augments = module.augments[augKey];
434 const viewSpec = this.resolveView(augKey);
435 if (!viewSpec) console.warn(`Could not find view to augment [${augKey}] in [${module.name}].`);
436 if (augments && viewSpec) {
437 augments.forEach(augment => Object.keys(augment.elements).forEach(key => {
438 const elm = augment.elements[key];
439 viewSpec.elements[key] = {
440 ...augment.elements[key],
441 when: elm.when ? `(${augment.when}) and (${elm.when})` : augment.when,
442 ifFeature: elm.ifFeature ? `(${augment.ifFeature}) and (${elm.ifFeature})` : augment.ifFeature,
449 // process Identities
450 const traverseIdentity = (identities: Identity[]) => {
451 const result: Identity[] = [];
452 for (let identity of identities) {
453 if (identity.children && identity.children.length > 0) {
454 result.push(...traverseIdentity(identity.children));
456 result.push(identity);
463 const baseIdentites: Identity[] = [];
464 Object.keys(this.modules).forEach(modKey => {
465 const module = this.modules[modKey];
466 Object.keys(module.identities).forEach(idKey => {
467 const identity = module.identities[idKey];
468 if (identity.base != null) {
469 const base = this.resolveIdentity(identity.base, module);
470 base.children ?.push(identity);
472 baseIdentites.push(identity);
476 baseIdentites.forEach(identity => {
477 identity.values = identity.children && traverseIdentity(identity.children) || [];
480 this._identityToResolve.forEach(cb => {
481 try { cb(); } catch (error) {
482 console.warn(error.message);
488 private get nextId() {
489 return this._nextId++;
492 private extractNodes(statement: Statement, key: string): Statement[] {
493 return statement.sub && statement.sub.filter(s => s.key === key) || [];
496 private extractValue(statement: Statement, key: string): string | undefined;
497 private extractValue(statement: Statement, key: string, parser: RegExp): RegExpExecArray | undefined;
498 private extractValue(statement: Statement, key: string, parser?: RegExp): string | RegExpExecArray | undefined {
499 const typeNodes = this.extractNodes(statement, key);
500 const rawValue = typeNodes.length > 0 && typeNodes[0].arg || undefined;
502 ? rawValue && parser.exec(rawValue) || undefined
506 private extractTypeDefinitions(statement: Statement, module: Module, currentPath: string): void {
507 const typedefs = this.extractNodes(statement, "typedef");
508 typedefs && typedefs.forEach(def => {
510 throw new Error(`Module: [${module.name}]. Found typefed without name.`);
512 module.typedefs[def.arg] = this.getViewElement(def, module, 0, currentPath, false);
516 /** Handles Goupings like named Container */
517 private extractGroupings(statement: Statement, parentId: number, module: Module, currentPath: string): ViewSpecification[] {
518 const subViews: ViewSpecification[] = [];
519 const groupings = this.extractNodes(statement, "grouping");
520 if (groupings && groupings.length > 0) {
521 subViews.push(...groupings.reduce<ViewSpecification[]>((acc, cur) => {
523 throw new Error(`Module: [${module.name}][${currentPath}]. Found grouping without name.`);
525 const grouping = cur.arg;
527 // the default for config on module level is config = true;
528 const [currentView, subViews] = this.extractSubViews(cur, parentId, module, currentPath);
529 grouping && (module.groupings[grouping] = currentView);
530 acc.push(currentView, ...subViews);
538 /** Handles Augmants also like named Container */
539 private extractAugments(statement: Statement, parentId: number, module: Module, currentPath: string): ViewSpecification[] {
540 const subViews: ViewSpecification[] = [];
541 const augments = this.extractNodes(statement, "augment");
542 if (augments && augments.length > 0) {
543 subViews.push(...augments.reduce<ViewSpecification[]>((acc, cur) => {
545 throw new Error(`Module: [${module.name}][${currentPath}]. Found augment without path.`);
547 const augment = this.resolveReferencePath(cur.arg, module);
549 // the default for config on module level is config = true;
550 const [currentView, subViews] = this.extractSubViews(cur, parentId, module, currentPath);
552 module.augments[augment] = module.augments[augment] || [];
553 module.augments[augment].push(currentView);
555 acc.push(currentView, ...subViews);
563 /** Handles Identities */
564 private extractIdentites(statement: Statement, parentId: number, module: Module, currentPath: string) {
565 const identities = this.extractNodes(statement, "identity");
566 module.identities = identities.reduce<{ [name: string]: Identity }>((acc, cur) => {
568 throw new Error(`Module: [${module.name}][${currentPath}]. Found identiy without name.`);
571 id: `${module.name}:${cur.arg}`,
573 base: this.extractValue(cur, "base"),
574 description: this.extractValue(cur, "description"),
575 reference: this.extractValue(cur, "reference"),
582 private extractSubViews(statement: Statement, parentId: number, module: Module, currentPath: string): [ViewSpecification, ViewSpecification[]] {
583 const subViews: ViewSpecification[] = [];
584 const currentId = this.nextId;
585 let elements: ViewElement[] = [];
587 const configValue = this.extractValue(statement, "config");
588 const config = configValue == null ? true : configValue.toLocaleLowerCase() !== "false";
590 // extract conditions
591 const ifFeature = this.extractValue(statement, "if-feature");
592 const whenCondition = this.extractValue(statement, "when");
593 if (whenCondition) console.warn("Found in [" + module.name + "]" + currentPath + " when: " + whenCondition);
595 // extract all container
596 const container = this.extractNodes(statement, "container");
597 if (container && container.length > 0) {
598 subViews.push(...container.reduce<ViewSpecification[]>((acc, cur) => {
600 throw new Error(`Module: [${module.name}]${currentPath}. Found container without name.`);
602 const [currentView, subViews] = this.extractSubViews(cur, currentId, module, `${currentPath}/${module.name}:${cur.arg}`);
604 id: parentId === 0 ? `${module.name}:${cur.arg}` : cur.arg,
607 viewId: currentView.id,
610 acc.push(currentView, ...subViews);
616 // a list is a list of containers with the leafs contained in the list
617 const lists = this.extractNodes(statement, "list");
618 if (lists && lists.length > 0) {
619 subViews.push(...lists.reduce<ViewSpecification[]>((acc, cur) => {
621 throw new Error(`Module: [${module.name}]${currentPath}. Found list without name.`);
623 const key = this.extractValue(cur, "key") || undefined;
624 if (config && !key) {
625 throw new Error(`Module: [${module.name}]${currentPath}. Found configurable list without key.`);
627 const [currentView, subViews] = this.extractSubViews(cur, currentId, module, `${currentPath}/${module.name}:${cur.arg}`);
629 id: parentId === 0 ? `${module.name}:${cur.arg}` : cur.arg,
633 viewId: currentView.id,
637 acc.push(currentView, ...subViews);
642 // process all leaf-lists
643 // a leaf-list is a list of some type
644 const leafLists = this.extractNodes(statement, "leaf-list");
645 if (leafLists && leafLists.length > 0) {
646 elements.push(...leafLists.reduce<ViewElement[]>((acc, cur) => {
647 const element = this.getViewElement(cur, module, parentId, currentPath, true);
648 element && acc.push(element);
654 // a leaf is mainly a property of an object
655 const leafs = this.extractNodes(statement, "leaf");
656 if (leafs && leafs.length > 0) {
657 elements.push(...leafs.reduce<ViewElement[]>((acc, cur) => {
658 const element = this.getViewElement(cur, module, parentId, currentPath, false);
659 element && acc.push(element);
665 const choiceStms = this.extractNodes(statement, "choice");
666 if (choiceStms && choiceStms.length > 0) {
667 elements.push(...choiceStms.reduce<ViewElementChoise[]>((accChoise, curChoise) => {
668 if (!curChoise.arg) {
669 throw new Error(`Module: [${module.name}]${currentPath}. Found choise without name.`);
671 // extract all cases like containers
672 const cases: { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } }[] = [];
673 const caseStms = this.extractNodes(curChoise, "case");
674 if (caseStms && caseStms.length > 0) {
675 cases.push(...caseStms.reduce((accCase, curCase) => {
677 throw new Error(`Module: [${module.name}]${currentPath}/${curChoise.arg}. Found case without name.`);
679 const description = this.extractValue(curCase, "description") || undefined;
680 const [caseView, caseSubViews] = this.extractSubViews(curCase, parentId, module, `${currentPath}/${module.name}:${curChoise.arg}`);
681 subViews.push(caseView, ...caseSubViews);
683 const caseDef: { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } } = {
684 id: parentId === 0 ? `${module.name}:${curCase.arg}` : curCase.arg,
686 description: description,
687 elements: caseView.elements
689 accCase.push(caseDef);
691 }, [] as { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } }[]));
694 // extract all simple cases (one case per leaf, container, etc.)
695 const [choiseView, choiseSubViews] = this.extractSubViews(curChoise, parentId, module, `${currentPath}/${module.name}:${curChoise.arg}`);
696 subViews.push(choiseView, ...choiseSubViews);
697 cases.push(...Object.keys(choiseView.elements).reduce((accElm, curElm) => {
698 const elm = choiseView.elements[curElm];
699 const caseDef: { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } } = {
702 description: elm.description,
703 elements: { [elm.id]: elm }
705 accElm.push(caseDef);
707 }, [] as { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } }[]));
709 const description = this.extractValue(curChoise, "description") || undefined;
710 const configValue = this.extractValue(curChoise, "config");
711 const config = configValue == null ? true : configValue.toLocaleLowerCase() !== "false";
713 const mandatory = this.extractValue(curChoise, "mandatory") === "true" || false;
715 const element: ViewElementChoise = {
717 id: parentId === 0 ? `${module.name}:${curChoise.arg}` : curChoise.arg,
718 label: curChoise.arg,
720 mandatory: mandatory,
721 description: description,
722 cases: cases.reduce((acc, cur) => {
725 }, {} as { [name: string]: { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } } })
728 accChoise.push(element);
733 const rpcs = this.extractNodes(statement, "rpc");
734 if (rpcs && rpcs.length > 0) {
738 if (!statement.arg) {
739 throw new Error(`Module: [${module.name}]. Found statement without name.`);
742 const viewSpec: ViewSpecification = {
743 id: String(currentId),
744 parentView: String(parentId),
746 title: statement.arg,
749 ifFeature: ifFeature,
751 elements: elements.reduce<{ [name: string]: ViewElement }>((acc, cur) => {
757 // evaluate canEdit depending on all conditions
758 Object.defineProperty(viewSpec, "canEdit", {
760 return Object.keys(viewSpec.elements).some(key => {
761 const elm = viewSpec.elements[key];
762 return (!isViewElementObjectOrList(elm) && elm.config);
767 // merge in all uses references and resolve groupings
768 const usesRefs = this.extractNodes(statement, "uses");
769 if (usesRefs && usesRefs.length > 0) {
771 viewSpec.uses = (viewSpec.uses || []);
772 for (let i = 0; i < usesRefs.length; ++i) {
773 const groupingName = usesRefs[i].arg;
775 throw new Error(`Module: [${module.name}]. Found an uses statement without a grouping name.`);
778 viewSpec.uses.push(this.resolveReferencePath(groupingName, module));
780 this._groupingsToResolve.push(() => {
781 const groupingViewSpec = this.resolveGrouping(groupingName, module);
782 if (groupingViewSpec) {
783 Object.keys(groupingViewSpec.elements).forEach(key => {
784 const elm = groupingViewSpec.elements[key];
785 viewSpec.elements[key] = {
786 ...groupingViewSpec.elements[key],
787 when: elm.when ? `(${groupingViewSpec.when}) and (${elm.when})` : groupingViewSpec.when,
788 ifFeature: elm.ifFeature ? `(${groupingViewSpec.ifFeature}) and (${elm.ifFeature})` : groupingViewSpec.ifFeature,
796 return [viewSpec, subViews];
799 // https://tools.ietf.org/html/rfc7950#section-9.3.4
800 private static decimalRange = [
801 { min: -9223372036854775808, max: 9223372036854775807 },
802 { min: -922337203685477580.8, max: 922337203685477580.7 },
803 { min: -92233720368547758.08, max: 92233720368547758.07 },
804 { min: -9223372036854775.808, max: 9223372036854775.807 },
805 { min: -922337203685477.5808, max: 922337203685477.5807 },
806 { min: -92233720368547.75808, max: 92233720368547.75807 },
807 { min: -9223372036854.775808, max: 9223372036854.775807 },
808 { min: -922337203685.4775808, max: 922337203685.4775807 },
809 { min: -92233720368.54775808, max: 92233720368.54775807 },
810 { min: -9223372036.854775808, max: 9223372036.854775807 },
811 { min: -922337203.6854775808, max: 922337203.6854775807 },
812 { min: -92233720.36854775808, max: 92233720.36854775807 },
813 { min: -9223372.036854775808, max: 9223372.036854775807 },
814 { min: -922337.2036854775808, max: 922337.2036854775807 },
815 { min: -92233.72036854775808, max: 92233.72036854775807 },
816 { min: -9223.372036854775808, max: 9223.372036854775807 },
817 { min: -922.3372036854775808, max: 922.3372036854775807 },
818 { min: -92.23372036854775808, max: 92.23372036854775807 },
819 { min: -9.223372036854775808, max: 9.223372036854775807 },
822 /** Extracts the UI View from the type in the cur statement. */
823 private getViewElement(cur: Statement, module: Module, parentId: number, currentPath: string, isList: boolean): ViewElement {
825 const type = this.extractValue(cur, "type");
826 const defaultVal = this.extractValue(cur, "default") || undefined;
827 const description = this.extractValue(cur, "description") || undefined;
829 const configValue = this.extractValue(cur, "config");
830 const config = configValue == null ? true : configValue.toLocaleLowerCase() !== "false";
832 const extractRange = (min: number, max: number, property: string = "range"): { expression: Expression<YangRange> | undefined, min: number, max: number } => {
833 const ranges = this.extractValue(this.extractNodes(cur, "type")[0]!, property) || undefined;
834 const range = ranges ?.replace(/min/i, String(min)).replace(/max/i, String(max)).split("|").map(r => {
835 const [minStr, maxStr] = r.split('..');
836 const minValue = Number(minStr);
837 const maxValue = Number(maxStr);
839 if (minValue > min) min = minValue;
840 if (maxValue < max) max = maxValue;
850 expression: range && range.length === 1
852 : range && range.length > 1
853 ? { operation: "OR", arguments: range }
858 const extractPattern = (): Expression<RegExp> | undefined => {
859 const pattern = this.extractNodes(this.extractNodes(cur, "type")[0]!, "pattern").map(p => p.arg!).filter(p => !!p).map(p => `^${p}$`);
860 return pattern && pattern.length == 1
861 ? new RegExp(pattern[0])
862 : pattern && pattern.length > 1
863 ? { operation: "AND", arguments: pattern.map(p => new RegExp(p)) }
867 const mandatory = this.extractValue(cur, "mandatory") === "true" || false;
870 throw new Error(`Module: [${module.name}]. Found element without name.`);
874 throw new Error(`Module: [${module.name}].[${cur.arg}]. Found element without type.`);
877 const element: ViewElementBase = {
878 id: parentId === 0 ? `${module.name}:${cur.arg}` : cur.arg,
881 mandatory: mandatory,
884 description: description
887 if (type === "string") {
888 const length = extractRange(0, +18446744073709551615, "length");
892 length: length.expression,
893 pattern: extractPattern(),
895 } else if (type === "boolean") {
900 } else if (type === "uint8") {
901 const range = extractRange(0, +255);
905 range: range.expression,
908 units: this.extractValue(cur, "units") || undefined,
909 format: this.extractValue(cur, "format") || undefined,
911 } else if (type === "uint16") {
912 const range = extractRange(0, +65535);
916 range: range.expression,
919 units: this.extractValue(cur, "units") || undefined,
920 format: this.extractValue(cur, "format") || undefined,
922 } else if (type === "uint32") {
923 const range = extractRange(0, +4294967295);
927 range: range.expression,
930 units: this.extractValue(cur, "units") || undefined,
931 format: this.extractValue(cur, "format") || undefined,
933 } else if (type === "uint64") {
934 const range = extractRange(0, +18446744073709551615);
938 range: range.expression,
941 units: this.extractValue(cur, "units") || undefined,
942 format: this.extractValue(cur, "format") || undefined,
944 } else if (type === "int8") {
945 const range = extractRange(-128, +127);
949 range: range.expression,
952 units: this.extractValue(cur, "units") || undefined,
953 format: this.extractValue(cur, "format") || undefined,
955 } else if (type === "int16") {
956 const range = extractRange(-32768, +32767);
960 range: range.expression,
963 units: this.extractValue(cur, "units") || undefined,
964 format: this.extractValue(cur, "format") || undefined,
966 } else if (type === "int32") {
967 const range = extractRange(-2147483648, +2147483647);
971 range: range.expression,
974 units: this.extractValue(cur, "units") || undefined,
975 format: this.extractValue(cur, "format") || undefined,
977 } else if (type === "int64") {
978 const range = extractRange(-9223372036854775808, +9223372036854775807);
982 range: range.expression,
985 units: this.extractValue(cur, "units") || undefined,
986 format: this.extractValue(cur, "format") || undefined,
988 } else if (type === "decimal64") {
990 const fDigits = Number(this.extractValue(this.extractNodes(cur, "type")[0]!, "fraction-digits")) || -1;
991 if (fDigits === -1) {
992 throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found decimal64 with invalid fraction-digits.`);
994 const range = extractRange(YangParser.decimalRange[fDigits].min, YangParser.decimalRange[fDigits].max);
999 range: range.expression,
1002 units: this.extractValue(cur, "units") || undefined,
1003 format: this.extractValue(cur, "format") || undefined,
1005 } else if (type === "enumeration") {
1006 const typeNode = this.extractNodes(cur, "type")[0]!;
1007 const enumNodes = this.extractNodes(typeNode, "enum");
1010 uiType: "selection",
1011 options: enumNodes.reduce<{ key: string; value: string; description?: string }[]>((acc, enumNode) => {
1012 if (!enumNode.arg) {
1013 throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found option without name.`);
1015 const ifClause = this.extractValue(enumNode, "if-feature");
1016 const value = this.extractValue(enumNode, "value");
1017 const enumOption = {
1019 value: value != null ? value : enumNode.arg,
1020 description: this.extractValue(enumNode, "description") || undefined
1022 // todo: ❗ handle the if clause ⚡
1023 acc.push(enumOption);
1027 } else if (type === "leafref") {
1028 const typeNode = this.extractNodes(cur, "type")[0]!;
1029 const vPath = this.extractValue(typeNode, "path");
1031 throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found leafref without path.`);
1033 const refPath = this.resolveReferencePath(vPath, module);
1034 const resolve = this.resolveReference.bind(this);
1035 const res: ViewElement = {
1037 uiType: "reference",
1038 referencePath: refPath,
1039 ref(this: ViewElement, currentPath: string) {
1040 const resolved = resolve(refPath, currentPath);
1041 return resolved && {
1045 config: this.config,
1046 mandatory: this.mandatory,
1047 isList: this.isList,
1048 default: this.default,
1049 description: this.description,
1054 } else if (type === "identityref") {
1055 const typeNode = this.extractNodes(cur, "type")[0]!;
1056 const base = this.extractValue(typeNode, "base");
1058 throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found identityref without base.`);
1060 const res: ViewElement = {
1062 uiType: "selection",
1065 this._identityToResolve.push(() => {
1066 const identity: Identity = this.resolveIdentity(base, module);
1068 throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Could not resolve identity [${base}].`);
1070 if (!identity.values || identity.values.length === 0) {
1071 throw new Error(`Identity: [${base}] has no values.`);
1073 res.options = identity.values.map(val => ({
1076 description: val.description
1080 } else if (type === "empty") {
1081 // todo: ❗ handle empty ⚡
1082 /* 9.11. The empty Built-In Type
1083 The empty built-in type represents a leaf that does not have any
1084 value, it conveys information by its presence or absence. */
1085 console.warn(`found type: empty in [${module.name}][${currentPath}][${element.label}]`);
1090 } else if (type === "union") {
1091 // todo: ❗ handle union ⚡
1092 /* 9.12. The union Built-In Type */
1093 const typeNode = this.extractNodes(cur, "type")[0]!;
1094 const typeNodes = this.extractNodes(typeNode, "type");
1096 const resultingElement = {
1100 } as ViewElementUnion;
1102 const resolveUnion = () => {
1103 resultingElement.elements.push(...typeNodes.map(node => {
1104 const stm: Statement = {
1107 ...(cur.sub ?.filter(s => s.key !== "type") || []),
1112 ...this.getViewElement(stm, module, parentId, currentPath, isList),
1118 this._unionsToResolve.push(resolveUnion);
1120 return resultingElement;
1121 } else if (type === "bits") {
1122 const typeNode = this.extractNodes(cur, "type")[0]!;
1123 const bitNodes = this.extractNodes(typeNode, "bit");
1127 flags: bitNodes.reduce<{ [name: string]: number | undefined; }>((acc, bitNode) => {
1129 throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found bit without name.`);
1131 const ifClause = this.extractValue(bitNode, "if-feature");
1132 const pos = Number(this.extractValue(bitNode, "position"));
1133 acc[bitNode.arg] = pos === pos ? pos : undefined;
1137 } else if (type === "binary") {
1141 length: extractRange(0, +18446744073709551615, "length"),
1144 // not a build in type, have to resolve type
1145 let typeRef = this.resolveType(type, module);
1146 if (typeRef == null) console.error(new Error(`Could not resolve type ${type} in [${module.name}][${currentPath}].`));
1148 if (isViewElementString(typeRef)) {
1149 typeRef = this.resolveStringType(typeRef, extractPattern(), extractRange(0, +18446744073709551615));
1151 } else if (isViewElementNumber(typeRef)) {
1152 typeRef = this.resolveNumberType(typeRef, extractRange(typeRef.min, typeRef.max));
1158 description: description,
1163 private resolveStringType(parentElement: ViewElementString, pattern: Expression<RegExp> | undefined, length: { expression: Expression<YangRange> | undefined, min: number, max: number }) {
1166 pattern: pattern != null && parentElement.pattern
1167 ? { operation: "AND", arguments: [pattern, parentElement.pattern] }
1168 : parentElement.pattern
1169 ? parentElement.pattern
1171 length: length.expression != null && parentElement.length
1172 ? { operation: "AND", arguments: [length.expression, parentElement.length] }
1173 : parentElement.length
1174 ? parentElement.length
1175 : length ?.expression,
1176 } as ViewElementString;
1179 private resolveNumberType(parentElement: ViewElementNumber, range: { expression: Expression<YangRange> | undefined, min: number, max: number }) {
1182 range: range.expression != null && parentElement.range
1183 ? { operation: "AND", arguments: [range.expression, parentElement.range] }
1184 : parentElement.range
1185 ? parentElement.range
1189 } as ViewElementNumber;
1192 private resolveReferencePath(vPath: string, module: Module) {
1193 const vPathParser = /(?:(?:([^\/\:]+):)?([^\/]+))/g // 1 = opt: namespace / 2 = property
1194 return vPath.replace(vPathParser, (_, ns, property) => {
1195 const nameSpace = ns && module.imports[ns] || module.name;
1196 return `${nameSpace}:${property}`;
1200 private resolveReference(vPath: string, currentPath: string) {
1201 const vPathParser = /(?:(?:([^\/\[\]\:]+):)?([^\/\[\]]+)(\[[^\]]+\])?)/g // 1 = opt: namespace / 2 = property / 3 = opt: indexPath
1202 let element: ViewElement | null = null;
1203 let moduleName = "";
1205 const vPathParts = splitVPath(vPath, vPathParser).map(p => ({ ns: p[1], property: p[2], ind: p[3] }));
1206 const resultPathParts = !vPath.startsWith("/")
1207 ? splitVPath(currentPath, vPathParser).map(p => ({ ns: p[1], property: p[2], ind: p[3] }))
1210 for (let i = 0; i < vPathParts.length; ++i) {
1211 const vPathPart = vPathParts[i];
1212 if (vPathPart.property === "..") {
1213 resultPathParts.pop();
1214 } else if (vPathPart.property !== ".") {
1215 resultPathParts.push(vPathPart);
1219 // resolve element by path
1220 for (let j = 0; j < resultPathParts.length; ++j) {
1221 const pathPart = resultPathParts[j];
1223 moduleName = pathPart.ns;
1224 const rootModule = this._modules[moduleName];
1225 if (!rootModule) throw new Error("Could not resolve module [" + moduleName + "].\r\n" + vPath);
1226 element = rootModule.elements[`${pathPart.ns}:${pathPart.property}`];
1227 } else if (element && isViewElementObjectOrList(element)) {
1228 const view: ViewSpecification = this._views[+element.viewId];
1229 if (moduleName !== pathPart.ns) {
1230 moduleName = pathPart.ns;
1231 element = view.elements[`${moduleName}:${pathPart.property}`];
1233 element = view.elements[pathPart.property] || view.elements[`${moduleName}:${pathPart.property}`];
1236 throw new Error("Could not resolve reference.\r\n" + vPath);
1238 if (!element) throw new Error("Could not resolve path [" + pathPart.property + "] in [" + currentPath + "] \r\n" + vPath);
1244 private resolveView(vPath: string) {
1245 const vPathParser = /(?:(?:([^\/\[\]\:]+):)?([^\/\[\]]+)(\[[^\]]+\])?)/g // 1 = opt: namespace / 2 = property / 3 = opt: indexPath
1246 let element: ViewElement | null = null;
1247 let partMatch: RegExpExecArray | null;
1248 let view: ViewSpecification | null = null;
1249 let moduleName = "";
1251 partMatch = vPathParser.exec(vPath);
1253 if (element === null) {
1254 moduleName = partMatch[1]!;
1255 const rootModule = this._modules[moduleName];
1256 if (!rootModule) return null;
1257 element = rootModule.elements[`${moduleName}:${partMatch[2]!}`];
1258 } else if (isViewElementObjectOrList(element)) {
1259 view = this._views[+element.viewId];
1260 if (moduleName !== partMatch[1]) {
1261 moduleName = partMatch[1];
1262 element = view.elements[`${moduleName}:${partMatch[2]}`];
1264 element = view.elements[partMatch[2]];
1269 if (!element) return null;
1272 return element && isViewElementObjectOrList(element) && this._views[+element.viewId] || null;
1275 private resolveType(type: string, module: Module) {
1276 const collonInd = type.indexOf(":");
1277 const preFix = collonInd > -1 ? type.slice(0, collonInd) : "";
1278 const typeName = collonInd > -1 ? type.slice(collonInd + 1) : type;
1281 ? this._modules[module.imports[preFix]].typedefs[typeName]
1282 : module.typedefs[typeName];
1286 private resolveGrouping(grouping: string, module: Module) {
1287 const collonInd = grouping.indexOf(":");
1288 const preFix = collonInd > -1 ? grouping.slice(0, collonInd) : "";
1289 const groupingName = collonInd > -1 ? grouping.slice(collonInd + 1) : grouping;
1292 ? this._modules[module.imports[preFix]].groupings[groupingName]
1293 : module.groupings[groupingName];
1297 private resolveIdentity(identity: string, module: Module) {
1298 const collonInd = identity.indexOf(":");
1299 const preFix = collonInd > -1 ? identity.slice(0, collonInd) : "";
1300 const identityName = collonInd > -1 ? identity.slice(collonInd + 1) : identity;
1303 ? this._modules[module.imports[preFix]].identities[identityName]
1304 : module.identities[identityName];