| // 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'); |
| } |
| } |