• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2024 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this 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 */
15
16type TypeConstructor<T> = { new(): T };
17type FactoryConstructor<T> = (json: object) => TypeConstructor<T>;
18type JSONAny = object | number | string | boolean | undefined | null;
19
20const V2_STATE_PREFIX = '__ob_';
21const V2_PREFIX_LENGTH = V2_STATE_PREFIX.length;
22
23interface TransformOptions<T> {
24  factory?: FactoryConstructor<T>;
25  alias?: string;
26  disabled?: boolean;
27};
28
29class Meta {
30  private static proto2props: WeakMap<object, object> = new WeakMap();
31
32  public static define(proto: object, prop: string, value: any) {
33    const meta = Meta.proto2props.get(proto);
34    if (!meta) {
35      Meta.proto2props.set(proto, { [prop]: value });
36    } else {
37      meta[prop] = value;
38    }
39  }
40
41  public static get(obj: any, prop: string): any {
42    let proto = obj.__proto__;
43    while (proto) {
44      let meta = Meta.proto2props.get(proto);
45      if (meta && meta[prop]) {
46        return meta[prop];
47      }
48      proto = proto.__proto__;
49    }
50    return undefined;
51  }
52
53  public static gets(obj: any): object {
54    const ret = {};
55    let proto = obj.__proto__;
56    while (proto) {
57      let meta = Meta.proto2props.get(proto);
58      Object.assign(ret, meta);
59      proto = proto.__proto__;
60    }
61    return ret;
62  }
63
64  public static getOwn(obj: any, prop: string): any {
65    const meta = Meta.proto2props.get(obj.__proto__);
66    return meta && meta[prop];
67  }
68}
69
70function __Type__<T>(type: TypeConstructor<T> | TransformOptions<T> | string, alias?: string) {
71  const options: TransformOptions<T> = JSONCoder.getOptions(type);
72
73  if (alias) {
74    options.alias = alias;
75  }
76
77  return (target: any, prop: string) => {
78    const tar = typeof target === 'function' ? target.prototype : target;
79    Meta.define(tar, prop, options);
80  };
81}
82
83function ObservedReplacer(replacer: any) {
84  const defaultReplacer = function (key: string, value: any) {
85    return value;
86  }
87
88  const realReplacer = replacer || defaultReplacer;
89  return function (this: any, key: string, value: any) {
90    if (typeof value !== 'object' || Array.isArray(value)) {
91      return realReplacer.call(this, key, value);
92    }
93    if (value instanceof Set) {
94      return realReplacer.call(this, key, Array.from(value));
95    }
96    if (value instanceof Map) {
97      return realReplacer.call(this, key, Array.from(value.entries()));
98    }
99
100    const ret: any = {};
101    const meta = Meta.gets(value);
102    Object.keys(value).forEach(key => {
103      let saveKey = key.startsWith(V2_STATE_PREFIX) ? key.substring(V2_PREFIX_LENGTH) : key;
104      let options = meta && meta[saveKey];
105      if (options && options.disabled) {
106        return;
107      }
108      ret[(options && options.alias) || saveKey] = value[saveKey];
109    });
110    return realReplacer.call(this, key, ret);
111  }
112}
113
114/**
115 * JSONCoder
116 *
117 * The JSONCoder utility enhances the serialization and deserialization capabilities beyond
118 * the standard JSON.stringify and JSON.parse methods. While JSON.stringify serializes
119 * object properties and their values, it drops functions, class, property, and function decorators,
120 * and does not support Map, Set, or Date serialization. JSONCoder addresses these limitations,
121 * providing robust support for a wider range of JavaScript features.
122 *
123 * Main Features:
124 * - Adds support for serializing and deserializing class instances, including methods and decorators.
125 * - Supports serialization of complex data structures like Map, Set, and Date.
126 * - Provides full reconstruction of class instances through the JSONCoder.parseTo method.
127 *
128 * Usage Scenarios:
129 * - Serializing class instances to JSON for network transmission or storage.
130 * - Deserializing JSON data back into fully functional class instances, preserving methods and decorators.
131 * - Converting JSON data received from network or database into state management observable view models (e.g., @ObservedV2 class objects).
132 *
133 * The name 'JSONCoder' is derived from the 'JSON stringify/parse', reflecting its purpose to enhance JSON serialization and deserialization for classes.
134 *
135 */
136class JSONCoder {
137  /**
138   * Serializes the given object into a string. This string includes additional meta info
139   * allowing `stringify` to fully reconstruct the original object, including its class
140   * type and properties.
141   *
142   * @template T - The type of the object being serialized.
143   * @param { T } value - The object to serialize.
144   * @param { (this: JSONAny, key: string, value: JSONAny) => JSONAny } [replacer] - A function that alters the behavior when stringify
145   * @param { string | number } [space] - For format
146   * @returns { string } The serialized string representation of the object.
147   */
148  public static stringify<T>(value: T, replacer?: (this: JSONAny, key: string, value: JSONAny) => JSONAny,
149    space?: string | number): string {
150    return JSON.stringify(value, ObservedReplacer(replacer), space);
151  }
152
153  /**
154   * Parses a JSON string or object and applies the nested key-values to a class object.
155   * The main usage scenario is to convert JSON data received from a network or database
156   * to a state management observable view model.
157   *
158   * @template T - The type of the object being parsed.
159   * @param { TypeConstructor<T> | TransformOptions<T> } type - The class prototype or constructor function that has no parameters.
160   * @param { object | string } source - The JSON string or JSON object.
161   * @returns { T | T[] } The parsed object of type T or T[].
162   */
163  public static parse<T extends object>(type: TypeConstructor<T> | TransformOptions<T>, source: object | string): T | T[] {
164    const json = typeof source === 'string' ? JSON.parse(source) : source;
165    const options: TransformOptions<T> = JSONCoder.getOptions(type);
166    return Array.isArray(json) ?
167      JSONCoder.parseIntoArray([], json, options) :
168      JSONCoder.parseInto(JSONCoder.newItem(json, options), json);
169  }
170
171  /**
172   * Deserializes a string produced by `parseTo` back into the original object,
173   * fully reconstructing its class type and properties.
174   *
175   * @template T - The original object being parsed.
176   * @param { T | T[] } type - The original object.
177   * @param { object | string } source - The JSON string or JSON object.
178   * @param { TypeConstructor<T> | TransformOptions<T> } [type] - The class prototype or constructor function that has no parameters.
179   * @returns { T | T[] } The parsed object of type T or T[].
180   */
181  public static parseTo<T extends object>(target: T | T[], source: object | string,
182    type?: TypeConstructor<T> | TransformOptions<T>): T | T[] {
183    const json = typeof source === 'string' ? JSON.parse(source) : source;
184    const t1 = Array.isArray(json);
185    const t2 = Array.isArray(target);
186    const options: TransformOptions<T> = JSONCoder.getOptions(type);
187    if (t1 && t2) {
188      JSONCoder.parseIntoArray(target, json, options);
189    } else if (!t1 && !t2) {
190      JSONCoder.parseInto(target, json);
191    } else {
192      throw new Error(`The type of target '${t2}' mismatches the type of source '${t1}'`);
193    }
194    return target;
195  }
196
197  /**
198   * Get the type options from the object creator.
199   *
200   * @template T - The object being parsed.
201   * @param { TypeConstructor<T> | TransformOptions<T> | string } [type] - The type info of the object creator.
202   * @returns { TransformOptions<T> } The options of the type info.
203   */
204  public static getOptions<T>(type?: TypeConstructor<T> | TransformOptions<T> | string): TransformOptions<T> {
205    const paramType = typeof type;
206    const options: TransformOptions<T> = {};
207    if (paramType === 'object') {
208      Object.assign(options, type);
209    } else if (paramType === 'function') {
210      options.factory = (_: object) => type as TypeConstructor<T>;
211    } else if (paramType === 'string') {
212      options.alias = type as string;
213    }
214    return options;
215  }
216
217  private static getAlias2Prop(meta: any, target: any): Map<string, string> {
218    const ret = new Map<string, string>();
219    Object.keys(meta).forEach(prop => {
220      const options = meta[prop];
221      ret.set(options.alias || prop, prop);
222    });
223    return ret;
224  }
225
226  private static parseInto(target: any, source: any): any {
227    if (typeof source !== 'object') {
228      throw new Error(`The type of target '${typeof target}' mismatches the type of source '${typeof source}'`);
229    }
230
231    const meta = Meta.gets(target);
232    const alias2prop = JSONCoder.getAlias2Prop(meta, target);
233
234    Object.keys(source).forEach((key: string) => {
235      const prop = alias2prop.get(key) || key;
236      const options = meta && meta[prop];
237      if (options && options.disabled) {
238        return;
239      }
240      JSONCoder.parseItemInto(target, prop, source, options);
241    });
242
243    return target;
244  }
245
246  private static parseItemInto(target: any, targetKey: string, source: any, options: any) {
247    if (source === null || source === undefined) {
248      return;
249    }
250
251    let tarType = typeof target[targetKey];
252    if (tarType === 'function') {
253      return;
254    }
255
256    const sourceKey = options?.alias || targetKey;
257    // Handling invalid values
258    const value = JSONCoder.getTargetValue(source[sourceKey], options);
259    if (value === undefined || value === null) {
260      if (tarType === 'object') {
261        if (target[targetKey] instanceof Map || target[targetKey] instanceof Set) {
262          target[targetKey].clear();
263        } else if (Array.isArray(target[targetKey])) {
264          target[targetKey].splice(0, target[targetKey].length);
265        } else if (options && options.factory) {
266          // if options.factory exists, can be assigned to undefined or null
267          target[targetKey] = value;
268        }
269      }
270      // other scene ignore all
271      return;
272    }
273
274    // value is array, it maybe array or map or set
275    if (Array.isArray(value)) {
276      target[targetKey] = JSONCoder.parseIntoArray(target[targetKey], value, options);
277      return;
278    }
279
280    // if target[targetKey] invalid, then attempt create
281    if (target[targetKey] === null || target[targetKey] === undefined) {
282      target[targetKey] = JSONCoder.newItem(value, options);
283      tarType = typeof target[targetKey];
284    }
285
286    if (typeof value !== 'object') {
287      // value is Primitive Type
288      if (target[targetKey] instanceof Date) {
289        target[targetKey] = new Date(value);
290      } else if (tarType === 'string') {
291        target[targetKey] = value.toString();
292      } else if (tarType === typeof value) {
293        target[targetKey] = value;
294      } else if (target[targetKey] !== undefined) {
295        throw new Error(`The type of target '${tarType}' mismatches the type of source '${typeof value}'`);
296      }
297      return;
298    }
299
300    // value is object, target[targetKey] is undefined or null
301    if (target[targetKey] === null) {
302      throw new Error(`Miss @Type in object defined, the property name is ${targetKey}`);
303    } else if (target[targetKey] === undefined) {
304      // ignore target[targetKey] undefined
305      return;
306    }
307    this.parseInto(target[targetKey], value);
308  }
309
310  private static newItem(json: any, options: any): any {
311    const type = options?.factory(json);
312    return type && new type();
313  }
314
315  private static getTargetValue(value: any, options: any) {
316    // future can convert the value to different type or value
317    return value;
318  }
319
320  private static parseIntoArray(target: any, source: any, options: TransformOptions<any>): any {
321    if (typeof target !== 'object') {
322      throw new Error(`The type of target '${typeof target}' mismatches the type of source '${typeof source}'`);
323    }
324    // here, source maybe a array or map or set
325    if (target instanceof Map) {
326      target.clear();
327      for (let i = 0; i < source.length; ++i) {
328        // If target is a map, item must be an array. Otherwise, ignore it
329        const item = source[i];
330        if (!Array.isArray(item) || item.length < 2 || typeof item[0] !== 'string') {
331          continue;
332        }
333        target.set(item[0], typeof item[1] !== 'object' ? item[1] : JSONCoder.parse(options, item[1]));
334      }
335      return target;
336    }
337
338    if (target instanceof Set) {
339      target.clear();
340      for (let i = 0; i < source.length; ++i) {
341        const item = source[i];
342        target.add(typeof item !== 'object' ? item : JSONCoder.parse(options, item));
343      }
344      return target;
345    }
346
347    target.length = source.length;
348    for (let i = 0; i < source.length; ++i) {
349      const item = source[i];
350      if (typeof item !== 'object') {
351        target[i] = item;
352        continue;
353      }
354
355      if (i === 0) {
356        if (!options?.factory) {
357          target.length = 0;
358          throw new Error(`Miss @Type in array defined`);
359        }
360      }
361
362      target[i] = Array.isArray(item) ?
363        JSONCoder.parseIntoArray(target[i] || [], item, options) :
364        JSONCoder.parseInto(target[i] || JSONCoder.newItem(item, options), item);
365    }
366    return target;
367  }
368}
369