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.
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