UNPKG

@hashgraph/solo

Version:

An opinionated CLI tool to deploy and manage private Hedera Networks.

368 lines (297 loc) 10.9 kB
// SPDX-License-Identifier: Apache-2.0 import {type KeyFormatter} from '../key-formatter.js'; import {ConfigKeyFormatter} from '../config-key-formatter.js'; import {type Node} from './node.js'; import {LexerInternalNode} from './lexer-internal-node.js'; import {LexerLeafNode} from './lexer-leaf-node.js'; import {KeyName} from '../key-name.js'; import {ConfigKeyError} from '../config-key-error.js'; import {IllegalArgumentError} from '../../../business/errors/illegal-argument-error.js'; import {FlatKeyMapper} from '../../mapper/impl/flat-key-mapper.js'; export class Lexer { private readonly _roots: Map<string, Node> = new Map(); private readonly flatMapper: FlatKeyMapper; private _rendered: boolean = false; public constructor( public readonly tokens: Map<string, string>, private readonly formatter: KeyFormatter = ConfigKeyFormatter.instance(), ) { if (!this.tokens) { throw new ConfigKeyError('tokens must be provided'); } if (!this.formatter) { throw new ConfigKeyError('formatter must be provided'); } this.flatMapper = new FlatKeyMapper(this.formatter); } public get rendered(): boolean { return this._rendered; } private set rendered(rendered: boolean) { this._rendered = rendered; } public get rootNodes(): Node[] { if (!this.rendered) { this.renderTrees(); } return [...this._roots.values()]; } public get tree(): Map<string, Node> { if (!this.rendered) { this.renderTrees(); } return this._roots; } public nodeFor(key: string): Node { if (!key) { throw new IllegalArgumentError('key must not be null or undefined'); } const segments: string[] = this.formatter.split(key); if (segments.length === 0 || segments[0].trim().length === 0) { throw new IllegalArgumentError('key must not be empty'); } let currentNode: Node = this.tree.get(segments[0]); if (!currentNode) { return null; } for (let index: number = 1; index < segments.length; index++) { const segment: string = segments[index]; if (currentNode.isLeaf()) { return null; } const inode: LexerInternalNode = currentNode as LexerInternalNode; const nextNode: Node = inode.children.find((n): boolean => n.name === segment); if (!nextNode) { return null; } currentNode = nextNode; } return currentNode; } public addValue(key: string, value: string | null): void { if (!key) { throw new IllegalArgumentError('key must not be null or undefined'); } const segments: string[] = this.formatter.split(this.formatter.normalize(key)); const rootNode: Node = this._roots.has(segments[0]) ? this._roots.get(segments[0]) : this.rootNodeFor(segments); this.processSegments(rootNode as LexerInternalNode, value, segments); this.tokens.set(key, value); } public addOrReplaceObject(key: string, value: object | null): void { if (!key) { throw new IllegalArgumentError('key must not be null or undefined'); } const normalizedKey: string = this.formatter.normalize(key); this.addOrReplaceValue(normalizedKey, null); if (!value) { return; } const flatMap: Map<string, string> = this.flatMapper.flatten(value); for (const [k, v] of flatMap.entries()) { this.addOrReplaceValue(this.formatter.join(normalizedKey, k), v); } } public addOrReplaceArray<T>(key: string, values: T[] | null): void { if (!key) { throw new IllegalArgumentError('key must not be null or undefined'); } const normalizedKey: string = this.formatter.normalize(key); const node: Node = this.nodeFor(normalizedKey); if (node && !node.isArray()) { if (node.isRoot()) { this._roots.delete(node.name); this.tokens.delete(node.name); } else { const parent: LexerInternalNode = node.parent as LexerInternalNode; parent.remove(node); this.tokens.delete(node.path()); } } if (!values) { return; } for (const [index, value] of values.entries()) { this.addOrReplaceArrayElement(normalizedKey, index, value); } } private addOrReplaceArrayElement<T>(normalizedKey: string, index: number, value: T | null): void { if (value === null || value === undefined) { this.addOrReplaceValue(this.formatter.join(normalizedKey, index.toString()), null); } const valueType = typeof value; if (valueType === 'string' || valueType === 'number' || valueType === 'boolean' || valueType === 'bigint') { this.addOrReplaceValue(this.formatter.join(normalizedKey, index.toString()), value.toString()); } else { const flatMap: Map<string, string> = this.flatMapper.flatten(value as object); for (const [k, value_] of flatMap.entries()) { this.addOrReplaceValue(this.formatter.join(normalizedKey, index.toString(), k), value_); } } } public addOrReplaceValue(key: string, value: string | null): void { if (!key) { throw new IllegalArgumentError('key must not be null or undefined'); } const node: Node = this.nodeFor(key); if (node) { this.replaceValue(node, value); } else { this.addValue(key, value); } } public replaceValue(node: Node, value: string | null): void { if (!node.isLeaf()) { throw new ConfigKeyError('key must be a leaf node'); } if (node.isRoot()) { this._roots.set(node.name, new LexerLeafNode(null, node.name, value, this.formatter)); this.tokens.set(node.name, value); } else { this.tokens.set(node.path(), value); (node.parent as LexerInternalNode).replaceValue(node, value); } } /** * Parses the token map and returns all the root nodes. * * @returns {Node[]} The root nodes. */ public renderTrees(): void { if (this.tokens.size === 0 || this.rendered) { return; } const keys: string[] = [...this.tokens.keys()]; // Sort the keys so that we can process them in order. keys.sort(); this.processKeys(keys); this.rendered = true; } private processKeys(keys: string[]): void { for (const k of keys) { const key: string = this.formatter.normalize(k); const segments: string[] = this.formatter.split(key); const root: Node = this.rootNodeFor(segments); if (!root.isLeaf()) { this.processSegments(root as LexerInternalNode, this.tokens.get(key), segments); } } } private rootNodeFor(keyParts: string[]): Node { const rootName: string = keyParts[0]; if (this._roots.has(rootName)) { return this._roots.get(rootName); } let array: boolean = false; let root: Node; if (keyParts.length >= 2) { const nextSegment: string = keyParts[1]; if (KeyName.isArraySegment(nextSegment)) { array = true; } root = new LexerInternalNode(null, rootName, [], array, false, this.formatter); } else { root = new LexerLeafNode(null, rootName, this.tokens.get(rootName), this.formatter); } this._roots.set(rootName, root); return root; } private processSegments(root: LexerInternalNode, value: string, segments: string[]): void { let currentRoot: LexerInternalNode = root; for (let index: number = 1; index < segments.length; index++) { const segment: string = segments[index]; let node: Node; if (KeyName.isArraySegment(segment)) { node = this.processArraySegment(currentRoot, segment, value, index, segments); } else if (index >= segments.length - 1) { node = this.processLeafNode(currentRoot, segment, value); } else { node = this.processIntermediateSegment(currentRoot, segment, index, segments); } if (node.isInternal()) { currentRoot = node as LexerInternalNode; } } } /** * Processes an array segment. This method will create the necessary node to represent the array index. * * @param root {LexerInternalNode} the root node of this segment. * @param value {string} the value of the key. * @param segment {string} the segment to process. * @param index {number} the index of the segment in the array. * @param segments {string[]} the array of segments. * @return {Node} the new root node which should be used as the current root or null if no intermediate/leaf node was * created. * @private */ private processArraySegment( root: LexerInternalNode, segment: string, value: string, index: number, segments: string[], ): Node { let node: Node = root.children.find((n): boolean => n.name === segment); if (node) { if (node.isLeaf()) { throw new ConfigKeyError( `Cannot add a leaf node to another leaf node [ parent = '${root.path()}', child = '${segment}' ]`, ); } return node; } // Case where the array segment points at a value. Eg: LeafNode node = index >= segments.length - 1 ? new LexerLeafNode(root, segment, value, this.formatter) : new LexerInternalNode(root, segment, [], false, true, this.formatter); root.add(node); return node; } private processIntermediateSegment( root: LexerInternalNode, segment: string, index: number, segments: string[], ): Node { const existingNode: Node = root.children.find((n): boolean => n.name === segment); if (existingNode) { if (existingNode.isLeaf()) { throw new ConfigKeyError('Cannot add a leaf node to another leaf node'); } return existingNode; } let node: Node; // root.arrVal.0 = string|number (not handled by this case) // root.arrVal.0.scalar = string|number (handles this case) if (root.isArray()) { node = new LexerInternalNode(root, segment, [], false, true, this.formatter); } if (index < segments.length - 1) { const nextSegment: string = segments[index + 1]; if (KeyName.isArraySegment(nextSegment)) { node = new LexerInternalNode(root, segment, [], true, false, this.formatter); } } if (!node) { node = new LexerInternalNode(root, segment, [], false, false, this.formatter); } root.add(node); return node; } private processLeafNode(root: LexerInternalNode, segment: string, value: string): Node { if (root.isArray()) { throw new ConfigKeyError( `Cannot add a leaf node to an array node [ parent: '${root.path()}', child: '${segment}' ]`, ); } if (root.children.some((n): boolean => n.name === segment)) { throw new ConfigKeyError( `Cannot add a leaf node to another leaf node [ parent: '${root.path()}', child: '${segment}' ]`, ); } const node: Node = new LexerLeafNode(root, segment, value, this.formatter); root.add(node); return node; } }