1// Copyright (C) 2021 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use size file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {isString} from '../../base/object_utils'; 16import {exists} from '../../base/utils'; 17 18export type Key = string | number; 19 20export interface ArgNode<T> { 21 key: Key; 22 value?: T; 23 children?: ArgNode<T>[]; 24} 25 26// Arranges a flat list of arg-like objects (objects with a string "key" value 27// indicating their path) into a nested tree. 28// 29// This process is relatively forgiving as it allows nodes with both values and 30// child nodes as well as children with mixed key types in the same node. 31// 32// When duplicate nodes exist, the latest one is picked. 33// 34// If you want to convert args to a POJO, try convertArgsToObject(). 35// 36// Key should be a path seperated by periods (.) or indexes specified using a 37// number inside square brackets. 38// e.g. foo.bar[0].x 39// 40// See unit tests for examples. 41export function convertArgsToTree<T extends {key: string}>( 42 input: T[], 43): ArgNode<T>[] { 44 const result: ArgNode<T>[] = []; 45 for (const arg of input) { 46 const {key} = arg; 47 const nestedKey = getNestedKey(key); 48 insert(result, nestedKey, key, arg); 49 } 50 return result; 51} 52 53function getNestedKey(key: string): Key[] { 54 const result: Key[] = []; 55 let match; 56 const re = /([^\.\[\]]+)|\[(\d+)\]/g; 57 while ((match = re.exec(key)) !== null) { 58 result.push(match[2] ? parseInt(match[2]) : match[1]); 59 } 60 return result; 61} 62 63function insert<T>( 64 args: ArgNode<T>[], 65 keys: Key[], 66 path: string, 67 value: T, 68): void { 69 const currentKey = keys.shift()!; 70 let node = args.find((x) => x.key === currentKey); 71 if (!node) { 72 node = {key: currentKey}; 73 args.push(node); 74 } 75 if (keys.length > 0) { 76 if (node.children === undefined) { 77 node.children = []; 78 } 79 insert(node.children, keys, path, value); 80 } else { 81 node.value = value; 82 } 83} 84 85type ArgLike<T> = { 86 key: string; 87 value: T; 88}; 89type ObjectType<T> = T | ObjectType<T>[] | {[key: string]: ObjectType<T>}; 90 91// Converts a list of argument-like objects (i.e. objects with key and value 92// fields) to a POJO. 93// 94// This function cannot handle cases where nodes contain mixed node types (i.e. 95// both number and string types) as nodes cannot be both an object and an array, 96// and will throw when this situation arises. 97// 98// Key should be a path seperated by periods (.) or indexes specified using a 99// number inside square brackets. 100// e.g. foo.bar[0].x 101// 102// See unit tests for examples. 103export function convertArgsToObject<A extends ArgLike<T>, T>( 104 input: A[], 105): ObjectType<T> { 106 const nested = convertArgsToTree(input); 107 return parseNodes(nested); 108} 109 110function parseNodes<A extends ArgLike<T>, T>( 111 nodes: ArgNode<A>[], 112): ObjectType<T> { 113 if (nodes.every(({key}) => isString(key))) { 114 const dict: ObjectType<T> = {}; 115 for (const node of nodes) { 116 if (node.key in dict) { 117 throw new Error(`Duplicate key ${node.key}`); 118 } 119 dict[node.key] = parseNode(node); 120 } 121 return dict; 122 } else if (nodes.every(({key}) => typeof key === 'number')) { 123 const array: ObjectType<T>[] = []; 124 for (const node of nodes) { 125 const index = node.key as number; 126 if (index in array) { 127 throw new Error(`Duplicate array index ${index}`); 128 } 129 array[index] = parseNode(node); 130 } 131 return array; 132 } else { 133 throw new Error('Invalid mix of node key types'); 134 } 135} 136 137function parseNode<A extends ArgLike<T>, T>({ 138 value, 139 children, 140}: ArgNode<A>): ObjectType<T> { 141 if (exists(value) && !exists(children)) { 142 return value.value; 143 } else if (!exists(value) && exists(children)) { 144 return parseNodes(children); 145 } else { 146 throw new Error('Invalid node type'); 147 } 148} 149