@haixing_hu/common-decorator

2.3.3 • Public • Published

js-common-decorator

npm package License 中文文档 CircleCI Coverage Status

@haixing_hu/common-decorator is a JavaScript library of common decorators, provides decorators to add common methods to domain classes. The library supports the most recent (currently May 2023) stage 3 proposal of JavaScript decorators.

Table of Contents

Usage

@Model Decorator

This decorator is used to decorate a domain model class, which adds the following instance and class methods to the decorated class.

NOTE: If the decorated class already implements any of the following methods, this decorator will not override the methods already implemented by the decorated class.

Instance method: Class.prototype.assign(obj, normalized)

  • Parameters:
    • obj: object: the object whose fields will be copied to this object, which may have a different prototype to this object.
    • normalized: boolean: whether to normalize this object after copying properties. Default value is true.
  • Returns:
    • object: the calling object itself.

This function copies the fields of the object obj to this object, only copying fields defined in this object's class. If a field in the obj object is undefined or null, it sets the field's value to the default value. Note that obj can have a different prototype to this object. The normalized parameter indicates whether to normalize this object after copying properties, with a default value of true.

Instance method: Class.prototype.clone()

  • Parameters: none.
  • Returns:
    • object: a instance of the specified class deep cloned from the calling object.

This function deep clones the calling object, returning a new instance of the specified class with the same property values as the calling object. Note that the returned object has the same prototype as the calling object.

Instance method: Class.prototype.clear()

  • Parameters: none.
  • Returns:
    • object: the calling object itself.

This function sets all the properties of this object to their default values. The default value of a field is the value of the field of the default constructed instance of the class.

Instance method: Class.prototype.isEmpty()

  • Parameters: none.
  • Returns:
    • boolean: whether this object is empty.

This function checks if this object is empty, meaning that all of its fields have default values. The default value of a field is the value of the field of the default constructed instance of the class.

Instance method: Class.prototype.equals(other)

  • Parameters:
    • other: object: the object to be compared with this object.
  • Returns:
    • boolean: whether this object is deeply equal to other.

This function checks whether this object is deeply equal to other. Two objects are deeply equal if and only if they have the same prototype, and all of their fields are deeply equal. Two fields are deeply equal if and only if they have the same value, or they are both undefined or null. If a field is an array, it is deeply equal to another array if and only if they have the same length, and all of their elements are deeply equal. If a field is an object, it is deeply equal to another object if and only if they have the same prototype, and all of their fields are deeply equal.

Instance method: Class.prototype.generateId()

  • Parameters: none.
  • Returns:
    • string: the string representation of the generated globally unique ID set to the calling object.

If the decorated class defines a property named id, this instance method generateId() is automatically added to the decorated class. Each call to this method generates a globally unique ID for the current calling object (represented as a string of an integer), sets the id field of the calling object to the generated ID, and returns the generated ID. Note that if a parent class A defines the id field, and a subclass B inherits the id field but does not define its own id field, the generateId() method is added only to class A, not to class B.

Instance method: Class.prototype.normalizeField(field)

  • Parameters:
    • field: string: the name of the specified field to be normalized.
  • Returns:
    • boolean: whether the specified field was normalized.

This function normalizes the specified field of this object. If the object has the specified field and the specified field is normalizable, the function normalizes the specified field and returns true; otherwise, the function does nothing and returns false. Note that a field is normalizable if and only if it is decorated by the {@link Normalizable} decorator.

Instance method: Class.prototype.normalize(fields)

  • Parameters:
    • fields: undefined | null | string | string[]: the fields to be normalized. It can be one of the following values:
      • undefined: normalizes all the normalizable fields of this object.
      • null: normalizes all the normalizable fields of this object.
      • "*": normalizes all the normalizable fields of this object.
      • string[]: normalizes all the normalizable fields whose names are specified in this array.
  • Returns:
    • object: the normalized calling object.

This function normalizes the specified fields of this object. The fields parameter specifies the names of fields to be normalized. If fields is undefined, null, or the string "*", it normalizes all the normalizable fields of this object. If fields is an array of strings, it normalizes all the normalizable fields whose names are specified in the array. Note that a field is normalizable if and only if it is decorated by the {@link Normalizable} decorator.

Instance method: Class.prototype.validateField(field)

  • Parameters:
    • field: string: the name of the specified field to be validated.
  • Returns:
    • ValidationResult | null: the validation result.

This function validates the specified field of this object. If the object has the specified field and the specified field is validatable, the function validates the specified field and returns the validation result; otherwise, the function does nothing and returns null. Note that a field is validatable if and only if it is decorated by the {@link Validatable} decorator.

Instance method: Class.prototype.validate(fields)

  • Parameters:
    • fields: undefined | null | string | string[]: the fields to be validated. It can be one of the following values:
      • undefined: validates all the validatable fields of this object.
      • null: validates all the validatable fields of this object.
      • "*": validates all the validatable fields of this object.
      • string[]: validates all the validatable fields whose names are specified in this array.
  • Returns:
    • ValidationResult: the validation result.

This function validates the specified fields of this object. The fields parameter specifies the names of fields to be validated. If fields is undefined, null, or the string "*", it validates all the validatable fields of this object. If fields is an array of strings, it validates all the validatable fields whose names are specified in the array. Note that a field is validatable if and only if it is decorated by the {@link Validatable} decorator.

Class method: Class.create(obj, normalized)

  • Parameters:
    • obj: object: the data object used to create the new instance.
    • normalized: boolean: whether to normalize the returned object. Default value is true.
  • Returns:
    • object | null: if the obj is undefined or null, returns null; otherwise, returns a new instance of the model class whose fields are initialized with the data in the obj.

This function creates a instance of the specified class from a data object, whose fields are recursively initialized with properties in the obj. Note that obj can have a different prototype to the specified class. The normalized parameter indicates whether to normalize the returned object, with a default value of true.

Class method: Class.createArray(array, normalized)

  • Parameters:
    • array: object[]: the data object array used to create the new array.
    • normalized: boolean: whether to normalize the objects in the returned array. Default value is true.
  • Returns:
    • object[] | null: if the array is undefined or null, returns null; otherwise, returns a new array of instances of the model class whose fields are initialized with corresponding data object in the array.

This function creates an array of instances of the specified class from a data object array. The fields of instances in the returned array are recursively initialized with corresponding properties of the corresponding data object in the array. Note that data objects in array can have different prototypes to the specified class. The normalized parameter indicates whether to normalize instances in the returned array, with a default value of true.

Class method: Class.createPage(page)

  • Parameters:
    • page: object: the pagination data object used to create the new Page instance.
  • Returns:
    • Page | null: if the page is undefined or null, returns null; otherwise, returns a new instance of the Page class whose content are initialized with the content of the pagination data object page.

This function creates a Page object, whose content are initialized with the content of the specified pagination data object. Typically, page is a list of domain objects obtained from a server using the GET method, and the object should conform to the Page class definition. This class method returns a new Page object, with the content property being the result of createArray(page.content, true), and the other properties matching those of the page object. If page is not a valid Page object, it returns null.

Class method: Class.isNullishOrEmpty(obj)

  • Parameters:
    • obj: object: the object to be checked.
  • Returns:
    • boolean: whether the specified object is undefined, null, or an empty object constructed with default values.

This function checks whether the specified object is undefined, null, or an empty object constructed with default values. An object is empty if and only if all of its fields have default values. The default value of a field is the value of the field of the default constructed instance of the class. This function is a convenient method to call Class.prototype.isEmpty(), with the handling of nullish values.

Usage Examples

The following is the usage example of the @Model decorator.

@Model 
class Credential {

  @Normalizable
  @Validator(validateCredentialTypeField)
  @Type(CredentialType)
  @Label('证件类型')
  type = 'IDENTITY_CARD';

  @Normalizable(trimUppercaseString)
  @Validator(validateCredentialNumberField)
  @Label('证件号码')
  number = '';

  constructor(type = CredentialType.DEFAULT.value, number = '') {
    this.type = type;
    this.number = number;
  }

  isIdentityCard() {
    return (this.type === 'IDENTITY_CARD');
  }
}

@Model 
class Person {

  @Normalizable(trimString)
  @Label('ID')
  id = null;

  @Normalizable(trimUppercaseString)
  @Validator(validatePersonNameField)
  @Label('姓名')
  name = '';

  @Normalizable
  @DefaultValidator
  @Type(Credential)
  @Label('证件')
  credential = null;

  @Normalizable
  @Validator(validatePersonGenderField)
  @Type(Gender)
  @Label('性别')
  gender = '';

  @Normalizable(trimString)
  @Validator(validatePersonBirthdayField)
  @Label('出生日期')
  birthday = '';

  @Normalizable(trimUppercaseString)
  @Validator(validateMobileField)
  @Label('手机号码')
  mobile = '';

  @Normalizable(trimString)
  @Validator(validateEmailField)
  @Label('电子邮件地址')
  @Nullable
  email = '';

  equals(other) {
    if (!(other instanceof PersonWithEquals)) {
      return false;
    }
    if ((this.credential === null) || (other.credential === null)) {
      // If one of the two people does not have ID inofmation, it is impossible
      // to compare whether they are the same person thus they will be considered 
      // different.
      return false;
    }
    // Two persons are logically equals if and only if they have the same 
    // credential.
    return (this.credential.type === other.credential.type)
        && (this.credential.number === other.credential.number);
  }
}

After applying the @Model decorator, the following methods will be automatically added:

  • Credential.prototype.assign(obj, normalized)
  • Credential.prototype.clone()
  • Credential.prototype.clear()
  • Credential.prototype.isEmpty()
  • Credential.prototype.equals(obj)
  • Credential.prototype.normalizeField(field)
  • Credential.prototype.normalize(fields)
  • Credential.prototype.validateField(field, options)
  • Credential.prototype.validate(fields, options)
  • Credential.create(obj, normalized)
  • Credential.createArray(array, normalized)
  • Credential.createPage(page, normalized)
  • Credential.isNullishOrEmpty(obj)
  • Person.prototype.assign(obj, normalized)
  • Person.prototype.clone()
  • Person.prototype.clear()
  • Person.prototype.isEmpty()
  • Person.prototype.generateId()
  • Person.prototype.normalizeField(field)
  • Person.prototype.normalize(fields)
  • Person.prototype.validateField(field, options)
  • Person.prototype.validate(fields, options)
  • Person.create(obj, normalized)
  • Person.createArray(array, normalized)
  • Person.createPage(page, normalized)
  • Person.isNullishOrEmpty(obj)

NOTE:

  • Because the Credential class does not have an id attribute, the @Model decorator does not add a generateId() instance method to it.
  • Because Person already implements the Person.prototype.equals() method, the @Model decorator will not override its own implementation of the Person.prototype.equals() method.

@Enum Decorator

This decorator is used to decorate an enumeration class.

Enumerator Fields

An enumeration class is a class whose instances are enumerators. An enumerator is an object with the following properties:

  • value:the value of the enumerator, which is exactly the name of the static field of the enumeration class that corresponds to the enumerator.
  • name: the display name of the enumerator, which could be specified by the default string or object value of the static field of the enumeration class that corresponds to the enumerator. It the default value is not specified, the name of the enumerator is the same as its value.
  • i18n: the i18n key of the enumerator, which is an optional property. It could be specified by the default object value of the static field of the enumeration class that corresponds to the enumerator. If this property is specified, the name property will be transformed to a getter, which will get the i18n value of the enumerator from the i18n resource bundle.
  • code: the code of the enumerator, which is an optional property. It could be specified by the default object value of the static field of the enumeration class that corresponds to the enumerator.
  • other properties: other properties of the enumerator could be specified by the default object value of the static field of the enumeration class that corresponds to the enumerator.

Instance method: Class.prototype.toString()

  • Parameters: none.
  • Returns:
    • string: the string representation of this enumerator, which is the value of this enumerator.

This function returns the string representation of this enumerator, which is the value of this enumerator.

Instance method: Class.prototype.toJSON()

  • Parameters: none.
  • Returns:
    • string: the JSON representation of this enumerator, which is the JSON string representation of the value of this enumerator, i.e., the double quoted string of the value.

This function returns the JSON representation of this enumerator.

Class method: Class.values()

  • Parameters: none.
  • Returns:
    • Class[]: the array of all enumerators of this enumeration class.

This function returns the array of all enumerators of this enumeration class.

Class method: Class.ofValue(value)

  • Parameters:
    • value: string: the value of the enumerator to be returned. Note that this argument will be trimmed and uppercased to get the actual value of the enumerator.
  • Returns:
    • Class: the enumerator in this enumeration class with the specified value, or undefined if no such enumerator exists.

This function returns the enumerator with the specified value.

Class method: Class.hasValue(value)

  • Parameters:
    • value: string: the value of the enumerator to be tested. Note that this argument will be trimmed and uppercased to get the actual value of the enumerator.
  • Returns:
    • boolean: returns true if there is an enumerator in this enumeration class with the specified value, or false otherwise.

This function tests whether there is an enumerator with the specified value.

Class method: Class.ofName(name)

  • Parameters:
    • name: string: the name of the enumerator to be returned.
  • Returns:
    • Class: the enumerator in this enumeration class with the specified name, or undefined if no such enumerator exists.

This function returns the enumerator with the specified name.

Class method: Class.hasName(name)

  • Parameters:
    • name: string: the name of the enumerator to be tested.
  • Returns:
    • boolean: returns true if there is an enumerator in this enumeration class with the specified name, or false otherwise.

This function tests whether there is an enumerator with the specified name.

Class method: Class.ofCode(code)

  • Parameters:
    • code: string: the code of the enumerator to be returned.
  • Returns:
    • Class: the enumerator in this enumeration class with the specified code, or undefined if no such enumerator exists.

This function returns the enumerator with the specified value.

Class method: Class.hasCode(code)

  • Parameters:
    • code: string: the code of the enumerator to be tested.
  • Returns:
    • boolean: returns true if there is an enumerator in this enumeration class with the specified code, or false otherwise.

This function tests whether there is an enumerator with the specified code.

Usage Example

@Enum
class Gender {
  static MALE = 'Male';
  static FEMALE = 'Female';
}

The above code is equivalent to the following code:

class Gender {
  static MALE = Object.freeze(new Gender('MALE', 'Male'));

  static FEMALE = Object.freeze(new Gender('FEMALE', 'Female'));

  static values() {
    return [ Gender.MALE, Gender.FEMALE ];
  }

  static ofValue(value) {
    switch (value) {
    case 'MALE':
      return Gender.MALE;
    case 'FEMALE':
      return Gender.FEMALE;
    default:
      return undefined;
    }
  }

  static hasValue(value) {
    return Gender.ofValue(value) !== undefined;
  }

  static ofName(name) {
    return Gender.values().find((e) => e.name === name);
  }

  static hasName(name) {
    return Gender.ofName(name) !== undefined;
  }

  static ofCode(code) {
    return Gender.values().find((e) => e.code === code);
  }

  static hasCode(code) {
    return Gender.ofCode(code) !== undefined;
  }

  constructor(value, name) {
    this.value = value;
    this.name = name;
  }

  toString() {
    return this.value;
  }

  toJSON() {
    return this.value;
  }
}

The static fields of the enumeration class could also be specified as objects, which will be used to initialize the enumerators. For example:

@Enum
class Gender {
  static MALE = { name: 'Male', i18n: 'i18n.gender.male', code: '001', data: { value: 0 } };

  static FEMALE = { name: 'Female', i18n: 'i18n.gender.female', code: '002', data: { value: 1 } };
}

The above code is equivalent to the following code:

class Gender {
  static MALE = Object.freeze(new Gender('MALE', 'Male',
     { i18n: 'i18n.gender.male', code: '001', data: {value: 0 } }));

  static FEMALE = Object.freeze(new Gender('FEMALE', 'Female',
     { i18n: 'i18n.gender.female', code: '002', data: {value: 1 } }));

  ...

  constructor(value, name, payload) {
    this.value = value;
    this.name = name;
    Object.assign(this, payload);
  }

  ...
}

Note that the enumerator in the above Gender class has a code, i18n and data properties. Since it has i18n property which specifies the i18n key of the enumerator in the resource bundle, the name property of the enumerator will be transformed to a getter which will get the i18n value corresponding to the i18n key from the i18n resource bundle.

The enumerators can also be defined without default values, for example:

@Enum
class Gender {
  static MALE;
  static FEMALE;
}

The above code is equivalent to the following code:

class Gender {
  static MALE = Object.freeze(new Gender('MALE'));

  static FEMALE = Object.freeze(new Gender('FEMALE'));

  ...

  constructor(value) {
    this.value = value;
    this.name = value;
  }

  ...
}

That is, the name of the enumerator is exactly its value.

Configuration

This library uses the most recent (currently May 2023) stage 3 proposal of JavaScript decorators. Therefore, you must configure Babel with @babel/plugin-transform-class-properties and the @babel/plugin-proposal-decorators plugins.

NOTE: To support the stage 3 proposal of JavaScript decorator metadata, the version of the Babel plugin @babel/plugin-proposal-decorators must be at least 7.23.0.

Bundling with webpack

  1. Install the required dependencies:
    yarn add @haixing_hu/common-decorator
    yarn add --dev @babel/core @babel/runtime @babel/preset-env
    yarn add --dev @babel/plugin-proposal-decorators @babel/plugin-transform-class-properties @babel/plugin-transform-runtime
  2. Configure Babel by using the @babel/plugin-transform-class-properties and @babel/plugin-proposal-decorators plugins. A possible Babel configuration file babelrc.json is as follows:
    {
      "presets": [
        "@babel/preset-env"
      ],
      "plugins": [
        "@babel/plugin-transform-runtime",
        ["@babel/plugin-proposal-decorators", { "version": "2023-05" }],
        "@babel/plugin-transform-class-properties"
      ]
    }

Bundling with vite

  1. Install the required dependencies:
    yarn add @haixing_hu/common-decorator
    yarn add --dev @babel/core @babel/runtime @babel/preset-env
    yarn add --dev @babel/plugin-proposal-decorators @babel/plugin-transform-class-properties @babel/plugin-transform-runtime
  2. Configure Babel by using @babel/plugin-transform-class-properties and @babel/plugin-proposal-decorators plugins. A possible Babel configuration file babelrc.json is as follows:
    {
      "presets": [
        ["@babel/preset-env", { "modules": false }]
      ],
      "plugins": [
        "@babel/plugin-transform-runtime",
        ["@babel/plugin-proposal-decorators", { "version": "2023-05" }],
        "@babel/plugin-transform-class-properties"
      ]
    }
    Note: When bundling with vite, make sure to set the modules parameter of @babel/preset-env to false.
  3. Configure vite by modifying the vite.config.js file to add support for Babel. A possible vite.config.js file is as follows:
    import { fileURLToPath, URL } from 'node:url';
    import { defineConfig } from 'vite';
    import vue from '@vitejs/plugin-vue';
    import * as babel from '@babel/core';
    
    // A very simple Vite plugin support babel transpilation
    const babelPlugin = {
      name: 'plugin-babel',
      transform: (src, id) => {
        if (/\.(jsx?|vue)$/.test(id)) {              // the pattern of the file to handle
          return babel.transform(src, {
            filename: id,
            babelrc: true,
          });
        }
      },
    };
    // https://vitejs.dev/config/
    export default defineConfig({
      plugins: [
        vue({
          script: {
            babelParserPlugins: ['decorators'],     // must enable decorators support
          },
        }),
        babelPlugin,                                // must be after the vue plugin
      ],
      resolve: {
        alias: {
          '@': fileURLToPath(new URL('./src', import.meta.url)),
        },
      },
    });
    Note: In the above configuration file, we've implemented a simple vite plugin to transpile the code processed by the vite-plugin-vue plugin using Babel. Although there's a vite-plugin-babel plugin that claims to add Babel support to vite, we found it doesn't correctly handle [vue] Single File Components (SFCs). After closely examining its source code, we determined that to achieve correct transpilation, we need to apply Babel after vite-plugin-vue processes the source code. Therefore, the very simple plugin function above suffices for our needs. As an alternative, you can use our version of vite-plugin-babel, and the following is an example configuration:
    import { fileURLToPath, URL } from 'node:url';
    import { defineConfig } from 'vite';
    import vue from '@vitejs/plugin-vue';
    import babel from '@haixing_hu/vite-plugin-babel';
    
    export default defineConfig({
      plugins: [
        vue({
          script: {
            babelParserPlugins: ['decorators'],     // must enable decorators support
          },
        }),
        babel(),
      ],
      resolve: {
        alias: {
          '@': fileURLToPath(new URL('./src', import.meta.url)),
        },
      },
    });

Contributing

If you find any issues or have suggestions for improvements, please feel free to open an issue or submit a pull request to the GitHub repository.

License

@haixing_hu/common-decorator is distributed under the Apache 2.0 license. See the LICENSE file for more details.

Readme

Keywords

Package Sidebar

Install

npm i @haixing_hu/common-decorator

Weekly Downloads

2

Version

2.3.3

License

Apache-2.0

Unpacked Size

4.4 MB

Total Files

152

Last publish

Collaborators

  • haixing-hu