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 +81 -15
- data/VERSION +1 -0
- data/lib/bitmask_attribute.rb +60 -6
- data/lib/bitmask_attribute/value_proxy.rb +2 -2
- data/test/bitmask_attribute_test.rb +113 -1
- data/test/test_helper.rb +14 -3
- metadata +3 -4
- data/VERSION.yml +0 -4
- data/bitmask-attribute.gemspec +0 -50
data/README.markdown
CHANGED
@@ -1,14 +1,16 @@
|
|
1
|
-
|
1
|
+
bitmask-attribute
|
2
|
+
=================
|
2
3
|
|
3
4
|
Transparent manipulation of bitmask attributes.
|
4
5
|
|
5
|
-
|
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
|
-
|
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
|
-
|
29
|
-
#
|
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
|
-
|
66
|
+
Later we'll support finding records without a specific bitmask.
|
32
67
|
|
33
|
-
|
34
|
-
|
35
|
-
the end.
|
68
|
+
Adding Methods
|
69
|
+
--------------
|
36
70
|
|
37
|
-
|
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
|
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
|
-
|
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
|
data/lib/bitmask_attribute.rb
CHANGED
@@ -15,7 +15,9 @@ module BitmaskAttribute
|
|
15
15
|
validate_for model
|
16
16
|
generate_bitmasks_on model
|
17
17
|
override model
|
18
|
-
|
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
|
-
|
27
|
-
|
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
|
-
|
63
|
+
self.#{attribute}.replace(values.reject(&:blank?))
|
57
64
|
end
|
58
65
|
)
|
59
66
|
end
|
60
67
|
|
61
|
-
def
|
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 |
|
24
|
-
|
25
|
-
|
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.
|
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-
|
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
|
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
data/bitmask-attribute.gemspec
DELETED
@@ -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
|