XingFramework/xing-frontend-utils

View on GitHub
src/xing-frontend-utils/serializer.js

Summary

Maintainability
F
2 wks
Test Coverage
import Inflector from 'xing-inflector';
import {applyAnnotation, Module, Factory, Provider} from 'a1atscript'

@Provider('Serializer')
class SerializerProvider {
  constructor() {
    var defaultOptions = {
      underscore: undefined,
      camelize: undefined,
      pluralize: undefined,
      exclusionMatchers: []
    };

    /**
     * Configures the underscore method used by the serializer.  If not defined then <code>RailsInflector.underscore</code>
     * will be used.
     *
     * @param {function(string):string} fn The function to use for underscore conversion
     * @returns {railsSerializerProvider} The provider for chaining
     */
    this.underscore = function(fn) {
      defaultOptions.underscore = fn;
      return this;
    };

    /**
     * Configures the camelize method used by the serializer.  If not defined then <code>RailsInflector.camelize</code>
     * will be used.
     *
     * @param {function(string):string} fn The function to use for camelize conversion
     * @returns {railsSerializerProvider} The provider for chaining
     */
    this.camelize = function(fn) {
      defaultOptions.camelize = fn;
      return this;
    };

    /**
     * Configures the pluralize method used by the serializer.  If not defined then <code>RailsInflector.pluralize</code>
     * will be used.
     *
     * @param {function(string):string} fn The function to use for pluralizing strings.
     * @returns {railsSerializerProvider} The provider for chaining
     */
    this.pluralize = function(fn) {
      defaultOptions.pluralize = fn;
      return this;
    };

    /**
     * Configures the array exclusion matchers by the serializer.  Exclusion matchers can be one of the following:
     * * string - Defines a prefix that is used to test for exclusion
     * * RegExp - A custom regular expression that is tested against the attribute name
     * * function - A custom function that accepts a string argument and returns a boolean with true indicating exclusion.
     *
     * @param {Array.<string|function(string):boolean|RegExp} exclusions An array of exclusion matchers
     * @returns {railsSerializerProvider} The provider for chaining
     */
    this.exclusionMatchers = function(exclusions) {
      defaultOptions.exclusionMatchers = exclusions;
      return this;
    };

    this.$get = ['$injector', 'Inflector', function ($injector, Inflector) {
      defaultOptions.underscore = defaultOptions.underscore || Inflector.underscore;
      defaultOptions.camelize = defaultOptions.camelize || Inflector.camelize;
      defaultOptions.pluralize = defaultOptions.pluralize || Inflector.pluralize;

      function Serializer() {

        this.exclusions = {};
        this.inclusions = {};
        this.serializeMappings = {};
        this.deserializeMappings = {};
        this.customSerializedAttributes = {};
        this.preservedAttributes = {};
        this.options = angular.extend({excludeByDefault: false}, defaultOptions || {});
      }

      /**
       * Accepts a variable list of attribute names to exclude from JSON serialization.
       *
       * @param attributeNames... {string} Variable number of attribute name parameters
       * @returns {Serializer} this for chaining support
       */
      Serializer.prototype.exclude = function () {
        var exclusions = this.exclusions;

        angular.forEach(arguments, function (attributeName) {
          exclusions[attributeName] = false;
        });

        return this;
      };

      /**
       * Accepts a variable list of attribute names that should be included in JSON serialization.
       * Using this method will by default exclude all other attributes and only the ones explicitly included using <code>only</code> will be serialized.
       * @param attributeNames... {string} Variable number of attribute name parameters
       * @returns {Serializer} this for chaining support
       */
      Serializer.prototype.only = function () {
        var inclusions = this.inclusions;
        this.options.excludeByDefault = true;

        angular.forEach(arguments, function (attributeName) {
          inclusions[attributeName] = true;
        });

        return this;
      };

      /**
       * Specifies a custom name mapping for an attribute.
       * On serializing to JSON the jsonName will be used.
       * On deserialization, if jsonName is seen then it will be renamed as javascriptName in the resulting resource.
       *
       * @param javascriptName {string} The attribute name as it appears in the JavaScript object
       * @param jsonName {string} The attribute name as it should appear in JSON
       * @param bidirectional {boolean} (optional) Allows turning off the bidirectional renaming, defaults to true.
       * @returns {Serializer} this for chaining support
       */
      Serializer.prototype.rename = function (javascriptName, jsonName, bidirectional) {
        this.serializeMappings[javascriptName] = jsonName;

        if (bidirectional || bidirectional === undefined) {
          this.deserializeMappings[jsonName] = javascriptName;
        }
        return this;
      };

      /**
       * Allows custom attribute creation as part of the serialization to JSON.
       *
       * @param attributeName {string} The name of the attribute to add
       * @param value {*} The value to add, if specified as a function then the function will be called during serialization
       *     and should return the value to add.
       * @returns {Serializer} this for chaining support
       */
      Serializer.prototype.add = function (attributeName, value) {
        this.customSerializedAttributes[attributeName] = value;
        return this;
      };


      /**
       * Allows the attribute to be preserved unmodified in the resulting object.
       *
       * @param attributeName {string} The name of the attribute to add
       * @returns {Serializer} this for chaining support
       */
      Serializer.prototype.preserve = function(attributeName) {
        this.preservedAttributes[attributeName] =  true;
        return this;
      };

      /**
       * Determines whether or not an attribute should be excluded.
       *
       * If the option excludeByDefault has been set then attributes will default to excluded and will only
       * be included if they have been included using the "only" customization function.
       *
       * If the option excludeByDefault has not been set then attributes must be explicitly excluded using the "exclude"
       * customization function or must be matched by one of the exclusionMatchers.
       *
       * @param attributeName The name of the attribute to check for exclusion
       * @returns {boolean} true if excluded, false otherwise
       */
      Serializer.prototype.isExcludedFromSerialization = function (attributeName) {
        if ((this.options.excludeByDefault && !this.inclusions.hasOwnProperty(attributeName)) || this.exclusions.hasOwnProperty(attributeName)) {
          return true;
        }

        if (this.options.exclusionMatchers) {
          var excluded = false;

          angular.forEach(this.options.exclusionMatchers, function (matcher) {
            if (angular.isString(matcher)) {
              excluded = excluded || attributeName.indexOf(matcher) === 0;
            } else if (angular.isFunction(matcher)) {
              excluded = excluded || matcher.call(undefined, attributeName);
            } else if (matcher instanceof RegExp) {
              excluded = excluded || matcher.test(attributeName);
            }
          });

          return excluded;
        }

        return false;
      };

      /**
       * Remaps the attribute name to the serialized form which includes:
       *   - checking for exclusion
       *   - remapping to a custom value specified by the rename customization function
       *   - underscoring the name
       *
       * @param attributeName The current attribute name
       * @returns {*} undefined if the attribute should be excluded or the mapped attribute name
       */
      Serializer.prototype.getSerializedAttributeName = function (attributeName) {
        var mappedName = this.serializeMappings[attributeName] || attributeName;

        var mappedNameExcluded = this.isExcludedFromSerialization(mappedName),
        attributeNameExcluded = this.isExcludedFromSerialization(attributeName);

        if(this.options.excludeByDefault) {
          if(mappedNameExcluded && attributeNameExcluded) {
            return undefined;
          }
        } else {
          if (mappedNameExcluded || attributeNameExcluded) {
            return undefined;
          }
        }

        return this.underscore(mappedName);
      };

      /**
       * Determines whether or not an attribute should be excluded from deserialization.
       *
       * By default, we do not exclude any attributes from deserialization.
       *
       * @param attributeName The name of the attribute to check for exclusion
       * @returns {boolean} true if excluded, false otherwise
       */
      Serializer.prototype.isExcludedFromDeserialization = function (attributeName) {
        return false;
      };

      /**
       * Remaps the attribute name to the deserialized form which includes:
       *   - camelizing the name
       *   - checking for exclusion
       *   - remapping to a custom value specified by the rename customization function
       *
       * @param attributeName The current attribute name
       * @returns {*} undefined if the attribute should be excluded or the mapped attribute name
       */
      Serializer.prototype.getDeserializedAttributeName = function (attributeName) {
        var camelizedName = this.camelize(attributeName);

        camelizedName = this.deserializeMappings[attributeName] ||
          this.deserializeMappings[camelizedName] ||
          camelizedName;

        if (this.isExcludedFromDeserialization(attributeName) || this.isExcludedFromDeserialization(camelizedName)) {
          return undefined;
        }

        return camelizedName;
      };

      /**
       * Prepares the data for serialization to JSON.
       *
       * @param data The data to prepare
       * @returns {*} A new object or array that is ready for JSON serialization
       */
      Serializer.prototype.serializeValue = function (data) {
        var result = data,
        self = this;

        if (angular.isArray(data)) {
          result = [];

          angular.forEach(data, function (value) {
            result.push(self.serializeValue(value));
          });
        } else if (angular.isObject(data)) {
          if (angular.isDate(data)) {
            return data;
          }
          result = {};

          angular.forEach(data, function (value, key) {
            // if the value is a function then it can't be serialized to JSON so we'll just skip it
            if (!angular.isFunction(value)) {
              self.serializeAttribute(result, key, value);
            }
          });
        }

        return result;
      };

      /**
       * Transforms an attribute and its value and stores it on the parent data object.  The attribute will be
       * renamed as needed and the value itself will be serialized as well.
       *
       * @param data The object that the attribute will be added to
       * @param attribute The attribute to transform
       * @param value The current value of the attribute
       */
      Serializer.prototype.serializeAttribute = function (data, attribute, value) {
        var serializedAttributeName = this.getSerializedAttributeName(attribute);

        // undefined means the attribute should be excluded from serialization
        if (serializedAttributeName === undefined) {
          return;
        }

        data[serializedAttributeName] = this.serializeValue(value);
      };

      /**
       * Serializes the data by applying various transformations such as:
       *   - Underscoring attribute names
       *   - attribute renaming
       *   - attribute exclusion
       *   - custom attribute addition
       *
       * @param data The data to prepare
       * @returns {*} A new object or array that is ready for JSON serialization
       */
      Serializer.prototype.serialize = function (data) {
        var result = this.serializeValue(data),
        self = this;

        if (angular.isObject(result)) {
          angular.forEach(this.customSerializedAttributes, function (value, key) {
            if (angular.isFunction(value)) {
              value = value.call(data, data);
            }

            self.serializeAttribute(result, key, value);
          });
        }

        return result;
      };

      /**
       * Iterates over the data deserializing each entry on arrays and each key/value on objects.
       *
       * @param data The object to deserialize
       * @param Resource (optional) The resource type to deserialize the result into
       * @returns {*} A new object or an instance of Resource populated with deserialized data.
       */
      Serializer.prototype.deserializeValue = function (data) {
        var result = data,
        self = this;

        if (angular.isArray(data)) {
          result = [];

          angular.forEach(data, function (value) {
            result.push(self.deserializeValue(value));
          });
        } else if (angular.isObject(data)) {
          if (angular.isDate(data)) {
            return data;
          }

          result = {};


          angular.forEach(data, function (value, key) {
            self.deserializeAttribute(result, key, value);
          });
        }

        return result;
      };

      /**
       * Transforms an attribute and its value and stores it on the parent data object.  The attribute will be
       * renamed as needed and the value itself will be deserialized as well.
       *
       * @param data The object that the attribute will be added to
       * @param attribute The attribute to transform
       * @param value The current value of the attribute
       */
      Serializer.prototype.deserializeAttribute = function (data, attribute, value) {
        var attributeName = this.getDeserializedAttributeName(attribute);

        // undefined means the attribute should be excluded from serialization
        if (attributeName === undefined) {
          return;
        }

        // preserved attributes are assigned unmodified
        if (this.preservedAttributes[attributeName]) {
          data[attributeName] = value;
        } else {
          data[attributeName] = this.deserializeValue(value);
        }
      };

      /**
       * Deserializes the data by applying various transformations such as:
       *   - Camelizing attribute names
       *   - attribute renaming
       *   - attribute exclusion
       *   - nested resource creation
       *
       * @param data The object to deserialize
       * @param Resource (optional) The resource type to deserialize the result into
       * @returns {*} A new object or an instance of Resource populated with deserialized data
       */
      Serializer.prototype.deserialize = function (data) {
        // just calls deserializeValue for now so we can more easily add on custom attribute logic for deserialize too
        return this.deserializeValue(data);
      };

      Serializer.prototype.pluralize = function (value) {
        if (this.options.pluralize) {
          return this.options.pluralize(value);
        }
        return value;
      };

      Serializer.prototype.underscore = function (value) {
        if (this.options.underscore) {
          return this.options.underscore(value);
        }
        return value;
      };

      Serializer.prototype.camelize = function (value) {
        if (this.options.camelize) {
          return this.options.camelize(value);
        }
        return value;
      };

      return Serializer;
    }];
  }
}

function RequestInterceptor(Serializer) {
  var serializer = new Serializer();

  return function(elem, operation, what) {
    var retElem = elem;
    if (operation === 'post' || operation === 'put') {
      retElem = serializer.serialize(elem);
    }
    return retElem;
  };
}

applyAnnotation(RequestInterceptor, Factory, 'RequestInterceptor', ['Serializer'])

function ResponseInterceptor(Serializer) {
  var serializer = new Serializer();

  return function(data, operation, what, url, response, deferred) {
    return serializer.deserialize(data);
  };
}

applyAnnotation(ResponseInterceptor, Factory, 'ResponseInterceptor', ['Serializer'])

var Serializer = new Module('serializer', [
  Inflector,
  SerializerProvider,
  RequestInterceptor,
  ResponseInterceptor]);

export default Serializer;