clean-bitmask-attribute 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.markdown
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+ \#*
7
+ .#*
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007-2009 Bruce Williams
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,121 @@
1
+ bitmask-attribute
2
+ =================
3
+
4
+ Transparent manipulation of bitmask attributes.
5
+
6
+ Example
7
+ -------
8
+
9
+ Simply declare an existing integer column as a bitmask with its possible
10
+ values.
11
+
12
+ class User < ActiveRecord::Base
13
+ bitmask :roles, :as => [:writer, :publisher, :editor, :proofreader]
14
+ end
15
+
16
+ Or with a clean class:
17
+
18
+ class User
19
+ include BitmaskAttribute
20
+ bitmask :roles, :as => [:writer, :publisher, :editor, :proofreader]
21
+ end
22
+
23
+ You can then modify the column using the declared values without resorting
24
+ to manual bitmasks.
25
+
26
+ user = User.create(:name => "Bruce", :roles => [:publisher, :editor])
27
+ user.roles
28
+ # => [:publisher, :editor]
29
+ user.roles << :writer
30
+ user.roles
31
+ # => [:publisher, :editor, :writer]
32
+
33
+ It's easy to find out if a record has a given value:
34
+
35
+ user.roles?(:editor)
36
+ # => true
37
+
38
+ You can check for multiple values (uses an `and` boolean):
39
+
40
+ user.roles?(:editor, :publisher)
41
+ # => true
42
+ user.roles?(:editor, :proofreader)
43
+ # => false
44
+
45
+ Or, just check if any values are present:
46
+
47
+ user.roles?
48
+ # => true
49
+
50
+ Named Scopes
51
+ ------------
52
+
53
+ A couple useful named scopes are also generated when you use
54
+ `bitmask` with ActiveRecord:
55
+
56
+ User.with_roles
57
+ # => (all users with roles)
58
+ User.with_roles(:editor)
59
+ # => (all editors)
60
+ User.with_roles(:editor, :writer)
61
+ # => (all users who are BOTH editors and writers)
62
+
63
+ Later we'll support an `or` boolean; for now, do something like:
64
+
65
+ User.with_roles(:editor) + User.with_roles(:writer)
66
+ # => (all users who are EITHER editors and writers)
67
+
68
+ Find records without any bitmask set:
69
+
70
+ User.without_roles
71
+ # => (all users without a role)
72
+
73
+ Later we'll support finding records without a specific bitmask.
74
+
75
+ Adding Methods
76
+ --------------
77
+
78
+ You can add your own methods to the bitmasked attributes (similar to
79
+ named scopes):
80
+
81
+ bitmask :other_attribute, :as => [:value1, :value2] do
82
+ def worked?
83
+ true
84
+ end
85
+ end
86
+
87
+ user = User.first
88
+ user.other_attribute.worked?
89
+ # => true
90
+
91
+
92
+ Warning: Modifying possible values
93
+ ----------------------------------
94
+
95
+ IMPORTANT: Once you have data using a bitmask, don't change the order
96
+ of the values, remove any values, or insert any new values in the `:as`
97
+ array anywhere except at the end. You won't like the results.
98
+
99
+ Contributing and reporting issues
100
+ ---------------------------------
101
+
102
+ Please feel free to fork & contribute fixes via GitHub pull requests.
103
+ The official repository for this project is
104
+ http://github.com/bruce/bitmask-attribute
105
+
106
+ Issues can be reported at
107
+ http://github.com/bruce/bitmask-attribute/issues
108
+
109
+ Credits
110
+ -------
111
+
112
+ Thanks to the following contributors:
113
+
114
+ * [Jason L Perry](http://github.com/ambethia)
115
+ * [Nicolas Fouché](http://github.com/nfo)
116
+ * [Pavel Chipiga](http://github.com/chipiga)
117
+
118
+ Copyright
119
+ ---------
120
+
121
+ Copyright (c) 2007-2010 Bruce Williams. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,59 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "clean-bitmask-attribute"
8
+ gem.summary = %Q{Simple bitmask attribute support for any class}
9
+ gem.email = "pavel.chipiga@gmail.com"
10
+ gem.homepage = "http://github.com/chipiga/bitmask-attribute/tree/clean"
11
+ gem.authors = ["Pavel Chipiga", "Bruce Williams"]
12
+ gem.add_development_dependency 'activerecord'
13
+ gem.add_development_dependency 'sqlite3-ruby'
14
+ gem.add_development_dependency "shoulda"
15
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ end
17
+ Jeweler::GemcutterTasks.new
18
+ rescue LoadError
19
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
20
+ end
21
+
22
+ require 'rake/testtask'
23
+ Rake::TestTask.new(:test) do |test|
24
+ test.libs << 'lib' << 'test'
25
+ test.pattern = 'test/**/*_test.rb'
26
+ test.verbose = true
27
+ end
28
+
29
+ begin
30
+ require 'rcov/rcovtask'
31
+ Rcov::RcovTask.new do |test|
32
+ test.libs << 'test'
33
+ test.pattern = 'test/**/*_test.rb'
34
+ test.verbose = true
35
+ end
36
+ rescue LoadError
37
+ task :rcov do
38
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
39
+ end
40
+ end
41
+
42
+
43
+ task :default => :test
44
+
45
+ require 'rake/rdoctask'
46
+ Rake::RDocTask.new do |rdoc|
47
+ if File.exist?('VERSION.yml')
48
+ config = YAML.load(File.read('VERSION.yml'))
49
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
50
+ else
51
+ version = ""
52
+ end
53
+
54
+ rdoc.rdoc_dir = 'rdoc'
55
+ rdoc.title = "bitmask-attribute #{version}"
56
+ rdoc.rdoc_files.include('README*')
57
+ rdoc.rdoc_files.include('lib/**/*.rb')
58
+ end
59
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 2.0.0
@@ -0,0 +1,2 @@
1
+ # Stub for dash-style requires
2
+ require File.dirname(__FILE__) << "/bitmask_attribute"
@@ -0,0 +1,25 @@
1
+ module BitmaskAttribute
2
+ module Attribute
3
+ attr_accessor :attributes
4
+
5
+ def initialize(attrs = {})
6
+ @attributes = {}
7
+ attrs.each do |k,v|
8
+ sym = :"#{k}="
9
+ if respond_to? sym
10
+ send sym, v
11
+ else
12
+ write_attribute(k, v)
13
+ end
14
+ end
15
+ end
16
+
17
+ def read_attribute(attr_name)
18
+ @attributes[attr_name.to_s]
19
+ end
20
+
21
+ def write_attribute(attr_name, value)
22
+ @attributes[attr_name.to_s] = value
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,77 @@
1
+ # Extracted from Active Support
2
+ class Object
3
+ # An object is blank if it's false, empty, or a whitespace string.
4
+ # For example, "", " ", +nil+, [], and {} are blank.
5
+ #
6
+ # This simplifies:
7
+ #
8
+ # if !address.nil? && !address.empty?
9
+ #
10
+ # ...to:
11
+ #
12
+ # if !address.blank?
13
+ def blank?
14
+ respond_to?(:empty?) ? empty? : !self
15
+ end
16
+
17
+ # An object is present if it's not blank.
18
+ def present?
19
+ !blank?
20
+ end
21
+
22
+ # Returns object if it's #present? otherwise returns nil.
23
+ # object.presence is equivalent to object.present? ? object : nil.
24
+ #
25
+ # This is handy for any representation of objects where blank is the same
26
+ # as not present at all. For example, this simplifies a common check for
27
+ # HTTP POST/query parameters:
28
+ #
29
+ # state = params[:state] if params[:state].present?
30
+ # country = params[:country] if params[:country].present?
31
+ # region = state || country || 'US'
32
+ #
33
+ # ...becomes:
34
+ #
35
+ # region = params[:state].presence || params[:country].presence || 'US'
36
+ def presence
37
+ self if present?
38
+ end
39
+ end
40
+
41
+ class NilClass #:nodoc:
42
+ def blank?
43
+ true
44
+ end
45
+ end
46
+
47
+ class FalseClass #:nodoc:
48
+ def blank?
49
+ true
50
+ end
51
+ end
52
+
53
+ class TrueClass #:nodoc:
54
+ def blank?
55
+ false
56
+ end
57
+ end
58
+
59
+ class Array #:nodoc:
60
+ alias_method :blank?, :empty?
61
+ end
62
+
63
+ class Hash #:nodoc:
64
+ alias_method :blank?, :empty?
65
+ end
66
+
67
+ class String #:nodoc:
68
+ def blank?
69
+ self !~ /\S/
70
+ end
71
+ end
72
+
73
+ class Numeric #:nodoc:
74
+ def blank?
75
+ false
76
+ end
77
+ end
@@ -0,0 +1,182 @@
1
+ # Extracted from Active Support
2
+ class Hash
3
+ # Return a new hash with all keys converted to strings.
4
+ def stringify_keys
5
+ dup.stringify_keys!
6
+ end
7
+
8
+ # Destructively convert all keys to strings.
9
+ def stringify_keys!
10
+ keys.each do |key|
11
+ self[key.to_s] = delete(key)
12
+ end
13
+ self
14
+ end
15
+
16
+ # Return a new hash with all keys converted to symbols, as long as
17
+ # they respond to +to_sym+.
18
+ def symbolize_keys
19
+ dup.symbolize_keys!
20
+ end
21
+
22
+ # Destructively convert all keys to symbols, as long as they respond
23
+ # to +to_sym+.
24
+ def symbolize_keys!
25
+ keys.each do |key|
26
+ self[(key.to_sym rescue key) || key] = delete(key)
27
+ end
28
+ self
29
+ end
30
+
31
+ alias_method :to_options, :symbolize_keys
32
+ alias_method :to_options!, :symbolize_keys!
33
+
34
+ # Validate all keys in a hash match *valid keys, raising ArgumentError on a mismatch.
35
+ # Note that keys are NOT treated indifferently, meaning if you use strings for keys but assert symbols
36
+ # as keys, this will fail.
37
+ #
38
+ # ==== Examples
39
+ # { :name => "Rob", :years => "28" }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key(s): years"
40
+ # { :name => "Rob", :age => "28" }.assert_valid_keys("name", "age") # => raises "ArgumentError: Unknown key(s): name, age"
41
+ # { :name => "Rob", :age => "28" }.assert_valid_keys(:name, :age) # => passes, raises nothing
42
+ def assert_valid_keys(*valid_keys)
43
+ unknown_keys = keys - [valid_keys].flatten
44
+ raise(ArgumentError, "Unknown key(s): #{unknown_keys.join(", ")}") unless unknown_keys.empty?
45
+ end
46
+ end
47
+
48
+ class HashWithIndifferentAccess < Hash
49
+ def extractable_options?
50
+ true
51
+ end
52
+
53
+ def initialize(constructor = {})
54
+ if constructor.is_a?(Hash)
55
+ super()
56
+ update(constructor)
57
+ else
58
+ super(constructor)
59
+ end
60
+ end
61
+
62
+ def default(key = nil)
63
+ if key.is_a?(Symbol) && include?(key = key.to_s)
64
+ self[key]
65
+ else
66
+ super
67
+ end
68
+ end
69
+
70
+ alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
71
+ alias_method :regular_update, :update unless method_defined?(:regular_update)
72
+
73
+ # Assigns a new value to the hash:
74
+ #
75
+ # hash = HashWithIndifferentAccess.new
76
+ # hash[:key] = "value"
77
+ #
78
+ def []=(key, value)
79
+ regular_writer(convert_key(key), convert_value(value))
80
+ end
81
+
82
+ # Updates the instantized hash with values from the second:
83
+ #
84
+ # hash_1 = HashWithIndifferentAccess.new
85
+ # hash_1[:key] = "value"
86
+ #
87
+ # hash_2 = HashWithIndifferentAccess.new
88
+ # hash_2[:key] = "New Value!"
89
+ #
90
+ # hash_1.update(hash_2) # => {"key"=>"New Value!"}
91
+ #
92
+ def update(other_hash)
93
+ other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) }
94
+ self
95
+ end
96
+
97
+ alias_method :merge!, :update
98
+
99
+ # Checks the hash for a key matching the argument passed in:
100
+ #
101
+ # hash = HashWithIndifferentAccess.new
102
+ # hash["key"] = "value"
103
+ # hash.key? :key # => true
104
+ # hash.key? "key" # => true
105
+ #
106
+ def key?(key)
107
+ super(convert_key(key))
108
+ end
109
+
110
+ alias_method :include?, :key?
111
+ alias_method :has_key?, :key?
112
+ alias_method :member?, :key?
113
+
114
+ # Fetches the value for the specified key, same as doing hash[key]
115
+ def fetch(key, *extras)
116
+ super(convert_key(key), *extras)
117
+ end
118
+
119
+ # Returns an array of the values at the specified indices:
120
+ #
121
+ # hash = HashWithIndifferentAccess.new
122
+ # hash[:a] = "x"
123
+ # hash[:b] = "y"
124
+ # hash.values_at("a", "b") # => ["x", "y"]
125
+ #
126
+ def values_at(*indices)
127
+ indices.collect {|key| self[convert_key(key)]}
128
+ end
129
+
130
+ # Returns an exact copy of the hash.
131
+ def dup
132
+ HashWithIndifferentAccess.new(self)
133
+ end
134
+
135
+ # Merges the instantized and the specified hashes together, giving precedence to the values from the second hash
136
+ # Does not overwrite the existing hash.
137
+ def merge(hash)
138
+ self.dup.update(hash)
139
+ end
140
+
141
+ # Performs the opposite of merge, with the keys and values from the first hash taking precedence over the second.
142
+ # This overloaded definition prevents returning a regular hash, if reverse_merge is called on a HashWithDifferentAccess.
143
+ def reverse_merge(other_hash)
144
+ super other_hash.with_indifferent_access
145
+ end
146
+
147
+ def reverse_merge!(other_hash)
148
+ replace(reverse_merge( other_hash ))
149
+ end
150
+
151
+ # Removes a specified key from the hash.
152
+ def delete(key)
153
+ super(convert_key(key))
154
+ end
155
+
156
+ def stringify_keys!; self end
157
+ def stringify_keys; dup end
158
+ undef :symbolize_keys!
159
+ def symbolize_keys; to_hash.symbolize_keys end
160
+ def to_options!; self end
161
+
162
+ # Convert to a Hash with String keys.
163
+ def to_hash
164
+ Hash.new(default).merge!(self)
165
+ end
166
+
167
+ protected
168
+ def convert_key(key)
169
+ key.kind_of?(Symbol) ? key.to_s : key
170
+ end
171
+
172
+ def convert_value(value)
173
+ case value
174
+ when Hash
175
+ value.with_indifferent_access
176
+ when Array
177
+ value.collect { |e| e.is_a?(Hash) ? e.with_indifferent_access : e }
178
+ else
179
+ value
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,43 @@
1
+ # Extracted from Active Support
2
+ class Object
3
+ # Returns +value+ after yielding +value+ to the block. This simplifies the
4
+ # process of constructing an object, performing work on the object, and then
5
+ # returning the object from a method. It is a Ruby-ized realization of the K
6
+ # combinator, courtesy of Mikael Brockman.
7
+ #
8
+ # ==== Examples
9
+ #
10
+ # # Without returning
11
+ # def foo
12
+ # values = []
13
+ # values << "bar"
14
+ # values << "baz"
15
+ # return values
16
+ # end
17
+ #
18
+ # foo # => ['bar', 'baz']
19
+ #
20
+ # # returning with a local variable
21
+ # def foo
22
+ # returning values = [] do
23
+ # values << 'bar'
24
+ # values << 'baz'
25
+ # end
26
+ # end
27
+ #
28
+ # foo # => ['bar', 'baz']
29
+ #
30
+ # # returning with a block argument
31
+ # def foo
32
+ # returning [] do |values|
33
+ # values << 'bar'
34
+ # values << 'baz'
35
+ # end
36
+ # end
37
+ #
38
+ # foo # => ['bar', 'baz']
39
+ def returning(value)
40
+ yield(value)
41
+ value
42
+ end
43
+ end
@@ -0,0 +1,77 @@
1
+ module BitmaskAttribute
2
+
3
+ class ValueProxy < Array
4
+
5
+ def initialize(record, attribute, &extension)
6
+ @record = record
7
+ @attribute = attribute
8
+ find_mapping
9
+ instance_eval(&extension) if extension
10
+ super(extract_values)
11
+ end
12
+
13
+ # =========================
14
+ # = OVERRIDE TO SERIALIZE =
15
+ # =========================
16
+
17
+ %w(push << delete replace reject! select!).each do |override|
18
+ class_eval(<<-EOEVAL)
19
+ def #{override}(*args)
20
+ returning(super) do
21
+ updated!
22
+ end
23
+ end
24
+ EOEVAL
25
+ end
26
+
27
+ def to_i
28
+ inject(0) { |memo, value| memo | @mapping[value] }
29
+ end
30
+
31
+ #######
32
+ private
33
+ #######
34
+
35
+ def validate!
36
+ each do |value|
37
+ if @mapping.key? value
38
+ true
39
+ else
40
+ raise ArgumentError, "Unsupported value for `#{@attribute}': #{value.inspect}"
41
+ end
42
+ end
43
+ end
44
+
45
+ def symbolize!
46
+ map!(&:to_sym)
47
+ end
48
+
49
+ def updated!
50
+ symbolize!
51
+ validate!
52
+ uniq!
53
+ serialize!
54
+ end
55
+
56
+ def serialize!
57
+ @record.send(:write_attribute, @attribute, to_i)
58
+ end
59
+
60
+ def extract_values
61
+ stored = [@record.send(:read_attribute, @attribute) || 0, 0].max
62
+ @mapping.inject([]) do |values, (value, bitmask)|
63
+ returning values do
64
+ values << value.to_sym if (stored & bitmask > 0)
65
+ end
66
+ end
67
+ end
68
+
69
+ def find_mapping
70
+ unless (@mapping = @record.class.bitmasks[@attribute])
71
+ raise ArgumentError, "Could not find mapping for bitmask attribute :#{@attribute}"
72
+ end
73
+ end
74
+
75
+ end
76
+
77
+ end