From db4866d86d1d787ddcabc70bbbf3c3429cb3da6b Mon Sep 17 00:00:00 2001 From: Joel Moss Date: Thu, 30 Jun 2011 17:46:55 +0100 Subject: [PATCH 01/14] Replaced #returning with #tap --- lib/bitmask_attribute.rb | 167 +++++++++++++-------------- lib/bitmask_attribute/value_proxy.rb | 60 +++++----- 2 files changed, 110 insertions(+), 117 deletions(-) diff --git a/lib/bitmask_attribute.rb b/lib/bitmask_attribute.rb index 0da85d1..a1174f9 100644 --- a/lib/bitmask_attribute.rb +++ b/lib/bitmask_attribute.rb @@ -1,10 +1,10 @@ require 'bitmask_attribute/value_proxy' module BitmaskAttribute - class Definition attr_reader :attribute, :values, :extension + def initialize(attribute, values=[], &extension) @attribute = attribute @values = values @@ -20,110 +20,108 @@ def install_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 + 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}'" + unless model.columns.detect { |col| col.name == attribute.to_s } + raise ArgumentError, "`#{attribute}' is not an attribute of `#{model}'" + end 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 + def generate_bitmasks_on(model) + model.bitmasks[attribute] = HashWithIndifferentAccess.new.tap do |mapping| + values.each_with_index do |value, index| + mapping[value] = 0b1 << index + end end end - end - def override(model) - override_getter_on(model) - override_setter_on(model) - 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_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 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}" + 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 - bitmask | bit end - end - ) - end + ) + end - def create_convenience_instance_methods_on(model) - values.each do |value| + 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}_for_#{value}? - self.#{attribute}?(:#{value}) + def #{attribute}?(*values) + if !values.blank? + values.all? do |value| + self.#{attribute}.include?(value) + end + else + self.#{attribute}.present? + end 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| + def create_named_scopes_on(model) model.class_eval %( - named_scope :#{attribute}_for_#{value}, - :conditions => ['#{attribute} & ? <> 0', #{model}.bitmask_for_#{attribute}(:#{value})] + 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" ) - end - end + values.each do |value| + model.class_eval %( + named_scope :#{attribute}_for_#{value}, + :conditions => ['#{attribute} & ? <> 0', #{model}.bitmask_for_#{attribute}(:#{value})] + ) + end + end end @@ -149,6 +147,5 @@ def bitmasks @bitmasks ||= {} end - end - + end end diff --git a/lib/bitmask_attribute/value_proxy.rb b/lib/bitmask_attribute/value_proxy.rb index a2e36be..454ba77 100644 --- a/lib/bitmask_attribute/value_proxy.rb +++ b/lib/bitmask_attribute/value_proxy.rb @@ -1,5 +1,4 @@ module BitmaskAttribute - class ValueProxy < Array def initialize(record, attribute, &extension) @@ -17,7 +16,7 @@ def initialize(record, attribute, &extension) %w(push << delete replace reject! select!).each do |override| class_eval(<<-EOEVAL) def #{override}(*args) - returning(super) do + (super).tap do updated! end end @@ -28,45 +27,42 @@ def to_i inject(0) { |memo, value| memo | @mapping[value] } end - ####### private - ####### - def validate! - each do |value| - if @mapping.key? value - true - else - raise ArgumentError, "Unsupported value for `#{@attribute}': #{value.inspect}" + def validate! + each do |value| + if @mapping.key? value + true + else + raise ArgumentError, "Unsupported value for `#{@attribute}': #{value.inspect}" + end end end - end - def updated! - validate! - uniq! - serialize! - end + def updated! + validate! + uniq! + serialize! + end - def serialize! - @record.send(:write_attribute, @attribute, to_i) - end + def serialize! + @record.send(:write_attribute, @attribute, to_i) + end - def extract_values - stored = [@record.send(:read_attribute, @attribute) || 0, 0].max - @mapping.inject([]) do |values, (value, bitmask)| - returning values do - values << value.to_sym if (stored & bitmask > 0) - end - end - end + def extract_values + stored = [@record.send(:read_attribute, @attribute) || 0, 0].max + @mapping.inject([]) do |values, (value, bitmask)| + values.tap do + values << value.to_sym if (stored & bitmask > 0) + end + end + end - def find_mapping - unless (@mapping = @record.class.bitmasks[@attribute]) - raise ArgumentError, "Could not find mapping for bitmask attribute :#{@attribute}" + def find_mapping + unless (@mapping = @record.class.bitmasks[@attribute]) + raise ArgumentError, "Could not find mapping for bitmask attribute :#{@attribute}" + end end - end end - end From cc379ca80c7caeead2643310fa47fc0d337188d7 Mon Sep 17 00:00:00 2001 From: Joel Moss Date: Thu, 30 Jun 2011 17:47:49 +0100 Subject: [PATCH 02/14] Replaced #named_scope with #scope --- lib/bitmask_attribute.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/bitmask_attribute.rb b/lib/bitmask_attribute.rb index a1174f9..7daf887 100644 --- a/lib/bitmask_attribute.rb +++ b/lib/bitmask_attribute.rb @@ -17,7 +17,7 @@ def install_on(model) override model create_convenience_class_method_on(model) create_convenience_instance_methods_on(model) - create_named_scopes_on(model) + create_scopes_on(model) end private @@ -98,9 +98,9 @@ def #{attribute}?(*values) ) end - def create_named_scopes_on(model) + def create_scopes_on(model) model.class_eval %( - named_scope :with_#{attribute}, + scope :with_#{attribute}, proc { |*values| if values.blank? {:conditions => '#{attribute} > 0 OR #{attribute} IS NOT NULL'} @@ -112,12 +112,12 @@ def create_named_scopes_on(model) {: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" + scope :without_#{attribute}, :conditions => "#{attribute} == 0 OR #{attribute} IS NULL" + scope :no_#{attribute}, :conditions => "#{attribute} == 0 OR #{attribute} IS NULL" ) values.each do |value| model.class_eval %( - named_scope :#{attribute}_for_#{value}, + scope :#{attribute}_for_#{value}, :conditions => ['#{attribute} & ? <> 0', #{model}.bitmask_for_#{attribute}(:#{value})] ) end From ef9ab2ec455fa4bebaa3c94df3dfc4f00cc10051 Mon Sep 17 00:00:00 2001 From: Joel Moss Date: Mon, 4 Jul 2011 00:24:46 +0100 Subject: [PATCH 03/14] Finally! We have touchdown! --- .rvmrc | 1 + Gemfile | 11 ++++ Gemfile.lock | 37 +++++++++++ README.markdown => README.md | 0 Rakefile | 61 +++---------------- bitmask_attributes.gemspec | 20 ++++++ lib/bitmask-attribute.rb | 2 - lib/bitmask_attributes.rb | 26 ++++++++ .../definition.rb} | 41 ++----------- .../value_proxy.rb | 2 +- lib/bitmask_attributes/version.rb | 3 + rails/init.rb | 3 - ...ute_test.rb => bitmask_attributes_test.rb} | 41 +++++-------- test/support/helpers.rb | 17 ++++++ test/support/models.rb | 26 ++++++++ test/test_helper.rb | 61 +++---------------- 16 files changed, 181 insertions(+), 171 deletions(-) create mode 100644 .rvmrc create mode 100644 Gemfile create mode 100644 Gemfile.lock rename README.markdown => README.md (100%) create mode 100644 bitmask_attributes.gemspec delete mode 100644 lib/bitmask-attribute.rb create mode 100644 lib/bitmask_attributes.rb rename lib/{bitmask_attribute.rb => bitmask_attributes/definition.rb} (79%) rename lib/{bitmask_attribute => bitmask_attributes}/value_proxy.rb (98%) create mode 100644 lib/bitmask_attributes/version.rb delete mode 100644 rails/init.rb rename test/{bitmask_attribute_test.rb => bitmask_attributes_test.rb} (90%) create mode 100644 test/support/helpers.rb create mode 100644 test/support/models.rb diff --git a/.rvmrc b/.rvmrc new file mode 100644 index 0000000..e10b4a2 --- /dev/null +++ b/.rvmrc @@ -0,0 +1 @@ +rvm use 1.9.2@bitmask_attributes --create --install \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..a3c3420 --- /dev/null +++ b/Gemfile @@ -0,0 +1,11 @@ +source "http://rubygems.org" + +gemspec + +if RUBY_VERSION < '1.9' + gem "ruby-debug", ">= 0.10.3" +end + +gem 'shoulda' +gem 'sqlite3' +gem 'turn' \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..7c12216 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,37 @@ +PATH + remote: . + specs: + bitmask_attributes (0.1.0) + activerecord (~> 3.0) + +GEM + remote: http://rubygems.org/ + specs: + activemodel (3.0.9) + activesupport (= 3.0.9) + builder (~> 2.1.2) + i18n (~> 0.5.0) + activerecord (3.0.9) + activemodel (= 3.0.9) + activesupport (= 3.0.9) + arel (~> 2.0.10) + tzinfo (~> 0.3.23) + activesupport (3.0.9) + ansi (1.3.0) + arel (2.0.10) + builder (2.1.2) + i18n (0.5.0) + shoulda (2.11.3) + sqlite3 (1.3.3) + turn (0.8.2) + ansi (>= 1.2.2) + tzinfo (0.3.29) + +PLATFORMS + ruby + +DEPENDENCIES + bitmask_attributes! + shoulda + sqlite3 + turn diff --git a/README.markdown b/README.md similarity index 100% rename from README.markdown rename to README.md diff --git a/Rakefile b/Rakefile index 26d0807..272cdda 100644 --- a/Rakefile +++ b/Rakefile @@ -1,57 +1,16 @@ -require 'rubygems' -require 'rake' +# encoding: UTF-8 -begin - require 'jeweler' - Jeweler::Tasks.new do |gem| - gem.name = "bitmask-attribute" - gem.summary = %Q{Simple bitmask attribute support for ActiveRecord} - gem.email = "bruce@codefluency.com" - gem.homepage = "http://github.com/bruce/bitmask-attribute" - gem.authors = ["Bruce Williams"] - gem.add_dependency 'activerecord' - # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings - end - Jeweler::GemcutterTasks.new -rescue LoadError - puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com" -end +require "bundler" +Bundler::GemHelper.install_tasks require 'rake/testtask' -Rake::TestTask.new(:test) do |test| - test.libs << 'lib' << 'test' - test.pattern = 'test/**/*_test.rb' - test.verbose = true -end - -begin - require 'rcov/rcovtask' - Rcov::RcovTask.new do |test| - test.libs << 'test' - test.pattern = 'test/**/*_test.rb' - test.verbose = true - end -rescue LoadError - task :rcov do - abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov" - end -end - +desc 'Default: run unit tests.' task :default => :test -require 'rake/rdoctask' -Rake::RDocTask.new do |rdoc| - if File.exist?('VERSION.yml') - config = YAML.load(File.read('VERSION.yml')) - version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}" - else - version = "" - end - - rdoc.rdoc_dir = 'rdoc' - rdoc.title = "bitmask-attribute #{version}" - rdoc.rdoc_files.include('README*') - rdoc.rdoc_files.include('lib/**/*.rb') -end - +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.libs << 'test' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end \ No newline at end of file diff --git a/bitmask_attributes.gemspec b/bitmask_attributes.gemspec new file mode 100644 index 0000000..12fcb41 --- /dev/null +++ b/bitmask_attributes.gemspec @@ -0,0 +1,20 @@ +# -*- encoding: utf-8 -*- +$:.push File.expand_path("../lib", __FILE__) +require "bitmask_attributes/version" + +Gem::Specification.new do |gem| + gem.name = "bitmask_attributes" + gem.summary = %Q{Simple bitmask attribute support for ActiveRecord} + gem.description = %Q{Simple bitmask attribute support for ActiveRecord} + gem.email = "joel@developwithstyle.com" + gem.homepage = "http://github.com/joelmoss/bitmask_attributes" + gem.authors = ['Joel Moss'] + + gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } + gem.files = `git ls-files`.split("\n") + gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") + gem.require_paths = ['lib'] + gem.version = BitmaskAttributes::VERSION + + gem.add_dependency 'activerecord', '~> 3.0' +end \ No newline at end of file diff --git a/lib/bitmask-attribute.rb b/lib/bitmask-attribute.rb deleted file mode 100644 index da25545..0000000 --- a/lib/bitmask-attribute.rb +++ /dev/null @@ -1,2 +0,0 @@ -# Stub for dash-style requires -require File.dirname(__FILE__) << "/bitmask_attribute" \ No newline at end of file diff --git a/lib/bitmask_attributes.rb b/lib/bitmask_attributes.rb new file mode 100644 index 0000000..774d43b --- /dev/null +++ b/lib/bitmask_attributes.rb @@ -0,0 +1,26 @@ +require 'bitmask_attributes/definition' +require 'bitmask_attributes/value_proxy' + +module BitmaskAttributes + extend ActiveSupport::Concern + + 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] = 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 + +ActiveRecord::Base.send :include, BitmaskAttributes \ No newline at end of file diff --git a/lib/bitmask_attribute.rb b/lib/bitmask_attributes/definition.rb similarity index 79% rename from lib/bitmask_attribute.rb rename to lib/bitmask_attributes/definition.rb index 7daf887..8241bdd 100644 --- a/lib/bitmask_attribute.rb +++ b/lib/bitmask_attributes/definition.rb @@ -1,8 +1,5 @@ -require 'bitmask_attribute/value_proxy' - -module BitmaskAttribute +module BitmaskAttributes class Definition - attr_reader :attribute, :values, :extension def initialize(attribute, values=[], &extension) @@ -15,9 +12,9 @@ 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_scopes_on(model) + create_convenience_class_method_on model + create_convenience_instance_methods_on model + create_scopes_on model end private @@ -49,7 +46,7 @@ def override(model) def override_getter_on(model) model.class_eval %( def #{attribute} - @#{attribute} ||= BitmaskAttribute::ValueProxy.new(self, :#{attribute}, &self.class.bitmask_definitions[:#{attribute}].extension) + @#{attribute} ||= BitmaskAttributes::ValueProxy.new(self, :#{attribute}, &self.class.bitmask_definitions[:#{attribute}].extension) end ) end @@ -76,7 +73,6 @@ def self.bitmask_for_#{attribute}(*values) ) end - def create_convenience_instance_methods_on(model) values.each do |value| model.class_eval %( @@ -122,30 +118,5 @@ def create_scopes_on(model) ) 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 +end \ No newline at end of file diff --git a/lib/bitmask_attribute/value_proxy.rb b/lib/bitmask_attributes/value_proxy.rb similarity index 98% rename from lib/bitmask_attribute/value_proxy.rb rename to lib/bitmask_attributes/value_proxy.rb index 454ba77..30d9cb7 100644 --- a/lib/bitmask_attribute/value_proxy.rb +++ b/lib/bitmask_attributes/value_proxy.rb @@ -1,4 +1,4 @@ -module BitmaskAttribute +module BitmaskAttributes class ValueProxy < Array def initialize(record, attribute, &extension) diff --git a/lib/bitmask_attributes/version.rb b/lib/bitmask_attributes/version.rb new file mode 100644 index 0000000..f8d9342 --- /dev/null +++ b/lib/bitmask_attributes/version.rb @@ -0,0 +1,3 @@ +module BitmaskAttributes + VERSION = "0.1.0" +end diff --git a/rails/init.rb b/rails/init.rb deleted file mode 100644 index c2f71d7..0000000 --- a/rails/init.rb +++ /dev/null @@ -1,3 +0,0 @@ -ActiveRecord::Base.instance_eval do - include BitmaskAttribute -end \ No newline at end of file diff --git a/test/bitmask_attribute_test.rb b/test/bitmask_attributes_test.rb similarity index 90% rename from test/bitmask_attribute_test.rb rename to test/bitmask_attributes_test.rb index 3303bfc..6214abe 100644 --- a/test/bitmask_attribute_test.rb +++ b/test/bitmask_attributes_test.rb @@ -1,9 +1,8 @@ require 'test_helper' -class BitmaskAttributeTest < Test::Unit::TestCase +class BitmaskAttributesTest < ActiveSupport::TestCase context "Campaign" do - teardown do Company.destroy_all Campaign.destroy_all @@ -98,11 +97,9 @@ class BitmaskAttributeTest < Test::Unit::TestCase end context "checking" do - setup { @campaign = Campaign.new(:medium => [:web, :print]) } context "for a single value" do - should "be supported by an attribute_for_value convenience method" do assert @campaign.medium_for_web? assert @campaign.medium_for_print? @@ -114,22 +111,17 @@ class BitmaskAttributeTest < Test::Unit::TestCase assert @campaign.medium?(:print) assert !@campaign.medium?(:email) end - end context "for multiple values" do - should "be supported by the simple predicate method" do assert @campaign.medium?(:web, :print) assert !@campaign.medium?(:web, :email) end - end - end context "named scopes" do - setup do @company = Company.create(:name => "Test Co, Intl.") @campaign1 = @company.campaigns.create :medium => [:web, :print] @@ -153,7 +145,6 @@ class BitmaskAttributeTest < Test::Unit::TestCase should "support retrieval for no values" do assert_equal [@campaign2], @company.campaigns.without_medium end - end should "can check if at least one value is set" do @@ -175,7 +166,7 @@ class BitmaskAttributeTest < Test::Unit::TestCase Campaign.medium_for_print ) - assert_equal Campaign.medium_for_print, Campaign.medium_for_print.medium_for_web + assert_equal Campaign.medium_for_print.first, Campaign.medium_for_print.medium_for_web.first assert_equal [], Campaign.medium_for_email assert_equal [], Campaign.medium_for_web.medium_for_email @@ -193,24 +184,22 @@ class BitmaskAttributeTest < Test::Unit::TestCase assert_equal [campaign], Campaign.no_medium end - ####### - private - ####### - def assert_unsupported(&block) - assert_raises(ArgumentError, &block) - end + private - def assert_stored(record, *values) - values.each do |value| - assert record.medium.any? { |v| v.to_s == value.to_s }, "Values #{record.medium.inspect} does not include #{value.inspect}" + def assert_unsupported(&block) + assert_raises(ArgumentError, &block) end - full_mask = values.inject(0) do |mask, value| - mask | Campaign.bitmasks[:medium][value] + + def assert_stored(record, *values) + values.each do |value| + assert record.medium.any? { |v| v.to_s == value.to_s }, "Values #{record.medium.inspect} does not include #{value.inspect}" + end + full_mask = values.inject(0) do |mask, value| + mask | Campaign.bitmasks[:medium][value] + end + assert_equal full_mask, record.medium.to_i end - assert_equal full_mask, record.medium.to_i - end end - -end +end \ No newline at end of file diff --git a/test/support/helpers.rb b/test/support/helpers.rb new file mode 100644 index 0000000..54eb8c1 --- /dev/null +++ b/test/support/helpers.rb @@ -0,0 +1,17 @@ +class ActiveSupport::TestCase + + def assert_unsupported(&block) + assert_raises(ArgumentError, &block) + end + + def assert_stored(record, *values) + values.each do |value| + assert record.medium.any? { |v| v.to_s == value.to_s }, "Values #{record.medium.inspect} does not include #{value.inspect}" + end + full_mask = values.inject(0) do |mask, value| + mask | Campaign.bitmasks[:medium][value] + end + assert_equal full_mask, record.medium.to_i + end + +end \ No newline at end of file diff --git a/test/support/models.rb b/test/support/models.rb new file mode 100644 index 0000000..73c0fd5 --- /dev/null +++ b/test/support/models.rb @@ -0,0 +1,26 @@ +ActiveRecord::Schema.define do + create_table :campaigns do |t| + t.integer :company_id + t.integer :medium, :misc, :Legacy + end + create_table :companies do |t| + t.string :name + end +end + + +class Company < ActiveRecord::Base + has_many :campaigns +end + +# Pseudo model for testing purposes +class Campaign < ActiveRecord::Base + belongs_to :company + bitmask :medium, :as => [:web, :print, :email, :phone] + bitmask :misc, :as => %w(some useless values) do + def worked? + true + end + end + bitmask :Legacy, :as => [:upper, :case] +end \ No newline at end of file diff --git a/test/test_helper.rb b/test/test_helper.rb index e57434a..ccc54d0 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,66 +1,21 @@ -require 'rubygems' +require "rubygems" +require 'bundler/setup' + require 'test/unit' +begin; require 'turn'; rescue LoadError; end require 'shoulda' -begin - require 'redgreen' -rescue LoadError -end -require 'active_support' require 'active_record' -$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) -$LOAD_PATH.unshift(File.dirname(__FILE__)) -require 'bitmask-attribute' -require File.dirname(__FILE__) + '/../rails/init' +$:.unshift File.expand_path("../../lib", __FILE__) +require 'bitmask_attributes' -# ActiveRecord::Base.logger = Logger.new(STDOUT) ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => ':memory:' ) -ActiveRecord::Schema.define do - create_table :campaigns do |t| - t.integer :company_id - t.integer :medium, :misc, :Legacy - end - create_table :companies do |t| - t.string :name - end -end - -class Company < ActiveRecord::Base - has_many :campaigns -end - -# Pseudo model for testing purposes -class Campaign < ActiveRecord::Base - belongs_to :company - bitmask :medium, :as => [:web, :print, :email, :phone] - bitmask :misc, :as => %w(some useless values) do - def worked? - true - end - end - bitmask :Legacy, :as => [:upper, :case] -end -class Test::Unit::TestCase - - def assert_unsupported(&block) - assert_raises(ArgumentError, &block) - end - - def assert_stored(record, *values) - values.each do |value| - assert record.medium.any? { |v| v.to_s == value.to_s }, "Values #{record.medium.inspect} does not include #{value.inspect}" - end - full_mask = values.inject(0) do |mask, value| - mask | Campaign.bitmasks[:medium][value] - end - assert_equal full_mask, record.medium.to_i - end - -end +# Load support files +Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } From a1579b0bc9eafdc423b69f8a5543db99b8aad58b Mon Sep 17 00:00:00 2001 From: Joel Moss Date: Mon, 4 Jul 2011 00:33:19 +0100 Subject: [PATCH 04/14] Updated Readme and copyrights --- LICENSE | 2 +- README.md | 40 ++++++++++++++++++++++++++++------------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/LICENSE b/LICENSE index e304404..8ae5aa1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2007-2009 Bruce Williams +Copyright (c) 2007-2009 Bruce Williams & 2011 Joel Moss Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md index 699ee5b..1aa4a43 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,20 @@ -bitmask-attribute +BitmaskAttributes ================= -Transparent manipulation of bitmask attributes. +Transparent manipulation of bitmask attributes for ActiveRecord, based on +the bitmask-attribute gem, which has been dormant since 2009. This updated +gem work with Rails 3 and up (including Rails 3.1). + +Installation +------------ + +The best way to install is with RubyGems: + + $ [sudo] gem install bitmask_attributes + +Or better still, just add it to your Gemfile: + + gem 'bitmask_attributes' Example ------- @@ -89,20 +102,23 @@ IMPORTANT: Once you have data using a bitmask, don't change the order of the values, remove any values, or insert any new values in the `:as` array anywhere except at the end. You won't like the results. -Contributing and reporting issues ---------------------------------- - -Please feel free to fork & contribute fixes via GitHub pull requests. -The official repository for this project is -http://github.com/bruce/bitmask-attribute +Contributing +------------ -Issues can be reported at -http://github.com/bruce/bitmask-attribute/issues +1. Fork it. +2. Create a branch (`git checkout -b new-feature`) +3. Make your changes +4. Run the tests (`bundle install` then `bundle exec rake`) +5. Commit your changes (`git commit -am "Created new feature"`) +6. Push to the branch (`git push origin new-feature`) +7. Create a [Pull Request](http://help.github.com/pull-requests/) from your branch. +8. Promote it. Get others to drop in and +1 it. Credits ------- -Thanks to the following contributors: +Thanks to [Bruce Williams](https://github.com/bruce) and the following contributors +of the bitmask-attribute plugin: * [Jason L Perry](http://github.com/ambethia) * [Nicolas Fouché](http://github.com/nfo) @@ -110,4 +126,4 @@ Thanks to the following contributors: Copyright --------- -Copyright (c) 2007-2009 Bruce Williams. See LICENSE for details. +Copyright (c) 2007-2009 Bruce Williams & 2011 Joel Moss. See LICENSE for details. From 950ea5b435c5ad71e75ebd96b701516d30a1da0f Mon Sep 17 00:00:00 2001 From: Joel Moss Date: Tue, 5 Jul 2011 10:32:24 +0100 Subject: [PATCH 05/14] Using single equals, instead of double --- lib/bitmask_attributes/definition.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/bitmask_attributes/definition.rb b/lib/bitmask_attributes/definition.rb index 8241bdd..0a3c6a4 100644 --- a/lib/bitmask_attributes/definition.rb +++ b/lib/bitmask_attributes/definition.rb @@ -108,8 +108,8 @@ def create_scopes_on(model) {:conditions => sets.join(' AND ')} end } - scope :without_#{attribute}, :conditions => "#{attribute} == 0 OR #{attribute} IS NULL" - scope :no_#{attribute}, :conditions => "#{attribute} == 0 OR #{attribute} IS NULL" + scope :without_#{attribute}, :conditions => "#{attribute} = 0 OR #{attribute} IS NULL" + scope :no_#{attribute}, :conditions => "#{attribute} = 0 OR #{attribute} IS NULL" ) values.each do |value| model.class_eval %( From 256bc6e1015fd7a59cb89a77c73ae6c2c1d09191 Mon Sep 17 00:00:00 2001 From: Joel Moss Date: Tue, 5 Jul 2011 10:39:48 +0100 Subject: [PATCH 06/14] Bumped to 0.1.1 --- lib/bitmask_attributes/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/bitmask_attributes/version.rb b/lib/bitmask_attributes/version.rb index f8d9342..2966bcb 100644 --- a/lib/bitmask_attributes/version.rb +++ b/lib/bitmask_attributes/version.rb @@ -1,3 +1,3 @@ module BitmaskAttributes - VERSION = "0.1.0" + VERSION = "0.1.1" end From d0a45c7096bdf1e7e5a136fd399325c45639de80 Mon Sep 17 00:00:00 2001 From: Joel Moss Date: Mon, 25 Jul 2011 14:27:31 +0100 Subject: [PATCH 07/14] Added support for OR conditions within the new #with_any_* named scope --- Gemfile.lock | 2 +- README.md | 7 ++----- VERSION | 1 - lib/bitmask_attributes/definition.rb | 15 ++++++++++++++- test/bitmask_attributes_test.rb | 6 ++++-- 5 files changed, 21 insertions(+), 10 deletions(-) delete mode 100644 VERSION diff --git a/Gemfile.lock b/Gemfile.lock index 7c12216..0d552a3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - bitmask_attributes (0.1.0) + bitmask_attributes (0.1.1) activerecord (~> 3.0) GEM diff --git a/README.md b/README.md index 1aa4a43..5a0ed7b 100644 --- a/README.md +++ b/README.md @@ -65,11 +65,8 @@ A couple useful named scopes are also generated when you use # => (all editors) User.with_roles(:editor, :writer) # => (all users who are BOTH editors and writers) - -Later we'll support an `or` boolean; for now, do something like: - - User.with_roles(:editor) + User.with_roles(:writer) - # => (all users who are EITHER editors and writers) + User.with_any_roles(:editor, :writer) + # => (all users who are editors OR writers) Find records without any bitmask set: diff --git a/VERSION b/VERSION deleted file mode 100644 index 9084fa2..0000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.1.0 diff --git a/lib/bitmask_attributes/definition.rb b/lib/bitmask_attributes/definition.rb index 0a3c6a4..f81ccc4 100644 --- a/lib/bitmask_attributes/definition.rb +++ b/lib/bitmask_attributes/definition.rb @@ -110,11 +110,24 @@ def create_scopes_on(model) } scope :without_#{attribute}, :conditions => "#{attribute} = 0 OR #{attribute} IS NULL" scope :no_#{attribute}, :conditions => "#{attribute} = 0 OR #{attribute} IS NULL" + + scope :with_any_#{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(' OR ')} + end + } ) values.each do |value| model.class_eval %( scope :#{attribute}_for_#{value}, - :conditions => ['#{attribute} & ? <> 0', #{model}.bitmask_for_#{attribute}(:#{value})] + :conditions => ['#{attribute} & ? <> 0', #{model}.bitmask_for_#{attribute}(:#{value})] ) end end diff --git a/test/bitmask_attributes_test.rb b/test/bitmask_attributes_test.rb index 6214abe..02dae7d 100644 --- a/test/bitmask_attributes_test.rb +++ b/test/bitmask_attributes_test.rb @@ -137,6 +137,10 @@ class BitmaskAttributesTest < ActiveSupport::TestCase assert_equal [@campaign1], @company.campaigns.with_medium(:print) end + should "support retrieval by any matching value (OR)" do + assert_equal [@campaign1, @campaign3], @company.campaigns.with_any_medium(:print, :email) + end + should "support retrieval by all matching values" do assert_equal [@campaign1], @company.campaigns.with_medium(:web, :print) assert_equal [@campaign3], @company.campaigns.with_medium(:web, :email) @@ -149,11 +153,9 @@ class BitmaskAttributesTest < ActiveSupport::TestCase should "can check if at least one value is set" do campaign = Campaign.new(:medium => [:web, :print]) - assert campaign.medium? campaign = Campaign.new - assert !campaign.medium? end From 1f108cd25dcffa287fef00c84b32dcfba96f839e Mon Sep 17 00:00:00 2001 From: Joel Moss Date: Mon, 25 Jul 2011 14:28:04 +0100 Subject: [PATCH 08/14] Bumped version to 0.2.1 --- lib/bitmask_attributes/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/bitmask_attributes/version.rb b/lib/bitmask_attributes/version.rb index 2966bcb..c0fad4a 100644 --- a/lib/bitmask_attributes/version.rb +++ b/lib/bitmask_attributes/version.rb @@ -1,3 +1,3 @@ module BitmaskAttributes - VERSION = "0.1.1" + VERSION = "0.2.1" end From f16f1c0513adfe3c7dc875a1542561ba93bfaed9 Mon Sep 17 00:00:00 2001 From: Joel Moss Date: Mon, 25 Jul 2011 14:33:49 +0100 Subject: [PATCH 09/14] Requiring psych --- Rakefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Rakefile b/Rakefile index 272cdda..a7d91eb 100644 --- a/Rakefile +++ b/Rakefile @@ -1,5 +1,6 @@ # encoding: UTF-8 +require 'psych' require "bundler" Bundler::GemHelper.install_tasks From 00afb0059f4c26e889d3cdd7342c37f9ed7d3cc2 Mon Sep 17 00:00:00 2001 From: Joel Moss Date: Mon, 25 Jul 2011 14:34:17 +0100 Subject: [PATCH 10/14] Removed psych --- Rakefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Rakefile b/Rakefile index a7d91eb..272cdda 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,5 @@ # encoding: UTF-8 -require 'psych' require "bundler" Bundler::GemHelper.install_tasks From 27684f66f885faba9931ff6e82979c854dbf8841 Mon Sep 17 00:00:00 2001 From: Joel Moss Date: Mon, 25 Jul 2011 14:35:20 +0100 Subject: [PATCH 11/14] Ignoring *.gem files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 09a4164..4b25654 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ rdoc pkg \#* .#* +*.gem \ No newline at end of file From 3281ce7def68b504ab452e868615f6e65f84ae57 Mon Sep 17 00:00:00 2001 From: Joel Moss Date: Thu, 4 Aug 2011 19:51:08 +0100 Subject: [PATCH 12/14] Added Model#values_for_attribute method which returns values array for the given attribute --- Gemfile.lock | 2 +- README.md | 5 +++++ lib/bitmask_attributes.rb | 2 +- lib/bitmask_attributes/definition.rb | 10 ++++++++++ test/bitmask_attributes_test.rb | 4 ++++ 5 files changed, 21 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 0d552a3..9cdeaea 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - bitmask_attributes (0.1.1) + bitmask_attributes (0.2.1) activerecord (~> 3.0) GEM diff --git a/README.md b/README.md index 5a0ed7b..414bd0f 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,11 @@ Or, just check if any values are present: user.roles? # => true +You can get the list of values for any given attribute: + + User.values_for_roles + # => [:writer, :publisher, :editor, :proofreader] + Named Scopes ------------ diff --git a/lib/bitmask_attributes.rb b/lib/bitmask_attributes.rb index 774d43b..6e0cbfb 100644 --- a/lib/bitmask_attributes.rb +++ b/lib/bitmask_attributes.rb @@ -19,7 +19,7 @@ def bitmask_definitions def bitmasks @bitmasks ||= {} - end + end end end diff --git a/lib/bitmask_attributes/definition.rb b/lib/bitmask_attributes/definition.rb index f81ccc4..d0870f8 100644 --- a/lib/bitmask_attributes/definition.rb +++ b/lib/bitmask_attributes/definition.rb @@ -15,6 +15,7 @@ def install_on(model) create_convenience_class_method_on model create_convenience_instance_methods_on model create_scopes_on model + create_attribute_methods_on model end private @@ -60,6 +61,15 @@ def #{attribute}=(raw_value) ) end + # Returns the defined values as an Array. + def create_attribute_methods_on(model) + model.class_eval %( + def self.values_for_#{attribute} # def self.values_for_numbers + #{values} # [:one, :two, :three] + end # end + ) + end + def create_convenience_class_method_on(model) model.class_eval %( def self.bitmask_for_#{attribute}(*values) diff --git a/test/bitmask_attributes_test.rb b/test/bitmask_attributes_test.rb index 02dae7d..fad2a8c 100644 --- a/test/bitmask_attributes_test.rb +++ b/test/bitmask_attributes_test.rb @@ -8,6 +8,10 @@ class BitmaskAttributesTest < ActiveSupport::TestCase Campaign.destroy_all end + should "return all defined values of a given bitmask attribute" do + assert_equal Campaign.values_for_medium, [:web, :print, :email, :phone] + end + should "can assign single value to bitmask" do assert_stored Campaign.new(:medium => :web), :web end From 777e9038952d677af4b86078b0325939feeba90d Mon Sep 17 00:00:00 2001 From: Joel Moss Date: Thu, 4 Aug 2011 19:56:07 +0100 Subject: [PATCH 13/14] Bumped version to 0.2.2 --- lib/bitmask_attributes/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/bitmask_attributes/version.rb b/lib/bitmask_attributes/version.rb index c0fad4a..c01b03c 100644 --- a/lib/bitmask_attributes/version.rb +++ b/lib/bitmask_attributes/version.rb @@ -1,3 +1,3 @@ module BitmaskAttributes - VERSION = "0.2.1" + VERSION = "0.2.2" end From 088a9fe8dae66a0097fb66ce36401cfd247a3768 Mon Sep 17 00:00:00 2001 From: Mathieu Arnold Date: Thu, 13 Oct 2011 19:00:58 +0200 Subject: [PATCH 14/14] Make sure our array only contain symbols. It makes bitmasks easier to use in Rails applications because you don't have to wonder if you did cast what you got from your web forms. --- lib/bitmask_attributes/value_proxy.rb | 6 ++++++ test/bitmask_attributes_test.rb | 3 +++ 2 files changed, 9 insertions(+) diff --git a/lib/bitmask_attributes/value_proxy.rb b/lib/bitmask_attributes/value_proxy.rb index 30d9cb7..ea7d691 100644 --- a/lib/bitmask_attributes/value_proxy.rb +++ b/lib/bitmask_attributes/value_proxy.rb @@ -13,6 +13,7 @@ def initialize(record, attribute, &extension) # = OVERRIDE TO SERIALIZE = # ========================= + alias_method :orig_replace, :replace %w(push << delete replace reject! select!).each do |override| class_eval(<<-EOEVAL) def #{override}(*args) @@ -39,8 +40,13 @@ def validate! end end + def symbolize! + orig_replace(map(&:to_sym)) + end + def updated! validate! + symbolize! uniq! serialize! end diff --git a/test/bitmask_attributes_test.rb b/test/bitmask_attributes_test.rb index fad2a8c..1545645 100644 --- a/test/bitmask_attributes_test.rb +++ b/test/bitmask_attributes_test.rb @@ -34,7 +34,10 @@ class BitmaskAttributesTest < ActiveSupport::TestCase assert_stored campaign, :web, :print, :phone campaign.medium << :phone assert_stored campaign, :web, :print, :phone + campaign.medium << "phone" + assert_stored campaign, :web, :print, :phone assert_equal 1, campaign.medium.select { |value| value == :phone }.size + assert_equal 0, campaign.medium.select { |value| value == "phone" }.size end should "can assign new values at once to bitmask" do