1/*
2 * Copyright (C) 2023 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {assertDefined, assertTrue} from './assert_utils';
18
19class Key {
20  constructor(public key: string, public index?: number) {}
21
22  isArrayKey(): boolean {
23    return this.index !== undefined;
24  }
25}
26
27export class ObjectUtils {
28  static readonly ARRAY_KEY_REGEX = new RegExp('(.+)\\[(\\d+)\\]');
29
30  static getProperty(obj: object, path: string): any {
31    const keys = ObjectUtils.parseKeys(path);
32    keys.forEach((key) => {
33      if (obj === undefined) {
34        return;
35      }
36
37      if (key.isArrayKey()) {
38        if ((obj as any)[key.key] === undefined) {
39          return;
40        }
41        assertTrue(
42          Array.isArray((obj as any)[key.key]),
43          () => 'Expected to be array',
44        );
45        obj = (obj as any)[key.key][assertDefined(key.index)];
46      } else {
47        obj = (obj as any)[key.key];
48      }
49    });
50    return obj;
51  }
52
53  static setProperty(obj: object, path: string, value: any) {
54    const keys = ObjectUtils.parseKeys(path);
55
56    keys.slice(0, -1).forEach((key) => {
57      if (key.isArrayKey()) {
58        ObjectUtils.initializePropertyArrayIfNeeded(obj, key);
59        obj = (obj as any)[key.key][assertDefined(key.index)];
60      } else {
61        ObjectUtils.initializePropertyIfNeeded(obj, key.key);
62        obj = (obj as any)[key.key];
63      }
64    });
65
66    const lastKey = assertDefined(keys.at(-1));
67    if (lastKey.isArrayKey()) {
68      ObjectUtils.initializePropertyArrayIfNeeded(obj, lastKey);
69      (obj as any)[lastKey.key][assertDefined(lastKey.index)] = value;
70    } else {
71      (obj as any)[lastKey.key] = value;
72    }
73  }
74
75  private static parseKeys(path: string): Key[] {
76    return path.split('.').map((rawKey) => {
77      const match = ObjectUtils.ARRAY_KEY_REGEX.exec(rawKey);
78      if (match) {
79        return new Key(match[1], Number(match[2]));
80      }
81      return new Key(rawKey);
82    });
83  }
84
85  private static initializePropertyIfNeeded(obj: object, key: string) {
86    if ((obj as any)[key] === undefined) {
87      (obj as any)[key] = {};
88    }
89    assertTrue(
90      typeof (obj as any)[key] === 'object',
91      () => 'Expected to be object',
92    );
93  }
94
95  private static initializePropertyArrayIfNeeded(obj: object, key: Key) {
96    if ((obj as any)[key.key] === undefined) {
97      (obj as any)[key.key] = [];
98    }
99    if ((obj as any)[key.key][assertDefined(key.index)] === undefined) {
100      (obj as any)[key.key][assertDefined(key.index)] = {};
101    }
102    assertTrue(
103      Array.isArray((obj as any)[key.key]),
104      () => 'Expected to be array',
105    );
106  }
107}
108