blob: 90dd45df237a11cb83e26d99459974a820eb340f [file] [log] [blame]
// Copyright (C) 2021 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use size file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {isString} from '../base/object_utils';
import {exists} from '../base/utils';
export type Key = string | number;
export interface ArgNode<T> {
key: Key;
value?: T;
children?: ArgNode<T>[];
}
// Arranges a flat list of arg-like objects (objects with a string "key" value
// indicating their path) into a nested tree.
//
// This process is relatively forgiving as it allows nodes with both values and
// child nodes as well as children with mixed key types in the same node.
//
// When duplicate nodes exist, the latest one is picked.
//
// If you want to convert args to a POJO, try convertArgsToObject().
//
// Key should be a path seperated by periods (.) or indexes specified using a
// number inside square brackets.
// e.g. foo.bar[0].x
//
// See unit tests for examples.
export function convertArgsToTree<T extends {key: string}>(
input: T[],
): ArgNode<T>[] {
const result: ArgNode<T>[] = [];
for (const arg of input) {
const {key} = arg;
const nestedKey = getNestedKey(key);
insert(result, nestedKey, key, arg);
}
return result;
}
function getNestedKey(key: string): Key[] {
const result: Key[] = [];
let match;
const re = /([^\.\[\]]+)|\[(\d+)\]/g;
while ((match = re.exec(key)) !== null) {
result.push(match[2] ? parseInt(match[2]) : match[1]);
}
return result;
}
function insert<T>(
args: ArgNode<T>[],
keys: Key[],
path: string,
value: T,
): void {
const currentKey = keys.shift()!;
let node = args.find((x) => x.key === currentKey);
if (!node) {
node = {key: currentKey};
args.push(node);
}
if (keys.length > 0) {
if (node.children === undefined) {
node.children = [];
}
insert(node.children, keys, path, value);
} else {
node.value = value;
}
}
type ArgLike<T> = {
key: string;
value: T;
};
type ObjectType<T> = T | ObjectType<T>[] | {[key: string]: ObjectType<T>};
// Converts a list of argument-like objects (i.e. objects with key and value
// fields) to a POJO.
//
// This function cannot handle cases where nodes contain mixed node types (i.e.
// both number and string types) as nodes cannot be both an object and an array,
// and will throw when this situation arises.
//
// Key should be a path seperated by periods (.) or indexes specified using a
// number inside square brackets.
// e.g. foo.bar[0].x
//
// See unit tests for examples.
export function convertArgsToObject<A extends ArgLike<T>, T>(
input: A[],
): ObjectType<T> {
const nested = convertArgsToTree(input);
return parseNodes(nested);
}
function parseNodes<A extends ArgLike<T>, T>(
nodes: ArgNode<A>[],
): ObjectType<T> {
if (nodes.every(({key}) => isString(key))) {
const dict: ObjectType<T> = {};
for (const node of nodes) {
if (node.key in dict) {
throw new Error(`Duplicate key ${node.key}`);
}
dict[node.key] = parseNode(node);
}
return dict;
} else if (nodes.every(({key}) => typeof key === 'number')) {
const array: ObjectType<T>[] = [];
for (const node of nodes) {
const index = node.key as number;
if (index in array) {
throw new Error(`Duplicate array index ${index}`);
}
array[index] = parseNode(node);
}
return array;
} else {
throw new Error('Invalid mix of node key types');
}
}
function parseNode<A extends ArgLike<T>, T>({
value,
children,
}: ArgNode<A>): ObjectType<T> {
if (exists(value) && !exists(children)) {
return value.value;
} else if (!exists(value) && exists(children)) {
return parseNodes(children);
} else {
throw new Error('Invalid node type');
}
}