module VacuumCleaner
# @private
# Suffix added to existing setter methods
WITHOUT_NORMALIZATION_SUFFIX = "_without_normalization"
# Base module required to be included in
#
module Normalizations
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
# List of already normalized attributes, to keep everything safe
# when calling `normalizes` twice for a certain attribute.
#
# When called for the first time checks it's `superclass` for any
# normalized attributes.
def normalized_attributes
@normalized_attributes ||= [].tap do |ary|
superclass.normalized_attributes.each { |a| ary << a } if superclass && superclass.respond_to?(:normalized_attributes)
end
end
# Enables normalization chain for supplied attributes.
#
# @example Basic usage for plain old ruby objects.
# class Doctor
# include VacuumCleaner::Normalizations
# attr_accessor :name
# normalizes :name
# end
#
#
# @param [Strings, Symbols] attributes list of attribute names to normalize, at least one attribute is required
# @param [Hash] options optional list of normalizers to use, like +:downcase => true+. To not run the default
# normalizer ({VacuumCleaner::Normalizer#normalize_value}) set +:default => false+
#
# @yield [value] optional block to define some one-time custom normalization logic
# @yieldparam value can be +nil+, otherwise value as passed through the default normalizer
# @yieldreturn should return value as normalized by the block
#
# @yield [instance, attribute, value] optional (extended) block with all arguments, like the +object+ and
# current +attribute+ name. Everything else behaves the same es the single-value +yield+
def normalizes(*attributes, &block)
normalizations = attributes.last.is_a?(Hash) ? attributes.pop : {}
raise ArgumentError, "You need to supply at least one attribute" if attributes.empty?
normalizers = []
normalizers << Normalizer.new unless normalizations.delete(:default) === false
normalizations.each do |key, options|
begin
normalizers << const_get("#{VacuumCleaner.camelize_value(key)}Normalizer").new(options === true ? {} : options)
rescue NameError
raise ArgumentError, "Unknown normalizer: '#{key}'"
end
end
attributes.each do |attribute|
attribute = attribute.to_sym
# guard against calling it twice!
unless normalized_attributes.include?(attribute)
send(:define_method, :"normalize_#{attribute}") do |value|
value = normalizers.inject(value) { |v,n| n.normalize(self, attribute, v) }
block_given? ? (block.arity == 1 ? yield(value) : yield(self, attribute, value)) : value
end
original_setter = "#{attribute}#{VacuumCleaner::WITHOUT_NORMALIZATION_SUFFIX}=".to_sym
send(:alias_method, original_setter, "#{attribute}=") if instance_methods.include?(RUBY_VERSION =~ /^1.9/ ? :"#{attribute}=" : "#{attribute}=")
class_eval <<-RUBY, __FILE__, __LINE__+1
def #{attribute}=(value) # 1. def name=(value)
value = send(:'normalize_#{attribute}', value) # 2. value = send(:'normalize_name', value)
return send(#{original_setter.inspect}, value) if respond_to?(#{original_setter.inspect}) # 3. return send(:'name_wi...=', value) if respond_to?(:'name_wi...=')
return send(:[]=, #{attribute.inspect}, value) if respond_to?(:[]=) # 4. return send(:[]=, :name, value) if respond_to?(:[]=)
@#{attribute} = value # 5. @name = value
end # 6. end
RUBY
normalized_attributes << attribute
end
end
end
end
end
# @private
# Okay, because this library currently does not depend on
# ActiveSupport or anything similar an "independent" camelizing process is
# required.
#
# How it works: If value.to_s responds to :camelize, then call it else, use implementation
# taken from http://github.com/rails/rails/blob/master/activesupport/lib/active_support/inflector/methods.rb#L25
def camelize_value(value)
value = value.to_s
value.respond_to?(:camelize) ? value.camelize : value.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
end
module_function :camelize_value
end
# load standard normalizations
Dir[File.dirname(__FILE__) + "/normalizations/*.rb"].sort.each do |path|
require "vacuum_cleaner/normalizations/#{File.basename(path)}"
end