require 'bitmask_attribute/value_proxy' module BitmaskAttribute class Definition attr_reader :attribute, :values, :extension def initialize(attribute, values=[], &extension) @attribute = attribute @values = values @extension = extension end def install_on(model) validate_for model generate_bitmasks_on model override model create_convenience_class_method_on(model) create_convenience_instance_methods_on(model) create_named_scopes_on(model) end ####### private ####### def validate_for(model) # The model cannot be validated if it is preloaded and the attribute/column is not in the # database (the migration has not been run). This usually # occurs in the 'test' and 'production' environments. return if defined?(Rails) && Rails.configuration.cache_classes unless model.columns.detect { |col| col.name == attribute.to_s } raise ArgumentError, "`#{attribute}' is not an attribute of `#{model}'" end end def generate_bitmasks_on(model) model.bitmasks[attribute] = returning HashWithIndifferentAccess.new do |mapping| values.each_with_index do |value, index| mapping[value] = 0b1 << index end end end def override(model) override_getter_on(model) override_setter_on(model) end def override_getter_on(model) model.class_eval %( def #{attribute} @#{attribute} ||= BitmaskAttribute::ValueProxy.new(self, :#{attribute}, &self.class.bitmask_definitions[:#{attribute}].extension) end ) end def override_setter_on(model) model.class_eval %( def #{attribute}=(raw_value) values = raw_value.kind_of?(Array) ? raw_value : [raw_value] self.#{attribute}.replace(values.reject(&:blank?)) end ) end def create_convenience_class_method_on(model) model.class_eval %( def self.bitmask_for_#{attribute}(*values) values.inject(0) do |bitmask, value| unless (bit = bitmasks[:#{attribute}][value]) raise ArgumentError, "Unsupported value for #{attribute}: \#{value.inspect}" end bitmask | bit end end ) end def create_convenience_instance_methods_on(model) values.each do |value| model.class_eval %( def #{attribute}_for_#{value}? self.#{attribute}?(:#{value}) end ) end model.class_eval %( def #{attribute}?(*values) if !values.blank? values.all? do |value| self.#{attribute}.include?(value) end else self.#{attribute}.present? end end ) end def create_named_scopes_on(model) model.class_eval %( named_scope :with_#{attribute}, proc { |*values| if values.blank? {:conditions => '#{attribute} > 0 OR #{attribute} IS NOT NULL'} else sets = values.map do |value| mask = #{model}.bitmask_for_#{attribute}(value) "#{attribute} & \#{mask} <> 0" end {:conditions => sets.join(' AND ')} end } named_scope :without_#{attribute}, :conditions => "#{attribute} == 0 OR #{attribute} IS NULL" named_scope :no_#{attribute}, :conditions => "#{attribute} == 0 OR #{attribute} IS NULL" ) values.each do |value| model.class_eval %( named_scope :#{attribute}_for_#{value}, :conditions => ['#{attribute} & ? <> 0', #{model}.bitmask_for_#{attribute}(:#{value})] ) end end end def self.included(model) model.extend ClassMethods end module ClassMethods def bitmask(attribute, options={}, &extension) unless options[:as] && options[:as].kind_of?(Array) raise ArgumentError, "Must provide an Array :as option" end bitmask_definitions[attribute] = BitmaskAttribute::Definition.new(attribute, options[:as].to_a, &extension) bitmask_definitions[attribute].install_on(self) end def bitmask_definitions @bitmask_definitions ||= {} end def bitmasks @bitmasks ||= {} end end end