@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
287 lines • 10.8 kB
JavaScript
// SPDX-License-Identifier: Apache-2.0
import { ConfigKeyFormatter } from '../config-key-formatter.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 {
tokens;
formatter;
_roots = new Map();
flatMapper;
_rendered = false;
constructor(tokens, formatter = ConfigKeyFormatter.instance()) {
this.tokens = tokens;
this.formatter = formatter;
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);
}
get rendered() {
return this._rendered;
}
set rendered(rendered) {
this._rendered = rendered;
}
get rootNodes() {
if (!this.rendered) {
this.renderTrees();
}
return [...this._roots.values()];
}
get tree() {
if (!this.rendered) {
this.renderTrees();
}
return this._roots;
}
nodeFor(key) {
if (!key) {
throw new IllegalArgumentError('key must not be null or undefined');
}
const segments = this.formatter.split(key);
if (segments.length === 0 || segments[0].trim().length === 0) {
throw new IllegalArgumentError('key must not be empty');
}
let currentNode = this.tree.get(segments[0]);
if (!currentNode) {
return null;
}
for (let index = 1; index < segments.length; index++) {
const segment = segments[index];
if (currentNode.isLeaf()) {
return null;
}
const inode = currentNode;
const nextNode = inode.children.find((n) => n.name === segment);
if (!nextNode) {
return null;
}
currentNode = nextNode;
}
return currentNode;
}
addValue(key, value) {
if (!key) {
throw new IllegalArgumentError('key must not be null or undefined');
}
const segments = this.formatter.split(this.formatter.normalize(key));
const rootNode = this._roots.has(segments[0]) ? this._roots.get(segments[0]) : this.rootNodeFor(segments);
this.processSegments(rootNode, value, segments);
this.tokens.set(key, value);
}
addOrReplaceObject(key, value) {
if (!key) {
throw new IllegalArgumentError('key must not be null or undefined');
}
const normalizedKey = this.formatter.normalize(key);
this.addOrReplaceValue(normalizedKey, null);
if (!value) {
return;
}
const flatMap = this.flatMapper.flatten(value);
for (const [k, v] of flatMap.entries()) {
this.addOrReplaceValue(this.formatter.join(normalizedKey, k), v);
}
}
addOrReplaceArray(key, values) {
if (!key) {
throw new IllegalArgumentError('key must not be null or undefined');
}
const normalizedKey = this.formatter.normalize(key);
const node = this.nodeFor(normalizedKey);
if (node && !node.isArray()) {
if (node.isRoot()) {
this._roots.delete(node.name);
this.tokens.delete(node.name);
}
else {
const parent = node.parent;
parent.remove(node);
this.tokens.delete(node.path());
}
}
if (!values) {
return;
}
for (const [index, value] of values.entries()) {
this.addOrReplaceArrayElement(normalizedKey, index, value);
}
}
addOrReplaceArrayElement(normalizedKey, index, value) {
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 = this.flatMapper.flatten(value);
for (const [k, value_] of flatMap.entries()) {
this.addOrReplaceValue(this.formatter.join(normalizedKey, index.toString(), k), value_);
}
}
}
addOrReplaceValue(key, value) {
if (!key) {
throw new IllegalArgumentError('key must not be null or undefined');
}
const node = this.nodeFor(key);
if (node) {
this.replaceValue(node, value);
}
else {
this.addValue(key, value);
}
}
replaceValue(node, value) {
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.replaceValue(node, value);
}
}
/**
* Parses the token map and returns all the root nodes.
*
* @returns {Node[]} The root nodes.
*/
renderTrees() {
if (this.tokens.size === 0 || this.rendered) {
return;
}
const keys = [...this.tokens.keys()];
// Sort the keys so that we can process them in order.
keys.sort();
this.processKeys(keys);
this.rendered = true;
}
processKeys(keys) {
for (const k of keys) {
const key = this.formatter.normalize(k);
const segments = this.formatter.split(key);
const root = this.rootNodeFor(segments);
if (!root.isLeaf()) {
this.processSegments(root, this.tokens.get(key), segments);
}
}
}
rootNodeFor(keyParts) {
const rootName = keyParts[0];
if (this._roots.has(rootName)) {
return this._roots.get(rootName);
}
let array = false;
let root;
if (keyParts.length >= 2) {
const nextSegment = 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;
}
processSegments(root, value, segments) {
let currentRoot = root;
for (let index = 1; index < segments.length; index++) {
const segment = segments[index];
let 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;
}
}
}
/**
* 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
*/
processArraySegment(root, segment, value, index, segments) {
let node = root.children.find((n) => 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;
}
processIntermediateSegment(root, segment, index, segments) {
const existingNode = root.children.find((n) => n.name === segment);
if (existingNode) {
if (existingNode.isLeaf()) {
throw new ConfigKeyError('Cannot add a leaf node to another leaf node');
}
return existingNode;
}
let 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 = 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;
}
processLeafNode(root, segment, value) {
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) => n.name === segment)) {
throw new ConfigKeyError(`Cannot add a leaf node to another leaf node [ parent: '${root.path()}', child: '${segment}' ]`);
}
const node = new LexerLeafNode(root, segment, value, this.formatter);
root.add(node);
return node;
}
}
//# sourceMappingURL=lexer.js.map