xref: /aosp_15_r20/external/perfetto/ui/src/components/details/slice_args_parser.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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