clean-bitmask-attribute 2.0.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.
@@ -0,0 +1,162 @@
1
+ require 'bitmask_attribute/value_proxy'
2
+ require 'bitmask_attribute/attribute'
3
+ require 'bitmask_attribute/core_ext/hash_with_indifferent_access' unless defined?(HashWithIndifferentAccess)
4
+ require 'bitmask_attribute/core_ext/returning' unless Object.respond_to?(:returning)
5
+ require 'bitmask_attribute/core_ext/blank' unless Object.respond_to?(:blank?)
6
+
7
+ module BitmaskAttribute
8
+
9
+ class Definition
10
+
11
+ attr_reader :attribute, :values, :extension
12
+ def initialize(attribute, values=[], &extension)
13
+ @attribute = attribute
14
+ @values = values
15
+ @extension = extension
16
+ end
17
+
18
+ def install_on(model)
19
+ validate_for model
20
+ generate_bitmasks_on model
21
+ override model
22
+ create_convenience_class_method_on(model)
23
+ create_convenience_instance_methods_on(model)
24
+ create_named_scopes_on(model) if defined?(ActiveRecord::Base) && model.respond_to?(scope_method)
25
+ end
26
+
27
+ #######
28
+ private
29
+ #######
30
+
31
+ def validate_for(model)
32
+ unless (model.columns.detect { |col| col.name == attribute.to_s } rescue true)
33
+ raise ArgumentError, "`#{attribute}' is not an attribute of `#{model}'"
34
+ end
35
+ end
36
+
37
+ def generate_bitmasks_on(model)
38
+ model.bitmasks[attribute] = returning HashWithIndifferentAccess.new do |mapping|
39
+ values.each_with_index do |value, index|
40
+ mapping[value] = 0b1 << index
41
+ end
42
+ end
43
+ end
44
+
45
+ def override(model)
46
+ override_getter_on(model)
47
+ override_setter_on(model)
48
+ end
49
+
50
+ def override_getter_on(model)
51
+ model.class_eval %(
52
+ def #{attribute}
53
+ @#{attribute} ||= BitmaskAttribute::ValueProxy.new(self, :#{attribute}, &self.class.bitmask_definitions[:#{attribute}].extension)
54
+ end
55
+ )
56
+ end
57
+
58
+ def override_setter_on(model)
59
+ model.class_eval %(
60
+ def #{attribute}=(raw_value)
61
+ values = raw_value.kind_of?(Array) ? raw_value : [raw_value]
62
+ self.#{attribute}.replace(values.reject(&:blank?))
63
+ end
64
+ )
65
+ end
66
+
67
+ def create_convenience_class_method_on(model)
68
+ model.class_eval %(
69
+ def self.bitmask_for_#{attribute}(*values)
70
+ values.inject(0) do |bitmask, value|
71
+ unless (bit = bitmasks[:#{attribute}][value])
72
+ raise ArgumentError, "Unsupported value for #{attribute}: \#{value.inspect}"
73
+ end
74
+ bitmask | bit
75
+ end
76
+ end
77
+ )
78
+ end
79
+
80
+
81
+ def create_convenience_instance_methods_on(model)
82
+ values.each do |value|
83
+ model.class_eval %(
84
+ def #{attribute}_for_#{value}?
85
+ self.#{attribute}?(:#{value})
86
+ end
87
+ )
88
+ end
89
+ model.class_eval %(
90
+ def #{attribute}?(*values)
91
+ if !values.blank?
92
+ values.all? do |value|
93
+ self.#{attribute}.include?(value.to_sym)
94
+ end
95
+ else
96
+ self.#{attribute}.present?
97
+ end
98
+ end
99
+ )
100
+ end
101
+
102
+ def scope_method
103
+ ActiveRecord::VERSION::STRING >= "3" ? :scope : :named_scope
104
+ end
105
+
106
+ def create_named_scopes_on(model)
107
+ model.class_eval %(
108
+ #{scope_method} :with_#{attribute},
109
+ proc { |*values|
110
+ if values.blank?
111
+ {:conditions => '#{attribute} > 0 OR #{attribute} IS NOT NULL'}
112
+ else
113
+ sets = values.map do |value|
114
+ mask = #{model}.bitmask_for_#{attribute}(value)
115
+ "#{attribute} & \#{mask} <> 0"
116
+ end
117
+ {:conditions => sets.join(' AND ')}
118
+ end
119
+ }
120
+ #{scope_method} :without_#{attribute}, :conditions => "#{attribute} == 0 OR #{attribute} IS NULL"
121
+ #{scope_method} :no_#{attribute}, :conditions => "#{attribute} == 0 OR #{attribute} IS NULL"
122
+ )
123
+ values.each do |value|
124
+ model.class_eval %(
125
+ #{scope_method} :#{attribute}_for_#{value},
126
+ :conditions => ['#{attribute} & ? <> 0', #{model}.bitmask_for_#{attribute}(:#{value})]
127
+ )
128
+ end
129
+ end
130
+
131
+ end
132
+
133
+ def self.included(model)
134
+ model.extend ClassMethods
135
+ # Include basic attributes support for clean class
136
+ # TODO improve attributes detection
137
+ model.send(:include, BitmaskAttribute::Attribute) unless model.included_modules.detect{|m| m.to_s.include? 'AttributeMethods'}
138
+ end
139
+
140
+ module ClassMethods
141
+
142
+ def bitmask(attribute, options={}, &extension)
143
+ unless options[:as] && options[:as].kind_of?(Array)
144
+ raise ArgumentError, "Must provide an Array :as option"
145
+ end
146
+ bitmask_definitions[attribute] = BitmaskAttribute::Definition.new(attribute, options[:as].to_a, &extension)
147
+ bitmask_definitions[attribute].install_on(self)
148
+ end
149
+
150
+ def bitmask_definitions
151
+ @bitmask_definitions ||= {}
152
+ end
153
+
154
+ def bitmasks
155
+ @bitmasks ||= {}
156
+ end
157
+
158
+ end
159
+
160
+ end
161
+
162
+ ActiveRecord::Base.send :include, BitmaskAttribute if defined? ActiveRecord::Base
data/rails/init.rb ADDED
@@ -0,0 +1,3 @@
1
+ ActiveRecord::Base.instance_eval do
2
+ include BitmaskAttribute
3
+ end
@@ -0,0 +1,223 @@
1
+ require 'test_helper'
2
+
3
+ class BitmaskAttributeTest < Test::Unit::TestCase
4
+
5
+ context "Campaign" do
6
+
7
+ teardown do
8
+ Company.destroy_all
9
+ Campaign.destroy_all
10
+ end
11
+
12
+ should "can assign single value to bitmask" do
13
+ assert_stored Campaign.new(:medium => :web), :web
14
+ end
15
+
16
+ should "can assign multiple values to bitmask" do
17
+ assert_stored Campaign.new(:medium => [:web, :print]), :web, :print
18
+ end
19
+
20
+ should "can add single value to bitmask" do
21
+ campaign = Campaign.new(:medium => [:web, :print])
22
+ assert_stored campaign, :web, :print
23
+ campaign.medium << :phone
24
+ assert_stored campaign, :web, :print, :phone
25
+ end
26
+
27
+ should "ignores duplicate values added to bitmask" do
28
+ campaign = Campaign.new(:medium => [:web, :print])
29
+ assert_stored campaign, :web, :print
30
+ campaign.medium << :phone
31
+ assert_stored campaign, :web, :print, :phone
32
+ campaign.medium << :phone
33
+ assert_stored campaign, :web, :print, :phone
34
+ assert_equal 1, campaign.medium.select { |value| value == :phone }.size
35
+ end
36
+
37
+ should "can assign new values at once to bitmask" do
38
+ campaign = Campaign.new(:medium => [:web, :print])
39
+ assert_stored campaign, :web, :print
40
+ campaign.medium = [:phone, :email]
41
+ assert_stored campaign, :phone, :email
42
+ end
43
+
44
+ should "can save bitmask to db and retrieve values transparently" do
45
+ campaign = Campaign.new(:medium => [:web, :print])
46
+ assert_stored campaign, :web, :print
47
+ assert campaign.save
48
+ assert_stored Campaign.find(campaign.id), :web, :print
49
+ end
50
+
51
+ should "can add custom behavor to value proxies during bitmask definition" do
52
+ campaign = Campaign.new(:medium => [:web, :print])
53
+ assert_raises NoMethodError do
54
+ campaign.medium.worked?
55
+ end
56
+ assert_nothing_raised do
57
+ campaign.misc.worked?
58
+ end
59
+ assert campaign.misc.worked?
60
+ end
61
+
62
+ should "cannot use unsupported values" do
63
+ assert_unsupported { Campaign.new(:medium => [:web, :print, :this_will_fail]) }
64
+ campaign = Campaign.new(:medium => :web)
65
+ assert_unsupported { campaign.medium << :this_will_fail_also }
66
+ assert_unsupported { campaign.medium = [:so_will_this] }
67
+ end
68
+
69
+ should "can determine bitmasks using convenience method" do
70
+ assert Campaign.bitmask_for_medium(:web, :print)
71
+ assert_equal(
72
+ Campaign.bitmasks[:medium][:web] | Campaign.bitmasks[:medium][:print],
73
+ Campaign.bitmask_for_medium(:web, :print)
74
+ )
75
+ end
76
+
77
+ should "assert use of unknown value in convenience method will result in exception" do
78
+ assert_unsupported { Campaign.bitmask_for_medium(:web, :and_this_isnt_valid) }
79
+ end
80
+
81
+ should "hash of values is with indifferent access" do
82
+ string_bit = nil
83
+ assert_nothing_raised do
84
+ assert (string_bit = Campaign.bitmask_for_medium('web', 'print'))
85
+ end
86
+ assert_equal Campaign.bitmask_for_medium(:web, :print), string_bit
87
+ end
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
+ should "convert values passed as strings to symbols" do
101
+ campaign = Campaign.new
102
+ campaign.medium << "web"
103
+ assert_equal [:web], campaign.medium
104
+ assert_equal true, campaign.medium?("web")
105
+ end
106
+
107
+ context "checking" do
108
+
109
+ setup { @campaign = Campaign.new(:medium => [:web, :print]) }
110
+
111
+ context "for a single value" do
112
+
113
+ should "be supported by an attribute_for_value convenience method" do
114
+ assert @campaign.medium_for_web?
115
+ assert @campaign.medium_for_print?
116
+ assert !@campaign.medium_for_email?
117
+ end
118
+
119
+ should "be supported by the simple predicate method" do
120
+ assert @campaign.medium?(:web)
121
+ assert @campaign.medium?(:print)
122
+ assert !@campaign.medium?(:email)
123
+ end
124
+
125
+ end
126
+
127
+ context "for multiple values" do
128
+
129
+ should "be supported by the simple predicate method" do
130
+ assert @campaign.medium?(:web, :print)
131
+ assert !@campaign.medium?(:web, :email)
132
+ end
133
+
134
+ end
135
+
136
+ end
137
+
138
+ context "named scopes" do
139
+
140
+ setup do
141
+ @company = Company.create(:name => "Test Co, Intl.")
142
+ @campaign1 = @company.campaigns.create :medium => [:web, :print]
143
+ @campaign2 = @company.campaigns.create
144
+ @campaign3 = @company.campaigns.create :medium => [:web, :email]
145
+ end
146
+
147
+ should "support retrieval by any value" do
148
+ assert_equal [@campaign1, @campaign3], @company.campaigns.with_medium
149
+ end
150
+
151
+ should "support retrieval by one matching value" do
152
+ assert_equal [@campaign1], @company.campaigns.with_medium(:print)
153
+ end
154
+
155
+ should "support retrieval by all matching values" do
156
+ assert_equal [@campaign1], @company.campaigns.with_medium(:web, :print)
157
+ assert_equal [@campaign3], @company.campaigns.with_medium(:web, :email)
158
+ end
159
+
160
+ should "support retrieval for no values" do
161
+ assert_equal [@campaign2], @company.campaigns.without_medium
162
+ end
163
+
164
+ end
165
+
166
+ should "can check if at least one value is set" do
167
+ campaign = Campaign.new(:medium => [:web, :print])
168
+
169
+ assert campaign.medium?
170
+
171
+ campaign = Campaign.new
172
+
173
+ assert !campaign.medium?
174
+ end
175
+
176
+ should "find by bitmask values" do
177
+ campaign = Campaign.new(:medium => [:web, :print])
178
+ assert campaign.save
179
+
180
+ assert_equal(
181
+ Campaign.find(:all, :conditions => ['medium & ? <> 0', Campaign.bitmask_for_medium(:print)]),
182
+ Campaign.medium_for_print
183
+ )
184
+
185
+ assert_equal Campaign.medium_for_print.all, Campaign.medium_for_print.medium_for_web.all
186
+
187
+ assert_equal [], Campaign.medium_for_email
188
+ assert_equal [], Campaign.medium_for_web.medium_for_email
189
+ end
190
+
191
+ should "find no values" do
192
+ campaign = Campaign.create(:medium => [:web, :print])
193
+ assert campaign.save
194
+
195
+ assert_equal [], Campaign.no_medium
196
+
197
+ campaign.medium = []
198
+ assert campaign.save
199
+
200
+ assert_equal [campaign], Campaign.no_medium
201
+ end
202
+
203
+ #######
204
+ private
205
+ #######
206
+
207
+ def assert_unsupported(&block)
208
+ assert_raises(ArgumentError, &block)
209
+ end
210
+
211
+ def assert_stored(record, *values)
212
+ values.each do |value|
213
+ assert record.medium.any? { |v| v.to_s == value.to_s }, "Values #{record.medium.inspect} does not include #{value.inspect}"
214
+ end
215
+ full_mask = values.inject(0) do |mask, value|
216
+ mask | Campaign.bitmasks[:medium][value]
217
+ end
218
+ assert_equal full_mask, record.medium.to_i
219
+ end
220
+
221
+ end
222
+
223
+ end
@@ -0,0 +1,155 @@
1
+ require 'clean_test_helper'
2
+
3
+ class CleanBitmaskAttributeTest < Test::Unit::TestCase
4
+
5
+ context "CleanCampaign" do
6
+
7
+ should "can assign single value to bitmask" do
8
+ assert_stored CleanCampaign.new(:medium => :web), :web
9
+ end
10
+
11
+ should "can assign multiple values to bitmask" do
12
+ assert_stored CleanCampaign.new(:medium => [:web, :print]), :web, :print
13
+ end
14
+
15
+ should "can add single value to bitmask" do
16
+ campaign = CleanCampaign.new(:medium => [:web, :print])
17
+ assert_stored campaign, :web, :print
18
+ campaign.medium << :phone
19
+ assert_stored campaign, :web, :print, :phone
20
+ end
21
+
22
+ should "ignores duplicate values added to bitmask" do
23
+ campaign = CleanCampaign.new(:medium => [:web, :print])
24
+ assert_stored campaign, :web, :print
25
+ campaign.medium << :phone
26
+ assert_stored campaign, :web, :print, :phone
27
+ campaign.medium << :phone
28
+ assert_stored campaign, :web, :print, :phone
29
+ assert_equal 1, campaign.medium.select { |value| value == :phone }.size
30
+ end
31
+
32
+ should "can assign new values at once to bitmask" do
33
+ campaign = CleanCampaign.new(:medium => [:web, :print])
34
+ assert_stored campaign, :web, :print
35
+ campaign.medium = [:phone, :email]
36
+ assert_stored campaign, :phone, :email
37
+ end
38
+
39
+ should "can add custom behavor to value proxies during bitmask definition" do
40
+ campaign = CleanCampaign.new(:medium => [:web, :print])
41
+ assert_raises NoMethodError do
42
+ campaign.medium.worked?
43
+ end
44
+ assert_nothing_raised do
45
+ campaign.misc.worked?
46
+ end
47
+ assert campaign.misc.worked?
48
+ end
49
+
50
+ should "cannot use unsupported values" do
51
+ assert_unsupported { CleanCampaign.new(:medium => [:web, :print, :this_will_fail]) }
52
+ campaign = CleanCampaign.new(:medium => :web)
53
+ assert_unsupported { campaign.medium << :this_will_fail_also }
54
+ assert_unsupported { campaign.medium = [:so_will_this] }
55
+ end
56
+
57
+ should "can determine bitmasks using convenience method" do
58
+ assert CleanCampaign.bitmask_for_medium(:web, :print)
59
+ assert_equal(
60
+ CleanCampaign.bitmasks[:medium][:web] | CleanCampaign.bitmasks[:medium][:print],
61
+ CleanCampaign.bitmask_for_medium(:web, :print)
62
+ )
63
+ end
64
+
65
+ should "assert use of unknown value in convenience method will result in exception" do
66
+ assert_unsupported { CleanCampaign.bitmask_for_medium(:web, :and_this_isnt_valid) }
67
+ end
68
+
69
+ should "hash of values is with indifferent access" do
70
+ string_bit = nil
71
+ assert_nothing_raised do
72
+ assert (string_bit = CleanCampaign.bitmask_for_medium('web', 'print'))
73
+ end
74
+ assert_equal CleanCampaign.bitmask_for_medium(:web, :print), string_bit
75
+ end
76
+
77
+ should "save bitmask with non-standard attribute names" do
78
+ campaign = CleanCampaign.new(:Legacy => [:upper, :case])
79
+ assert_equal [:upper, :case], campaign.Legacy
80
+ end
81
+
82
+ should "ignore blanks fed as values" do
83
+ campaign = CleanCampaign.new(:medium => [:web, :print, ''])
84
+ assert_stored campaign, :web, :print
85
+ end
86
+
87
+ should "convert values passed as strings to symbols" do
88
+ campaign = CleanCampaign.new
89
+ campaign.medium << "web"
90
+ assert_equal [:web], campaign.medium
91
+ assert_equal true, campaign.medium?("web")
92
+ end
93
+
94
+ context "checking" do
95
+
96
+ setup { @campaign = CleanCampaign.new(:medium => [:web, :print]) }
97
+
98
+ context "for a single value" do
99
+
100
+ should "be supported by an attribute_for_value convenience method" do
101
+ assert @campaign.medium_for_web?
102
+ assert @campaign.medium_for_print?
103
+ assert !@campaign.medium_for_email?
104
+ end
105
+
106
+ should "be supported by the simple predicate method" do
107
+ assert @campaign.medium?(:web)
108
+ assert @campaign.medium?(:print)
109
+ assert !@campaign.medium?(:email)
110
+ end
111
+
112
+ end
113
+
114
+ context "for multiple values" do
115
+
116
+ should "be supported by the simple predicate method" do
117
+ assert @campaign.medium?(:web, :print)
118
+ assert !@campaign.medium?(:web, :email)
119
+ end
120
+
121
+ end
122
+
123
+ end
124
+
125
+ should "can check if at least one value is set" do
126
+ campaign = CleanCampaign.new(:medium => [:web, :print])
127
+
128
+ assert campaign.medium?
129
+
130
+ campaign = CleanCampaign.new
131
+
132
+ assert !campaign.medium?
133
+ end
134
+
135
+ #######
136
+ private
137
+ #######
138
+
139
+ def assert_unsupported(&block)
140
+ assert_raises(ArgumentError, &block)
141
+ end
142
+
143
+ def assert_stored(record, *values)
144
+ values.each do |value|
145
+ assert record.medium.any? { |v| v.to_s == value.to_s }, "Values #{record.medium.inspect} does not include #{value.inspect}"
146
+ end
147
+ full_mask = values.inject(0) do |mask, value|
148
+ mask | CleanCampaign.bitmasks[:medium][value]
149
+ end
150
+ assert_equal full_mask, record.medium.to_i
151
+ end
152
+
153
+ end
154
+
155
+ end
@@ -0,0 +1,42 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+ begin
5
+ require 'redgreen'
6
+ rescue LoadError
7
+ end
8
+
9
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
10
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
11
+ require 'bitmask-attribute'
12
+ require File.dirname(__FILE__) + '/../rails/init'
13
+
14
+ # Pseudo model for testing purposes
15
+ class CleanCampaign
16
+ include BitmaskAttribute
17
+ bitmask :medium, :as => [:web, :print, :email, :phone]
18
+ bitmask :misc, :as => %w(some useless values) do
19
+ def worked?
20
+ true
21
+ end
22
+ end
23
+ bitmask :Legacy, :as => [:upper, :case]
24
+ end
25
+
26
+ class Test::Unit::TestCase
27
+
28
+ def assert_unsupported(&block)
29
+ assert_raises(ArgumentError, &block)
30
+ end
31
+
32
+ def assert_stored(record, *values)
33
+ values.each do |value|
34
+ assert record.medium.any? { |v| v.to_s == value.to_s }, "Values #{record.medium.inspect} does not include #{value.inspect}"
35
+ end
36
+ full_mask = values.inject(0) do |mask, value|
37
+ mask | Campaign.bitmasks[:medium][value]
38
+ end
39
+ assert_equal full_mask, record.medium.to_i
40
+ end
41
+
42
+ end
@@ -0,0 +1,69 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ require 'active_support'
6
+ require 'active_support/core_ext/object'
7
+ require 'active_record'
8
+
9
+ begin
10
+ require 'redgreen'
11
+ require 'active_support/hash_with_indifferent_access'
12
+ rescue LoadError
13
+ end
14
+
15
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
16
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
17
+ require 'bitmask-attribute'
18
+ require File.dirname(__FILE__) + '/../rails/init'
19
+
20
+ # ActiveRecord::Base.logger = Logger.new(STDOUT)
21
+
22
+ ActiveRecord::Base.establish_connection(
23
+ :adapter => 'sqlite3',
24
+ :database => ':memory:'
25
+ )
26
+
27
+ ActiveRecord::Schema.define do
28
+ create_table :campaigns do |t|
29
+ t.integer :company_id
30
+ t.integer :medium, :misc, :Legacy
31
+ end
32
+ create_table :companies do |t|
33
+ t.string :name
34
+ end
35
+ end
36
+
37
+ class Company < ActiveRecord::Base
38
+ has_many :campaigns
39
+ end
40
+
41
+ # Pseudo model for testing purposes
42
+ class Campaign < ActiveRecord::Base
43
+ belongs_to :company
44
+ bitmask :medium, :as => [:web, :print, :email, :phone]
45
+ bitmask :misc, :as => %w(some useless values) do
46
+ def worked?
47
+ true
48
+ end
49
+ end
50
+ bitmask :Legacy, :as => [:upper, :case]
51
+ end
52
+
53
+ class Test::Unit::TestCase
54
+
55
+ def assert_unsupported(&block)
56
+ assert_raises(ArgumentError, &block)
57
+ end
58
+
59
+ def assert_stored(record, *values)
60
+ values.each do |value|
61
+ assert record.medium.any? { |v| v.to_s == value.to_s }, "Values #{record.medium.inspect} does not include #{value.inspect}"
62
+ end
63
+ full_mask = values.inject(0) do |mask, value|
64
+ mask | Campaign.bitmasks[:medium][value]
65
+ end
66
+ assert_equal full_mask, record.medium.to_i
67
+ end
68
+
69
+ end