(function (undefined) {
angular.module('rails').provider('railsSerializer', function() {
var defaultOptions = {
underscore: undefined,
camelize: undefined,
pluralize: undefined,
exclusionMatchers: []
};
/**
* Configures the underscore method used by the serializer. If not defined then RailsInflector.underscore
* 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 RailsInflector.camelize
* 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 RailsInflector.pluralize
* 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.only 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;
};
/**
* This is a shortcut for rename that allows you to specify a variable number of attributes that should all be renamed to
* {attributeName}_attributes
to work with the Rails nested_attributes feature.
* @param attributeNames... {string} Variable number of attribute name parameters
* @returns {Serializer} this for chaining support
*/
Serializer.prototype.nestedAttribute = function () {
var self = this;
angular.forEach(arguments, function (attributeName) {
self.rename(attributeName, attributeName + '_attributes');
});
return this;
};
/**
* Specifies an attribute that is a nested resource within the parent object.
* Nested resources do not imply nested attributes, if you want both you still have to specify call nestedAttribute
as well.
*
* A nested resource serves two purposes. First, it defines the resource that should be used when constructing resources from the server.
* Second, it specifies how the nested object should be serialized.
*
* An optional third parameter serializer
is available to override the serialization logic
* of the resource in case you need to serialize it differently in multiple contexts.
*
* @param attributeName {string} The name of the attribute that is a nested resource
* @param resource {string | Resource} A reference to the resource that the attribute is a type of.
* @param serializer {string | Serializer} (optional) An optional serializer reference to override the nested resource's default serializer
* @returns {Serializer} this for chaining support
*/
Serializer.prototype.resource = function (attributeName, resource, serializer) {
this.nestedResources[attributeName] = resource;
if (serializer) {
this.serializeWith(attributeName, serializer);
}
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;
};
/**
* Specify a custom serializer to use for an attribute.
*
* @param attributeName {string} The name of the attribute
* @param serializer {string | constructor} A reference to the custom serializer to use for the attribute.
* @returns {Serializer} this for chaining support
*/
Serializer.prototype.serializeWith = function (attributeName, serializer) {
this.customSerializers[attributeName] = serializer;
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;
};
/**
* Returns a reference to the nested resource that has been specified for the attribute.
* @param attributeName The attribute name
* @returns {*} undefined if no nested resource has been specified or a reference to the nested resource class
*/
Serializer.prototype.getNestedResource = function (attributeName) {
return RailsResourceInjector.getDependency(this.nestedResources[attributeName]);
};
/**
* Returns a custom serializer for the attribute if one has been specified. Custom serializers can be specified
* in one of two ways. The serializeWith customization method allows specifying a custom serializer for any attribute.
* Or an attribute could have been specified as a nested resource in which case the nested resource's serializer
* is used. Custom serializers specified using serializeWith take precedence over the nested resource serializer.
*
* @param attributeName The attribute name
* @returns {*} undefined if no custom serializer has been specified or an instance of the Serializer
*/
Serializer.prototype.getAttributeSerializer = function (attributeName) {
var resource = this.getNestedResource(attributeName),
serializer = this.customSerializers[attributeName];
// custom serializer takes precedence over resource serializer
if (serializer) {
return RailsResourceInjector.createService(serializer);
} else if (resource) {
return resource.serializer;
}
return undefined;
};
/**
* 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 serializer = this.getAttributeSerializer(attribute),
serializedAttributeName = this.getSerializedAttributeName(attribute);
// undefined means the attribute should be excluded from serialization
if (serializedAttributeName === undefined) {
return;
}
data[serializedAttributeName] = serializer ? serializer.serialize(value) : 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, Resource) {
var result = data,
self = this;
if (angular.isArray(data)) {
result = [];
angular.forEach(data, function (value) {
result.push(self.deserializeValue(value, Resource));
});
} else if (angular.isObject(data)) {
if (angular.isDate(data)) {
return data;
}
result = {};
if (Resource) {
result = new Resource();
}
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 serializer,
NestedResource,
attributeName = this.getDeserializedAttributeName(attribute);
// undefined means the attribute should be excluded from serialization
if (attributeName === undefined) {
return;
}
serializer = this.getAttributeSerializer(attributeName);
NestedResource = this.getNestedResource(attributeName);
// preserved attributes are assigned unmodified
if (this.preservedAttributes[attributeName]) {
data[attributeName] = value;
} else {
data[attributeName] = serializer ? serializer.deserialize(value, NestedResource) : this.deserializeValue(value, NestedResource);
}
};
/**
* 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, Resource) {
// just calls deserializeValue for now so we can more easily add on custom attribute logic for deserialize too
return this.deserializeValue(data, Resource);
};
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;
}
railsSerializer.defaultOptions = defaultOptions;
return railsSerializer;
}];
});
}());