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