bitmask-attribute 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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