• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Haixing-Hu / js-common-decorator / 416011cd-471d-4350-a9d5-aafb569cdeb0

22 Oct 2024 11:32AM UTC coverage: 83.06% (+0.1%) from 82.915%
416011cd-471d-4350-a9d5-aafb569cdeb0

push

circleci

Haixing-Hu
refactor: refactor createPage()

469 of 577 branches covered (81.28%)

Branch coverage included in aggregate %.

23 of 24 new or added lines in 4 files covered. (95.83%)

5 existing lines in 1 file now uncovered.

644 of 763 relevant lines covered (84.4%)

172.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.31
/src/impl/model/assign-impl.js
1
////////////////////////////////////////////////////////////////////////////////
2
//
3
//    Copyright (c) 2022 - 2023.
4
//    Haixing Hu, Qubit Co. Ltd.
5
//
6
//    All rights reserved.
7
//
8
////////////////////////////////////////////////////////////////////////////////
9
import clone from '@haixing_hu/clone';
10
import { isBuiltInClass } from '@haixing_hu/type-detect';
11
import { isUndefinedOrNull } from '@haixing_hu/common-util';
12
import {
13
  getClassMetadata,
14
  getFieldMetadata,
15
  getDefaultInstance,
16
  hasPrototypeFunction,
17
} from '../utils';
18
import {
19
  KEY_CLASS_CATEGORY,
20
  KEY_FIELD_ELEMENT_TYPE,
21
  KEY_FIELD_TYPE,
22
} from '../metadata-keys';
23
import classMetadataCache from '../class-metadata-cache';
24
import DefaultOptions from '../../default-options';
25
import defaultNormalizer from '../../default-normalizer';
26
import ofValueImpl from '../enum/of-value-impl';
27
import getSourceField from './get-source-field';
28
import getExistFieldWithDifferentNamingStyle from './get-exist-field-with-different-naming-style';
29
import CLONE_OPTIONS from './clone-options';
30
import getTargetFieldElementType from './get-target-field-element-type';
31
import getTargetFieldType from './get-target-field-type';
32

33
const Impl = {
35✔
34
  /**
35
   * Gets the names of all fields of the default instance of target class.
36
   *
37
   * If the parent class of the target class has an `assign()` method, the
38
   * `assign()` method of the parent class is called firstly, and then the
39
   * fields owned by the parent class are excluded.
40
   *
41
   * @param {object} target
42
   *     The target object to be assigned to.
43
   * @param {object} source
44
   *     The source object to assigned from, which can be `null` or `undefined`,
45
   *     or a plain old JavaScript object without class information.
46
   * @param {function} type
47
   *     The type of the target object, i.e., the constructor of the class of
48
   *     the target object.
49
   * @param {object} options
50
   *     the additional options for the assignment.
51
   * @returns {string[]}
52
   *     The names of all fields of the default instance of the target class,
53
   *     excluding the fields owned by the parent class of the target class if
54
   *     the parent class has an `assign()` method.
55
   * @author Haixing Hu
56
   * @private
57
   */
58
  getAllFields(target, source, { type, options }) {
59
    // Get all names of fields of the default object of the `type`
60
    const fields = Object.keys(target);
316✔
61
    // call the `assign()` method in the parent classes of the target class
62
    const parentType = Object.getPrototypeOf(type);
316✔
63
    if (hasPrototypeFunction(parentType, 'assign')) {
316✔
64
      // If the parent class or its ancestor classes has an `assign()` method,
65
      // call the `assign()` method of the parent class.
66
      parentType.prototype.assign.call(target, source, options);
5✔
67
      // then exclude fields owned by the parent class
68
      const parentInstance = getDefaultInstance(parentType);
5✔
69
      const parentFields = Object.keys(parentInstance);
5✔
70
      return fields.filter((f) => !parentFields.includes(f));
20✔
71
    } else {
72
      return fields;
311✔
73
    }
74
  },
75

76
  /**
77
   * Copies all configurable, enumerable, and non-read-only properties of the
78
   * source object to the target object without naming conversion.
79
   *
80
   * @param {object} target
81
   *     The target object.
82
   * @param {object} source
83
   *     The source object, which may be `null` or `undefined`.
84
   * @author Haixing Hu
85
   * @private
86
   */
87
  copyAllPropertiesWithoutNamingConversion(target, source) {
88
    if (isUndefinedOrNull(source)) {
4!
UNCOV
89
      return;
×
90
    }
91
    Object.keys(target).forEach((key) => {
4✔
92
      if (Object.prototype.hasOwnProperty.call(source, key)) {
24!
93
        target[key] = clone(source[key], CLONE_OPTIONS);
24✔
94
      }
95
    });
96
  },
97

98
  /**
99
   * Clones an enumeration value.
100
   *
101
   * @param {string|any} source
102
   *    The source value being cloned.
103
   * @param {string} targetPath
104
   *    The path of the target value in the property tree of the original root.
105
   * @param {string} sourcePath
106
   *     The path of the source object in the property tree of the original root
107
   *     object.
108
   * @param {Function} type
109
   *    The constructor of the enumeration class of the target value. Note that
110
   *    this enumeration class **must** be decorated by `@Enum`.
111
   * @return {type|*|null}
112
   *    The target value deeply cloned from the source value, whose type is
113
   *    exactly the same as the specified type.
114
   * @author Haixing Hu
115
   * @private
116
   */
117
  cloneEnumValue(source, { sourcePath, type }) {
118
    if (source === null || source === undefined) {
155✔
119
      return null;
1✔
120
    } else if (source instanceof type) {
154✔
121
      return source;
26✔
122
    } else if (typeof source === 'string') {
128!
123
      // the blank string is treated as null enumerator
124
      if (source.trim().length === 0) {
128✔
125
        return null;
7✔
126
      }
127
      // convert the string representation to the enumerator
128
      const e = ofValueImpl(type, source);
121✔
129
      if (e === undefined) {
121✔
130
        throw new RangeError(`The value of property '${sourcePath}' is not an enumerator of ${type.name}: ${source}`);
3✔
131
      }
132
      return e;
118✔
133
    } else {
UNCOV
134
      throw new RangeError(`The value of property '${sourcePath}' is not an enumerator of ${type.name}: ${source}`);
×
135
    }
136
  },
137

138
  /**
139
   * Clones an object of a known type.
140
   *
141
   * @param {object} source
142
   *     The source object being cloned.
143
   * @param {string} targetPath
144
   *     The path of the target object in the property tree of the original root
145
   *     object.
146
   * @param {string} sourcePath
147
   *     The path of the source object in the property tree of the original root
148
   *     object.
149
   * @param {function} type
150
   *     The constructor of the class of the cloned target object.
151
   * @param {object} defaultInstance
152
   *     A default instance in the class of the target object. If a certain
153
   *     attribute value of the target object does not exist in the source
154
   *     object or is null, the corresponding attribute value of this default
155
   *     instance is used to assign the corresponding attribute of the target
156
   *     object. Note that this argument may be `null` or `undefined`.
157
   * @param {object} options
158
   *     the additional options for the assignment.
159
   * @returns
160
   *     A target object deeply cloned from the source object, whose type is
161
   *     exactly the same as the specified type.
162
   * @author Haixing Hu
163
   * @private
164
   */
165
  cloneWithTypeInfo(source, { targetPath, sourcePath, type, defaultInstance, options }) {
166
    if (isUndefinedOrNull(source)) {
284!
167
      // If the source object is nullish, directly deep clone the default object
168
      // without naming conversion
UNCOV
169
      return clone(defaultInstance, CLONE_OPTIONS);
×
170
    }
171
    if (isBuiltInClass(type)) {
284✔
172
      // For JS built-in standard types, directly deep clone the source object
173
      // without naming conversion
174
      return clone(source, CLONE_OPTIONS);
42✔
175
    }
176
    const category = getClassMetadata(type, KEY_CLASS_CATEGORY);
242✔
177
    switch (category) {
242✔
178
      case 'enum':
179
        return this.cloneEnumValue(source, {
152✔
180
          targetPath,
181
          sourcePath,
182
          type,
183
          defaultInstance,
184
        });
185
      case 'model':
186
      default: {
187
        // Construct a target value based on the source type
188
        /* eslint-disable-next-line new-cap */
189
        const target = new type();
90✔
190
        // Recursively assign each attribute value of `source` to `target`
191
        return this.doAssign(target, source, {
90✔
192
          targetPath,
193
          sourcePath,
194
          type,
195
          defaultInstance,
196
          options,
197
        });
198
      }
199
    }
200
  },
201

202
  /**
203
   * Clones an array whose element types are known.
204
   *
205
   * @param {object} sourceArray
206
   *     The source array being cloned.
207
   * @param {string} targetPath
208
   *     The path of the target array in the property tree of the original root
209
   *     object.
210
   * @param {string} sourcePath
211
   *     The path of the source object in the property tree of the original root
212
   *     object.
213
   * @param {function} elementType
214
   *     The constructor of the class of the target array elements to be cloned.
215
   * @param {object} defaultArray
216
   *     The default array, which may be `undefined` or `null`.
217
   * @param {object} options
218
   *     the additional options for the assignment.
219
   * @returns
220
   *     A target array deeply cloned from the source array, whose element type
221
   *     is exactly the same as the specified element type.
222
   * @author Haixing Hu
223
   * @private
224
   */
225
  cloneArrayWithElementTypeInfo(sourceArray, { targetPath, sourcePath, elementType, defaultArray, options }) {
226
    if (!Array.isArray(sourceArray)) {
18!
227
      console.error(`The value of the property '${sourcePath}' should be an array:`, sourceArray);
×
228
      // clone the default array without naming conversion
UNCOV
229
      return clone(defaultArray, CLONE_OPTIONS);
×
230
    }
231
    if (isBuiltInClass(elementType)) {
18✔
232
      // For JS built-in standard element types, directly deep clone the array
233
      // without naming conversion
234
      return clone(sourceArray, CLONE_OPTIONS);
4✔
235
    }
236
    const elementTypeCategory = getClassMetadata(elementType, KEY_CLASS_CATEGORY);
14✔
237
    switch (elementTypeCategory) {
14✔
238
      case 'enum':
239
        // For enumeration classes, because enumeration types are always
240
        // expressed in string form, we only need to copy the source string array.
241
        return sourceArray.map((sourceElement, index) => this.cloneEnumValue(sourceElement, {
3✔
242
          targetPath: `${targetPath}[${index}]`,
243
          sourcePath: `${sourcePath}[${index}]`,
244
          type: elementType,
245
          defaultInstance: (defaultArray ? defaultArray[index] : undefined),
3!
246
        }));
247
      case 'model':
248
      default: {
249
        // For non-enumerated element classes, create a target array of the same
250
        // element type, and assign the values in the source array to the target
251
        // array one by one.
252
        const defaultElement = getDefaultInstance(elementType);
13✔
253
        return sourceArray.map((sourceElement, index) => {
13✔
254
          if (isUndefinedOrNull(sourceElement)) {
34✔
255
            // If the source element is nullish, directly returns it
256
            return sourceElement;
7✔
257
          }
258
          // Create an element from the target array
259
          /* eslint-disable-next-line new-cap */
260
          const targetElement = new elementType();
27✔
261
          // Recursively assign each attribute value of `sourceElement` to `targetElement`.
262
          // Use `defaultElement` as default instance
263
          const defaultInstance = ((defaultArray && defaultArray[index]) ? defaultArray[index] : defaultElement);
27✔
264
          // Recursively assign each element of `sourceArray` to `targetArray`
265
          this.doAssign(targetElement, sourceElement, {
27✔
266
            targetPath: `${targetPath}[${index}]`,
267
            sourcePath: `${sourcePath}[${index}]`,
268
            type: elementType,
269
            defaultInstance,
270
            options,
271
          });
272
          return targetElement;
27✔
273
        });
274
      }
275
    }
276
  },
277

278
  /**
279
   * Clones an object of an unknown type.
280
   *
281
   * @param {object} source
282
   *     The source object being cloned.
283
   * @param {string} targetPath
284
   *     The path of the target object in the property tree of the original root
285
   *     object.
286
   * @param {string} sourcePath
287
   *     The path of the source object in the property tree of the original root
288
   *     object.
289
   * @param {object} defaultInstance
290
   *     A default instance in the class of the target object. If a certain
291
   *     attribute value of the target object does not exist in the source
292
   *     object or is null, the corresponding attribute value of this default
293
   *     instance is used to assign the corresponding attribute of the target
294
   *     object. Note that this argument may be `null` or `undefined`.
295
   * @param {object} options
296
   *     the additional options for the assignment.
297
   * @returns
298
   *     A target object deeply cloned from the source object, whose type is
299
   *     exactly the same as the default object.
300
   * @author Haixing Hu
301
   * @private
302
   */
303
  cloneWithoutTypeInfo(source, { targetPath, sourcePath, defaultInstance, options }) {
304
    // If the default instance is nullish, directly deep clone the source object
305
    if (isUndefinedOrNull(defaultInstance)) {
73!
306
      // clone the source with naming conversion options
UNCOV
307
      return clone(source, {
×
308
        ...CLONE_OPTIONS,
309
        convertNaming: options.convertNaming,
310
        sourceNamingStyle: options.sourceNamingStyle,
311
        targetNamingStyle: options.targetNamingStyle,
312
      });
313
    }
314
    // If the property value of the target object is an object, we must create
315
    // an object of the same prototype and copy the property values of the
316
    // source object
317
    const Class = Object.getPrototypeOf(defaultInstance).constructor;
73✔
318
    const target = new Class();
73✔
319
    // Recursively assign each attribute value of `source` to `target`
320
    return this.doAssign(target, source, {
73✔
321
      targetPath,
322
      sourcePath,
323
      type: Class,
324
      defaultInstance,
325
      options,
326
    });
327
  },
328

329
  /**
330
   * Clones an array whose element type is unknown.
331
   *
332
   * @param {array} sourceArray
333
   *     The source array being cloned.
334
   * @param {string} targetPath
335
   *     The path of the target array in the property tree of the original root
336
   *     object.
337
   * @param {string} sourcePath
338
   *     The path of the source object in the property tree of the original root
339
   *     object.
340
   * @param {array} defaultArray
341
   *     The default array, which may be `undefined` or `null`.
342
   * @param {object} options
343
   *     the additional options for the assignment.
344
   * @returns
345
   *     A target array deeply cloned from the source array, whose element type
346
   *     is exactly the same as the element type of the default array.
347
   * @author Haixing Hu
348
   * @private
349
   */
350
  cloneArrayWithoutElementTypeInfo(sourceArray, { sourcePath, defaultArray, options }) {
351
    // TODO: If there is type information in its default field value, we can
352
    //  construct an array of the same type based on the type information in
353
    //  the default field value.
354
    if (Array.isArray(sourceArray)) {
2✔
355
      // clone the source array with naming conversion options
356
      return clone(sourceArray, {
1✔
357
        ...CLONE_OPTIONS,
358
        convertNaming: options.convertNaming,
359
        sourceNamingStyle: options.sourceNamingStyle,
360
        targetNamingStyle: options.targetNamingStyle,
361
      });
362
    } else {
363
      console.error(`The value of the property '${sourcePath}' should be an array:`, sourceArray);
1✔
364
      // clone the default array without naming conversion
365
      return clone(defaultArray, CLONE_OPTIONS);
1✔
366
    }
367
  },
368

369
  /**
370
   * Copies all properties of a source object to a target object.
371
   *
372
   * @param {object} target
373
   *     The target object to be assigned to.
374
   * @param {object} source
375
   *     The source object to be assigned from, which can be `null` or `undefined`,
376
   *     or a plain old JavaScript object without class information.
377
   * @param {string} targetPath
378
   *     The path of the target object in the property tree of the original root
379
   *     object.
380
   * @param {string} sourcePath
381
   *     The path of the source object in the property tree of the original root
382
   *     object.
383
   * @param {function} type
384
   *     The type of the target object, i.e., the constructor of the class of
385
   *     the target object.
386
   * @param {object} defaultInstance
387
   *     A default instance in the class of the target object. If a certain
388
   *     attribute value of the target object does not exist in the source
389
   *     object or is null, the corresponding attribute value of this default
390
   *     instance is used to assign the corresponding attribute of the target
391
   *     object. Note that this argument may be `null` or `undefined`.
392
   * @param {object} options
393
   *     the additional options for the assignment.
394
   * @returns
395
   *     The target object after assignment.
396
   * @author Haixing Hu
397
   * @private
398
   */
399
  doAssign(target, source, { targetPath, sourcePath, type, defaultInstance, options }) {
400
    if (isUndefinedOrNull(source)) {
320✔
401
      // if source is nullish, assign the default object to the target object
402
      this.copyAllPropertiesWithoutNamingConversion(target, defaultInstance);
4✔
403
    } else {
404
      // Loops over all enumerable properties of the default instance,
405
      // excluding those inherited from the parent class
406
      const metadata = classMetadataCache.get(type);
316✔
407
      const theDefaultInstance = defaultInstance ?? getDefaultInstance(type);
316✔
408
      const targetKeys = this.getAllFields(target, source, { type, options });
316✔
409
      targetKeys.forEach((targetField) => {
316✔
410
        const defaultFieldValue = theDefaultInstance[targetField];
1,119✔
411
        const targetFieldPath = `${targetPath}.${targetField}`;
1,119✔
412
        const sourceField = getSourceField(targetField, options);
1,119✔
413
        const sourceFieldPath = `${sourcePath}.${sourceField}`;
1,119✔
414
        const sourceFieldValue = source[sourceField];
1,119✔
415
        // If field of the target object is annotated with `@Type`, or the
416
        // `options.targetTypes` has the type information of the field, get the field type
417
        const targetFieldType = getTargetFieldType(metadata, targetField, targetFieldPath, options);
1,119✔
418
        // If field of the target object is annotated with `@ElementType`,
419
        // or the `options.targetElementTypes` has the type information of the
420
        // field element, get the field element type
421
        const targetFieldElementType = getTargetFieldElementType(metadata, targetField, targetFieldPath, options);
1,119✔
422
        if (!Object.prototype.hasOwnProperty.call(source, sourceField)) {
1,119✔
423
          // If the source object does not have the field
424
          // warn if the source object has a field with different naming style
425
          const existSourceField = getExistFieldWithDifferentNamingStyle(sourceField, source);
98✔
426
          if (existSourceField) {
98✔
427
            console.warn(`Cannot find the source property '${sourceFieldPath}' `
3✔
428
              + `for the target property '${targetFieldPath}'. `
429
              + `But the source object has a property '${sourcePath}.${existSourceField}'. `
430
              + 'A correct naming conversion may be needed.');
431
            console.warn('source object:', source);
3✔
432
            console.warn('target object:', target);
3✔
433
          }
434
          // and then copy the default field value directly.
435
          target[targetField] = clone(defaultFieldValue, CLONE_OPTIONS);
98✔
436
        } else if (isUndefinedOrNull(sourceFieldValue)) {
1,021✔
437
          // If the source object has the field, but the field value is nullish,
438
          // copy the nullish source field value directly.
439
          target[targetField] = sourceFieldValue;
31✔
440
        } else if (targetFieldType) {
990✔
441
          // If the field of the target object is annotated with `@Type`
442
          target[targetField] = this.cloneWithTypeInfo(sourceFieldValue, {
284✔
443
            targetPath: targetFieldPath,
444
            sourcePath: sourceFieldPath,
445
            type: targetFieldType,
446
            defaultInstance: defaultFieldValue,
447
            options,
448
          });
449
        } else if (targetFieldElementType) {
706✔
450
          // If the field of the target object is annotated with `@ElementType`
451
          target[targetField] = this.cloneArrayWithElementTypeInfo(sourceFieldValue, {
18✔
452
            targetPath: targetFieldPath,
453
            sourcePath: sourceFieldPath,
454
            elementType: targetFieldElementType,
455
            defaultArray: defaultFieldValue,
456
            options,
457
          });
458
        } else if (isUndefinedOrNull(defaultFieldValue)) {
688✔
459
          // If the field value of the default instance is nullish, but the
460
          // source object field value is not nullish, and the field is not
461
          // decorated with `@Type`, it is impossible to determine the type of
462
          // the attribute, therefore we directly clone the source object field
463
          // value.
464
          // clone the source field value with the naming conversion options
465
          target[targetField] = clone(sourceFieldValue, {
13✔
466
            ...CLONE_OPTIONS,
467
            convertNaming: options.convertNaming,
468
            sourceNamingStyle: options.sourceNamingStyle,
469
            targetNamingStyle: options.targetNamingStyle,
470
          });
471
        } else if (Array.isArray(defaultFieldValue)) {
675✔
472
          // If the field value of the target object is an array but has not
473
          // been annotated with `@ElementType`
474
          target[targetField] = this.cloneArrayWithoutElementTypeInfo(sourceFieldValue, {
2✔
475
            targetPath: targetFieldPath,
476
            sourcePath: sourceFieldPath,
477
            defaultArray: defaultFieldValue,
478
            options,
479
          });
480
        } else if ((typeof defaultFieldValue) === 'object') {
673✔
481
          // If the property value of the target object is a non-null and
482
          // non-array object, we must create an object of the same prototype
483
          // and copy the property value of the source object
484
          target[targetField] = this.cloneWithoutTypeInfo(sourceFieldValue, {
73✔
485
            targetPath: targetFieldPath,
486
            sourcePath: sourceFieldPath,
487
            defaultInstance: defaultFieldValue,
488
            options,
489
          });
490
        } else {
491
          // If the attribute value of the target object is not an object,
492
          // directly clone the attribute value of the source object and assign
493
          // it to the target object.
494
          target[targetField] = clone(sourceFieldValue, {
600✔
495
            ...CLONE_OPTIONS,
496
            convertNaming: options.convertNaming,
497
            sourceNamingStyle: options.sourceNamingStyle,
498
            targetNamingStyle: options.targetNamingStyle,
499
          });
500
        }
501
      });
502
    }
503
    if (options.normalize) {
314✔
504
      return defaultNormalizer(target);
271✔
505
    } else {
506
      return target;
43✔
507
    }
508
  },
509
};
510

511
/**
512
 * Assigns the specified source object to the specified target object.
513
 *
514
 * @param {function|ObjectConstructor} Class
515
 *     The constructor of the class of the target object.
516
 * @param {object} target
517
 *     The target object which will be assigned to. This object must be an
518
 *     instance of the specified `Class`. Each fields of this object will be
519
 *     assigned from the corresponding fields of the source object, recursively.
520
 * @param {object} source
521
 *     The source object which will be assigned from. This object may be any
522
 *     plain old JavaScript object without class information.
523
 * @param {null|undefined|object} options
524
 *     the additional options for the assignment. If this argument is
525
 *     `undefined` or `null`, the default options will be used. The default
526
 *     options can be retrieved by calling `DefaultOptions.get('assign')`.
527
 *     Available options are:
528
 *     - `normalize: boolean`, indicates whether to normalize this object
529
 *       after the assignment. The default value is `true`.
530
 *     - `convertNaming: boolean`, indicates whether to convert the naming
531
 *       style of the target object. The default value is `false`.
532
 *     - `sourceNamingStyle: string | NamingStyle`, the naming style of the
533
 *       source object, i.e., the first argument of the `assign()` method.
534
 *       The default value of this argument is {@link LOWER_UNDERSCORE}.
535
 *     - `targetNamingStyle: string | NamingStyle`, the naming style of the
536
 *       target object, i.e., the object calling the `assign()` method. The
537
 *       default value of this argument is {@link LOWER_CAMEL}.
538
 * @return {Class}
539
 *     The target object after assignment.
540
 * @see DefaultOptions.get('assign')
541
 * @author Haixing Hu
542
 * @private
543
 */
544
function assignImpl(Class, target, source, options) {
545
  const defaultInstance = getDefaultInstance(Class);
130✔
546
  const opt = DefaultOptions.merge('assign', options);
130✔
547
  // If the source object is of the same class with the target object,
548
  // the naming conversion must be disabled
549
  if (source instanceof Class) {
130✔
550
    opt.convertNaming = false;
13✔
551
  }
552
  return Impl.doAssign(target, source, {
130✔
553
    targetPath: Class.name,
554
    sourcePath: 'source',
555
    type: Class,
556
    defaultInstance,
557
    options: opt,
558
  });
559
}
560

561
export default assignImpl;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc