bitmask-attribute 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.markdown CHANGED
@@ -1,14 +1,16 @@
1
- # bitmask-attribute
1
+ bitmask-attribute
2
+ =================
2
3
 
3
4
  Transparent manipulation of bitmask attributes.
4
5
 
5
- ## Example
6
+ Example
7
+ -------
6
8
 
7
9
  Simply declare an existing integer column as a bitmask with its possible
8
10
  values.
9
11
 
10
12
  class User < ActiveRecord::Base
11
- bitmask :roles, :as => [:writer, :publisher, :editor]
13
+ bitmask :roles, :as => [:writer, :publisher, :editor, :proofreader]
12
14
  end
13
15
 
14
16
  You can then modify the column using the declared values without resorting
@@ -21,27 +23,91 @@ to manual bitmasks.
21
23
  user.roles
22
24
  # => [:publisher, :editor, :writer]
23
25
 
24
- For the moment, querying for bitmasks is left as an exercise to the reader,
25
- but here's how to grab the bitmask for a specific possible value for use in
26
- your SQL query:
26
+ It's easy to find out if a record has a given value:
27
27
 
28
- bitmask = User.bitmasks[:roles][:editor]
29
- # Use `bitmask` as needed
28
+ user.roles?(:editor)
29
+ # => true
30
+
31
+ You can check for multiple values (uses an `and` boolean):
32
+
33
+ user.roles?(:editor, :publisher)
34
+ # => true
35
+ user.roles?(:editor, :proofreader)
36
+ # => false
37
+
38
+ Or, just check if any values are present:
39
+
40
+ user.roles?
41
+ # => true
42
+
43
+ Named Scopes
44
+ ------------
45
+
46
+ A couple useful named scopes are also generated when you use
47
+ `bitmask`:
48
+
49
+ User.with_roles
50
+ # => (all users with roles)
51
+ User.with_roles(:editor)
52
+ # => (all editors)
53
+ User.with_roles(:editor, :writer)
54
+ # => (all users who are BOTH editors and writers)
55
+
56
+ Later we'll support an `or` boolean; for now, do something like:
57
+
58
+ User.with_roles(:editor) + User.with_roles(:writer)
59
+ # => (all users who are EITHER editors and writers)
60
+
61
+ Find records without any bitmask set:
62
+
63
+ User.without_roles
64
+ # => (all users without a role)
30
65
 
31
- ## Modifying possible values
66
+ Later we'll support finding records without a specific bitmask.
32
67
 
33
- Once you have data using a bitmask, don't change the order of the values,
34
- remove any values, or insert any new values in the array anywhere except at
35
- the end.
68
+ Adding Methods
69
+ --------------
36
70
 
37
- ## Contributing and reporting issues
71
+ You can add your own methods to the bitmasked attributes (similar to
72
+ named scopes):
73
+
74
+ bitmask :other_attribute, :as => [:value1, :value2] do
75
+ def worked?
76
+ true
77
+ end
78
+ end
79
+
80
+ user = User.first
81
+ user.other_attribute.worked?
82
+ # => true
83
+
84
+
85
+ Warning: Modifying possible values
86
+ ----------------------------------
87
+
88
+ IMPORTANT: Once you have data using a bitmask, don't change the order
89
+ of the values, remove any values, or insert any new values in the `:as`
90
+ array anywhere except at the end. You won't like the results.
91
+
92
+ Contributing and reporting issues
93
+ ---------------------------------
38
94
 
39
95
  Please feel free to fork & contribute fixes via GitHub pull requests.
40
96
  The official repository for this project is
41
97
  http://github.com/bruce/bitmask-attribute
42
98
 
43
- Issues can be reported at http://github.com/bruce/bitmask-attribute/issues
99
+ Issues can be reported at
100
+ http://github.com/bruce/bitmask-attribute/issues
101
+
102
+ Credits
103
+ -------
104
+
105
+ Thanks to the following contributors:
106
+
107
+ * [Jason L Perry](http://github.com/ambethia)
108
+ * [Nicolas Fouché](http://github.com/nfo)
44
109
 
45
- ## Copyright
110
+ Copyright
111
+ ---------
46
112
 
47
113
  Copyright (c) 2007-2009 Bruce Williams. See LICENSE for details.
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.1.0
@@ -15,7 +15,9 @@ module BitmaskAttribute
15
15
  validate_for model
16
16
  generate_bitmasks_on model
17
17
  override model
18
- create_convenience_method_on model
18
+ create_convenience_class_method_on(model)
19
+ create_convenience_instance_methods_on(model)
20
+ create_named_scopes_on(model)
19
21
  end
20
22
 
21
23
  #######
@@ -23,8 +25,13 @@ module BitmaskAttribute
23
25
  #######
24
26
 
25
27
  def validate_for(model)
26
- unless model.columns.detect { |col| col.name == attribute.to_s && col.type == :integer }
27
- raise ArgumentError, "`#{attribute}' is not an integer column of `#{model}'"
28
+ # The model cannot be validated if it is preloaded and the attribute/column is not in the
29
+ # database (the migration has not been run). This usually
30
+ # occurs in the 'test' and 'production' environments.
31
+ return if defined?(Rails) && Rails.configuration.cache_classes
32
+
33
+ unless model.columns.detect { |col| col.name == attribute.to_s }
34
+ raise ArgumentError, "`#{attribute}' is not an attribute of `#{model}'"
28
35
  end
29
36
  end
30
37
 
@@ -53,12 +60,12 @@ module BitmaskAttribute
53
60
  model.class_eval %(
54
61
  def #{attribute}=(raw_value)
55
62
  values = raw_value.kind_of?(Array) ? raw_value : [raw_value]
56
- #{attribute}.replace(values)
63
+ self.#{attribute}.replace(values.reject(&:blank?))
57
64
  end
58
65
  )
59
66
  end
60
67
 
61
- def create_convenience_method_on(model)
68
+ def create_convenience_class_method_on(model)
62
69
  model.class_eval %(
63
70
  def self.bitmask_for_#{attribute}(*values)
64
71
  values.inject(0) do |bitmask, value|
@@ -70,6 +77,53 @@ module BitmaskAttribute
70
77
  end
71
78
  )
72
79
  end
80
+
81
+
82
+ def create_convenience_instance_methods_on(model)
83
+ values.each do |value|
84
+ model.class_eval %(
85
+ def #{attribute}_for_#{value}?
86
+ self.#{attribute}?(:#{value})
87
+ end
88
+ )
89
+ end
90
+ model.class_eval %(
91
+ def #{attribute}?(*values)
92
+ if !values.blank?
93
+ values.all? do |value|
94
+ self.#{attribute}.include?(value)
95
+ end
96
+ else
97
+ self.#{attribute}.present?
98
+ end
99
+ end
100
+ )
101
+ end
102
+
103
+ def create_named_scopes_on(model)
104
+ model.class_eval %(
105
+ named_scope :with_#{attribute},
106
+ proc { |*values|
107
+ if values.blank?
108
+ {:conditions => '#{attribute} > 0 OR #{attribute} IS NOT NULL'}
109
+ else
110
+ sets = values.map do |value|
111
+ mask = #{model}.bitmask_for_#{attribute}(value)
112
+ "#{attribute} & \#{mask} <> 0"
113
+ end
114
+ {:conditions => sets.join(' AND ')}
115
+ end
116
+ }
117
+ named_scope :without_#{attribute}, :conditions => "#{attribute} == 0 OR #{attribute} IS NULL"
118
+ named_scope :no_#{attribute}, :conditions => "#{attribute} == 0 OR #{attribute} IS NULL"
119
+ )
120
+ values.each do |value|
121
+ model.class_eval %(
122
+ named_scope :#{attribute}_for_#{value},
123
+ :conditions => ['#{attribute} & ? <> 0', #{model}.bitmask_for_#{attribute}(:#{value})]
124
+ )
125
+ end
126
+ end
73
127
 
74
128
  end
75
129
 
@@ -97,4 +151,4 @@ module BitmaskAttribute
97
151
 
98
152
  end
99
153
 
100
- end
154
+ end
@@ -33,7 +33,7 @@ module BitmaskAttribute
33
33
  #######
34
34
 
35
35
  def validate!
36
- each do |value|
36
+ each do |value|
37
37
  if @mapping.key? value
38
38
  true
39
39
  else
@@ -53,7 +53,7 @@ module BitmaskAttribute
53
53
  end
54
54
 
55
55
  def extract_values
56
- stored = @record.send(:read_attribute, @attribute) || 0
56
+ stored = [@record.send(:read_attribute, @attribute) || 0, 0].max
57
57
  @mapping.inject([]) do |values, (value, bitmask)|
58
58
  returning values do
59
59
  values << value.to_sym if (stored & bitmask > 0)
@@ -4,6 +4,11 @@ class BitmaskAttributeTest < Test::Unit::TestCase
4
4
 
5
5
  context "Campaign" do
6
6
 
7
+ teardown do
8
+ Company.destroy_all
9
+ Campaign.destroy_all
10
+ end
11
+
7
12
  should "can assign single value to bitmask" do
8
13
  assert_stored Campaign.new(:medium => :web), :web
9
14
  end
@@ -68,7 +73,7 @@ class BitmaskAttributeTest < Test::Unit::TestCase
68
73
  Campaign.bitmask_for_medium(:web, :print)
69
74
  )
70
75
  end
71
-
76
+
72
77
  should "assert use of unknown value in convenience method will result in exception" do
73
78
  assert_unsupported { Campaign.bitmask_for_medium(:web, :and_this_isnt_valid) }
74
79
  end
@@ -81,6 +86,113 @@ class BitmaskAttributeTest < Test::Unit::TestCase
81
86
  assert_equal Campaign.bitmask_for_medium(:web, :print), string_bit
82
87
  end
83
88
 
89
+ should "save bitmask with non-standard attribute names" do
90
+ campaign = Campaign.new(:Legacy => [:upper, :case])
91
+ assert campaign.save
92
+ assert_equal [:upper, :case], Campaign.find(campaign.id).Legacy
93
+ end
94
+
95
+ should "ignore blanks fed as values" do
96
+ campaign = Campaign.new(:medium => [:web, :print, ''])
97
+ assert_stored campaign, :web, :print
98
+ end
99
+
100
+ context "checking" do
101
+
102
+ setup { @campaign = Campaign.new(:medium => [:web, :print]) }
103
+
104
+ context "for a single value" do
105
+
106
+ should "be supported by an attribute_for_value convenience method" do
107
+ assert @campaign.medium_for_web?
108
+ assert @campaign.medium_for_print?
109
+ assert !@campaign.medium_for_email?
110
+ end
111
+
112
+ should "be supported by the simple predicate method" do
113
+ assert @campaign.medium?(:web)
114
+ assert @campaign.medium?(:print)
115
+ assert !@campaign.medium?(:email)
116
+ end
117
+
118
+ end
119
+
120
+ context "for multiple values" do
121
+
122
+ should "be supported by the simple predicate method" do
123
+ assert @campaign.medium?(:web, :print)
124
+ assert !@campaign.medium?(:web, :email)
125
+ end
126
+
127
+ end
128
+
129
+ end
130
+
131
+ context "named scopes" do
132
+
133
+ setup do
134
+ @company = Company.create(:name => "Test Co, Intl.")
135
+ @campaign1 = @company.campaigns.create :medium => [:web, :print]
136
+ @campaign2 = @company.campaigns.create
137
+ @campaign3 = @company.campaigns.create :medium => [:web, :email]
138
+ end
139
+
140
+ should "support retrieval by any value" do
141
+ assert_equal [@campaign1, @campaign3], @company.campaigns.with_medium
142
+ end
143
+
144
+ should "support retrieval by one matching value" do
145
+ assert_equal [@campaign1], @company.campaigns.with_medium(:print)
146
+ end
147
+
148
+ should "support retrieval by all matching values" do
149
+ assert_equal [@campaign1], @company.campaigns.with_medium(:web, :print)
150
+ assert_equal [@campaign3], @company.campaigns.with_medium(:web, :email)
151
+ end
152
+
153
+ should "support retrieval for no values" do
154
+ assert_equal [@campaign2], @company.campaigns.without_medium
155
+ end
156
+
157
+ end
158
+
159
+ should "can check if at least one value is set" do
160
+ campaign = Campaign.new(:medium => [:web, :print])
161
+
162
+ assert campaign.medium?
163
+
164
+ campaign = Campaign.new
165
+
166
+ assert !campaign.medium?
167
+ end
168
+
169
+ should "find by bitmask values" do
170
+ campaign = Campaign.new(:medium => [:web, :print])
171
+ assert campaign.save
172
+
173
+ assert_equal(
174
+ Campaign.find(:all, :conditions => ['medium & ? <> 0', Campaign.bitmask_for_medium(:print)]),
175
+ Campaign.medium_for_print
176
+ )
177
+
178
+ assert_equal Campaign.medium_for_print, Campaign.medium_for_print.medium_for_web
179
+
180
+ assert_equal [], Campaign.medium_for_email
181
+ assert_equal [], Campaign.medium_for_web.medium_for_email
182
+ end
183
+
184
+ should "find no values" do
185
+ campaign = Campaign.create(:medium => [:web, :print])
186
+ assert campaign.save
187
+
188
+ assert_equal [], Campaign.no_medium
189
+
190
+ campaign.medium = []
191
+ assert campaign.save
192
+
193
+ assert_equal [campaign], Campaign.no_medium
194
+ end
195
+
84
196
  #######
85
197
  private
86
198
  #######
data/test/test_helper.rb CHANGED
@@ -14,26 +14,37 @@ $LOAD_PATH.unshift(File.dirname(__FILE__))
14
14
  require 'bitmask-attribute'
15
15
  require File.dirname(__FILE__) + '/../rails/init'
16
16
 
17
+ # ActiveRecord::Base.logger = Logger.new(STDOUT)
18
+
17
19
  ActiveRecord::Base.establish_connection(
18
20
  :adapter => 'sqlite3',
19
21
  :database => ':memory:'
20
22
  )
21
23
 
22
24
  ActiveRecord::Schema.define do
23
- create_table :campaigns do |table|
24
- table.column :medium, :integer
25
- table.column :misc, :integer
25
+ create_table :campaigns do |t|
26
+ t.integer :company_id
27
+ t.integer :medium, :misc, :Legacy
28
+ end
29
+ create_table :companies do |t|
30
+ t.string :name
26
31
  end
27
32
  end
28
33
 
34
+ class Company < ActiveRecord::Base
35
+ has_many :campaigns
36
+ end
37
+
29
38
  # Pseudo model for testing purposes
30
39
  class Campaign < ActiveRecord::Base
40
+ belongs_to :company
31
41
  bitmask :medium, :as => [:web, :print, :email, :phone]
32
42
  bitmask :misc, :as => %w(some useless values) do
33
43
  def worked?
34
44
  true
35
45
  end
36
46
  end
47
+ bitmask :Legacy, :as => [:upper, :case]
37
48
  end
38
49
 
39
50
  class Test::Unit::TestCase
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bitmask-attribute
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bruce Williams
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-12-14 00:00:00 -08:00
12
+ date: 2009-12-15 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -37,8 +37,7 @@ files:
37
37
  - LICENSE
38
38
  - README.markdown
39
39
  - Rakefile
40
- - VERSION.yml
41
- - bitmask-attribute.gemspec
40
+ - VERSION
42
41
  - lib/bitmask-attribute.rb
43
42
  - lib/bitmask_attribute.rb
44
43
  - lib/bitmask_attribute/value_proxy.rb
data/VERSION.yml DELETED
@@ -1,4 +0,0 @@
1
- ---
2
- :major: 1
3
- :minor: 0
4
- :patch: 0
@@ -1,50 +0,0 @@
1
- # -*- encoding: utf-8 -*-
2
-
3
- Gem::Specification.new do |s|
4
- s.name = %q{bitmask-attribute}
5
- s.version = "1.0.0"
6
-
7
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
- s.authors = ["Bruce Williams"]
9
- s.date = %q{2009-05-18}
10
- s.email = %q{bruce@codefluency.com}
11
- s.extra_rdoc_files = [
12
- "LICENSE",
13
- "README.markdown"
14
- ]
15
- s.files = [
16
- "LICENSE",
17
- "README.markdown",
18
- "Rakefile",
19
- "VERSION.yml",
20
- "lib/bitmask-attribute.rb",
21
- "lib/bitmask_attribute.rb",
22
- "lib/bitmask_attribute/value_proxy.rb",
23
- "rails/init.rb",
24
- "test/bitmask_attribute_test.rb",
25
- "test/test_helper.rb"
26
- ]
27
- s.has_rdoc = true
28
- s.homepage = %q{http://github.com/bruce/bitmask-attribute}
29
- s.rdoc_options = ["--charset=UTF-8"]
30
- s.require_paths = ["lib"]
31
- s.rubygems_version = %q{1.3.2}
32
- s.summary = %q{Simple bitmask attribute support for ActiveRecord}
33
- s.test_files = [
34
- "test/bitmask_attribute_test.rb",
35
- "test/test_helper.rb"
36
- ]
37
-
38
- if s.respond_to? :specification_version then
39
- current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
40
- s.specification_version = 3
41
-
42
- if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
43
- s.add_runtime_dependency(%q<activerecord>, [">= 0"])
44
- else
45
- s.add_dependency(%q<activerecord>, [">= 0"])
46
- end
47
- else
48
- s.add_dependency(%q<activerecord>, [">= 0"])
49
- end
50
- end