has_many_booleans 0.9

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Jan Lelis
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.rdoc ADDED
@@ -0,0 +1,74 @@
1
+ == ActiveRecord plugin: has_many_booleans
2
+ <em>has_many_booleans creates virtual boolean attributes for a model.
3
+ When the object gets saved, the plugin transforms all attributes into a single integer, using a {bitset}[http://en.wikipedia.org/wiki/Bitset]. So you can easily add new attributes without changing the database structure.</em>
4
+
5
+
6
+ === Setup
7
+ Install the plugin with
8
+ * Rails 2: <tt>script/plugin install {http://http://github.com/janlelis/has_many_booleans}[http://github.com/janlelis/has_many_booleans]</tt>
9
+ * Rails 3: <tt>rails p install {http://http://github.com/janlelis/has_many_booleans}[http://github.com/janlelis/has_many_booleans]</tt>
10
+ * Or as a gem: <tt>gem install has_many_booleans</tt>
11
+
12
+ Add an integer field with the name +booleans+ to your the model's database table.
13
+
14
+ === Example usage
15
+
16
+ You simply list names for the desired booleans in the model.rb file...
17
+
18
+ class Model < ActiveRecord::Base
19
+ has_many_booleans :name, :password
20
+ end
21
+
22
+ ...to get the following methods:
23
+ [<tt>name_activated</tt>, <tt>name_activated?</tt>] get the value of the boolean
24
+ [<tt>name_activated!</tt>] set the value to true
25
+ [<tt>name_activated=</tt> value] set the value to false or true
26
+ [<tt>password_activated</tt>, ...] same methods for <tt>:password</tt>
27
+
28
+
29
+ When saving the object, all "virtual" booleans get converted to a single integer that is
30
+ saved in the database. Vice versa, when loading an model from the database, its boolean integer sets the value of the above methods.
31
+
32
+ ==== Example 2: basic options
33
+
34
+ class Model < ActiveRecord::Base
35
+ has_many_booleans :name, :password,
36
+ :true => [:name],
37
+ :append => 'set',
38
+ end
39
+
40
+ The default values of all booleans is +false+. However, with the <tt>:true</tt> option, you can list those booleans, which should default to +true+.
41
+
42
+ The <tt>:append</tt> option lets you modify the suffix to append to the boolean names.
43
+
44
+ ==== Example 3: advanced options
45
+
46
+ class Model < ActiveRecord::Base
47
+ has_many_booleans :name, :password,
48
+ :field => 'some_db_field',
49
+ :lazy => false,
50
+ :self => 'model_available',
51
+ :self_value => true,
52
+ end
53
+
54
+ The <tt>:field</tt> option lets you change the database field in which the integer gets stored (default is +booleans+).
55
+
56
+ When the <tt>:lazy</tt> option is set to +false+, the bitset integer gets changed every time you assign a new value for a boolean. The default setting is +true+, which means, the integer does not get updated until the object is saved in the database.
57
+
58
+ The <tt>:self</tt> option is just another virtual boolean, which's method name you can freely assign.
59
+
60
+ === Scopes
61
+ The plugin also generates a <tt>.true</tt> and a <tt>.false</tt> scope for the model. You have to pass a boolean name as parameter to filter for this value. If you pass multiple boolean names, they get connected with 'or'. To get an 'and' condition, chain multiple scopes. If you don't pass any boolean names (or +nil+), the special <tt>:self</tt> boolean is meant.
62
+
63
+ ==== Example queries
64
+ Model.true(:name) # scopes to all models, where :name is true
65
+ Model.false # scopes to all models, where the :self boolean is false
66
+ Model.true(:name, :password) # :name or :password must be true
67
+ Model.true(:name).true(:password) # :name and :password must be true
68
+
69
+ === Further reading
70
+ For a more detailed description of the options, see the rdoc for the {has_many_booleans}[link:classes/HasManyBooleans/ClassMethods.html#M000003] method.
71
+
72
+ Copyright (c) 2010 Jan Lelis, http://rbjl.net, released under the MIT license
73
+ available at http://github.com/janlelis/has_many_booleans
74
+
data/Rakefile ADDED
@@ -0,0 +1,46 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'rake/gempackagetask'
5
+
6
+ desc 'Default: run unit tests.'
7
+ task :default => :test
8
+
9
+ desc 'Test the has_many_booleans plugin.'
10
+ Rake::TestTask.new(:test) do |t|
11
+ t.libs << 'lib'
12
+ t.libs << 'test'
13
+ t.pattern = 'test/**/*_test.rb'
14
+ t.verbose = true
15
+ end
16
+
17
+ desc 'Generate documentation for the has_many_booleans plugin.'
18
+ Rake::RDocTask.new(:rdoc) do |rdoc|
19
+ rdoc.rdoc_dir = 'doc'
20
+ rdoc.title = 'has_many_booleans Rails plugin'
21
+ rdoc.options << '--inline-source'
22
+ rdoc.rdoc_files.include('README.rdoc')
23
+ rdoc.rdoc_files.include('lib/**/*.rb')
24
+ end
25
+
26
+ # gem
27
+ PKG_FILES = FileList[ '[a-zA-Z]*', 'lib/**/*', 'rails/**/*', 'test/**/*' ]
28
+ spec = Gem::Specification.new do |s|
29
+ s.name = "has_many_booleans"
30
+ s.version = "0.9"
31
+ s.author = "Jan Lelis"
32
+ s.email = "mail@janlelis.de"
33
+ s.homepage = "http://rbjl.net"
34
+ s.platform = Gem::Platform::RUBY
35
+ s.summary = "This Rails plugin/gem allows you to generate virtual boolean attributes, which get saved in the database as a single bitset integer"
36
+ s.files = PKG_FILES.to_a
37
+ s.require_path = "lib"
38
+ s.has_rdoc = true
39
+ s.extra_rdoc_files = ["README.rdoc"]
40
+ end
41
+
42
+ desc 'Turn this plugin into a gem.'
43
+ Rake::GemPackageTask.new(spec) do |pkg|
44
+ pkg.gem_spec = spec
45
+ end
46
+
data/install.rb ADDED
@@ -0,0 +1,4 @@
1
+ # Install hook code here
2
+ puts 'Thank you for using has_many_booleans :)'
3
+ puts ' J-_-L'
4
+
@@ -0,0 +1,335 @@
1
+ # J-_-L
2
+
3
+ require 'has_many_booleans/simple_bitset'
4
+
5
+ module HasManyBooleans #:nodoc:
6
+ RAILS2 = if ENV['RAILS_GEM_VERSION']
7
+ ENV['RAILS_GEM_VERSION'] < '3'
8
+ else
9
+ true
10
+ end
11
+
12
+ module ClassMethods
13
+ #=== Setup the booleans for a model
14
+ #The method takes the symbols of the desired booleans as parameters. As last
15
+ #parameter you can apply an options hash. Each symbol represents an index,
16
+ #<em>depending on the position</em> in the list, starting with 1.
17
+ #
18
+ # class Model < ActiveRecord::Base
19
+ # has_many_booleans :name, :password,
20
+ # :true => [ :name ],
21
+ # :append => 'set',
22
+ # end
23
+ #
24
+ #Another way of setting up the booleans is with an hash. This is useful when
25
+ #you want to choose the indexes yourself.
26
+ #
27
+ # class Model < ActiveRecord::Base
28
+ # has_many_booleans({:name => 23, :password => 99},
29
+ # :append => 'set')
30
+ # end
31
+ #
32
+ #==== Available options
33
+ #
34
+ #[<tt>:true</tt>] Takes an array of boolean names which shall default to +true+.
35
+ #[<tt>:append</tt>] The name to append to the listed booleans. The underscore is added automatically. +nil+ is also possible. Default is +activated+.
36
+ #[<tt>:field</tt>] The database field used. Defaults to +booleans+.
37
+ #[<tt>:suffixes</tt>] Specifies, which "alias" methods are created. Defaults to <tt>["?", "=", "!"]</tt>. You cannot add new ones, you can only forbid some of them.
38
+ #[<tt>:false_values</tt>] All the values in the array can be used to set a boolean to +false+ (when used with the <tt>=</tt> method). <b>Example:</b> Set this to <tt>["0"]</tt> and then call <tt>some_boolean_activated = "0"</tt>, it will set the boolean to +false+. Default settings is +false+ (deactivated).
39
+ #[<tt>:lazy</tt>] When the <tt>:lazy</tt> option is set to +false+, the bitset integer gets changed every time you assign a new value for a boolean. The default setting is +true+, which means, the integer gets only updated when the object is saved.
40
+ #[<tt>:self</tt>] This is just another virtual boolean. You can freely assign the name. It is always stored as first bit in the bitset integer (so if the bitset integer is odd, this special boolean is set). You can also set this to +true+, which means, the <tt>:append</tt> value is used as method name. Default: +false+.
41
+ #[<tt>:self_value</tt>] The default value for the special <tt>:self</tt> boolean above.
42
+ def has_many_booleans(*params)
43
+
44
+ # get params
45
+ if params.last.is_a? Hash # booleans_options
46
+ parse_booleans_options params.pop
47
+
48
+ if params.empty?
49
+ warn "has_many_booleans: You applied a single hash as parameter, which gets interpreted as option hash. If you wish to use it as boolean hash, give a {} as second parameter!"
50
+ end
51
+ else
52
+ parse_booleans_options Hash.new
53
+ end
54
+
55
+ if params.first.is_a? Hash # alternative usage with a hash instead of array
56
+ params = params.first
57
+ iter_method = :each
58
+ else
59
+ iter_method = :each_with_index
60
+ end
61
+
62
+ # data structure: { string => [index, boolean_value] }
63
+ @booleans_default = {}
64
+ params.send(iter_method){ |key, index|
65
+ index = index.to_i+1
66
+ @booleans_default[key.to_s] = [ index,
67
+ @booleans_options[:true].include?(key.to_sym) ||
68
+ @booleans_options[:true].include?(key.to_s)
69
+ ] if index > 0
70
+ }
71
+
72
+ # validators
73
+ @booleans_validators = { true => [], false => [] }
74
+
75
+ send :include, InstanceMethods
76
+
77
+ # hook in callbacks part 1 (to not overwrite after_initialize)
78
+ class << self
79
+ def instantiate_with_booleans(record) #:nodoc:
80
+ object = instantiate_without_booleans record
81
+ object.initialize_booleans
82
+ object
83
+ end
84
+ alias_method_chain :instantiate, :booleans
85
+ end
86
+
87
+ before_save :save_booleans
88
+
89
+ # register scopes
90
+ booleans_scope = lambda{ |true_or_false, *args|
91
+ indexes = if args.blank?
92
+ [1] # special self boolean
93
+ else
94
+ args.map{ |name|
95
+ if name == nil # allow self in "or" connection
96
+ 1
97
+ else
98
+ name = name.to_s
99
+ if !@booleans_default[name]
100
+ warn 'has_many_booleans: You are using unknown boolean names in your scope!'
101
+ else
102
+ 2 ** @booleans_default[name][0]
103
+ end
104
+ end
105
+ }.compact
106
+ end
107
+ cond = ["#{@booleans_options[:field]} & ?#{' < 1' if !true_or_false}"]*indexes.size*' or '
108
+ if RAILS2
109
+ {:conditions => [cond, *indexes]}
110
+ else
111
+ where cond, *indexes
112
+ end
113
+ }
114
+
115
+ scope_name = RAILS2 ? :named_scope : :scope
116
+
117
+ send scope_name, :true, lambda { |*args|
118
+ booleans_scope[true, *args]
119
+ }
120
+
121
+ send scope_name, :false, lambda { |*args|
122
+ booleans_scope[false, *args]
123
+ }
124
+ end
125
+
126
+ alias :hmb :has_many_booleans
127
+
128
+ # getters
129
+ def booleans_options #:nodoc:
130
+ @booleans_options
131
+ end
132
+
133
+ def booleans_default #:nodoc:
134
+ @booleans_default
135
+ end
136
+
137
+ def booleans_validators #:nodoc:
138
+ @booleans_validators
139
+ end
140
+
141
+ # List all booleans that are required to be false!
142
+ # validates_false :description, :password
143
+ def validates_false(*bools)
144
+ booleans_validators[false] = bools
145
+ validate :validator_false
146
+ end
147
+
148
+ # List all booleans that are required to be true!
149
+ # validates_false :description, :password
150
+ def validates_true(*bools)
151
+ booleans_validators[true] = bools
152
+ validate :validator_true
153
+ end
154
+
155
+ private
156
+
157
+ def parse_booleans_options(new_booleans_options)
158
+ # std booleans_options
159
+ @booleans_options = {
160
+ :append => '_activated',
161
+ :field => 'booleans',
162
+ :suffixes => %w| ! = ? |,
163
+ :true => [],
164
+ :false_values => [], # e.g. [0, "0", ""],
165
+ :self => false,
166
+ :self_value => false,
167
+ :lazy => true,
168
+ }
169
+
170
+ # parse params
171
+ new_booleans_options.each{|opt_name, opt_value|
172
+ if @booleans_options.has_key? opt_name
173
+
174
+ @booleans_options[opt_name] = case opt_name
175
+
176
+ when :append # prepend underscore
177
+ opt_value ? "_#{opt_value}" : ''
178
+
179
+ when :suffixes # only allow ! = ?
180
+ (opt_value||[]) & %w| ? ! = |
181
+
182
+ when :false_values
183
+ opt_value || []
184
+
185
+ when :self_value, :lazy # normalize
186
+ !!opt_value
187
+ else
188
+ opt_value
189
+ end
190
+ end
191
+ }
192
+ end
193
+ end
194
+
195
+ module InstanceMethods #:nodoc: callbacks
196
+
197
+ # Load boolean integer from database and define the methods.
198
+ def initialize_booleans
199
+ booleans_field = self.class.booleans_options[:field]
200
+ booleans_activated = self[booleans_field] ? self[booleans_field].to_bra : []
201
+
202
+ @booleans_data = { nil => [0, false] } # self@nil
203
+
204
+ self.class.booleans_default.each{ |name, (index, value)|
205
+ name = name.to_s
206
+
207
+ each_suffix{ |suffix|
208
+ # get values
209
+ init_value = if self.new_record?
210
+ self.class.booleans_options[:true].member?(name.to_sym)
211
+ else
212
+ booleans_activated.member?(index)
213
+ end
214
+ @booleans_data[name] = [index, init_value]
215
+
216
+ # define the methods
217
+ method_name = name + self.class.booleans_options[:append] + suffix
218
+ define_boolean_method method_name, suffix, name
219
+
220
+ # define the self method
221
+ if self.class.booleans_options[:self]
222
+ # get value
223
+ if self.new_record?
224
+ cond = self.class.booleans_options[:self_value]
225
+ else
226
+ cond = booleans_activated.member? 0
227
+ end
228
+ @booleans_data[nil][1] = cond ? true : false
229
+
230
+ # get self method name
231
+ self_method = if self.class.booleans_options[:self] === true
232
+ if !self.class.booleans_options[:append] || self.class.booleans_options[:append].empty?
233
+ raise "has_many_booleans: self method activated with true, but :append is nil! Please use :self => 'method_name'"
234
+ else
235
+ self.class.booleans_options[:append][1..-1]
236
+ end
237
+ else
238
+ self.class.booleans_options[:self]
239
+ end
240
+
241
+ each_suffix{ |suffix|
242
+ define_boolean_method self_method + suffix, suffix
243
+ }
244
+ end
245
+ }
246
+ }
247
+ end
248
+
249
+ # Transform booleans to integer and save in the database.
250
+ def save_booleans
251
+ act = []
252
+ @booleans_data.each{ |_, (index, value)|
253
+ act << index if value
254
+ }
255
+ self[self.class.booleans_options[:field]] = act.to_bri
256
+ end
257
+
258
+ private
259
+
260
+ def define_boolean_method(method_name, suffix, name=nil)
261
+ if self.respond_to?(method_name)
262
+ #warn "has_many_booleans: Could not define #{method_name} for #{self.class} (method already exists)"
263
+ else
264
+ self.class.send(:define_method, method_name) do |*new_value|
265
+ case suffix
266
+ when '', '?'
267
+ # nothing's changed
268
+ when '!'
269
+ @booleans_data[name][1] = true
270
+ save_booleans if !self.class.booleans_options[:lazy]
271
+ when '='
272
+ @booleans_data[name][1] =
273
+ !!( new_value[0] && !self.class.booleans_options[:false_values].member?(new_value[0]) )
274
+ save_booleans if !self.class.booleans_options[:lazy]
275
+ end
276
+ @booleans_data[name][1]
277
+ end
278
+ end
279
+ end
280
+
281
+ def each_suffix(&block)
282
+ ( [''] + self.class.booleans_options[:suffixes] ).each{ |suffix|
283
+ yield suffix
284
+ }
285
+ end
286
+
287
+
288
+
289
+ # hook in callbacks part 2 (to not overwrite after_initialize)
290
+ #TODO run callback after booleans have been fetched
291
+ def initialize(*args, &block) # :nodoc:
292
+ super *args, &block
293
+ initialize_booleans
294
+ end
295
+
296
+ if respond_to? :initialize_copy
297
+ def initialize_copy(record)
298
+ object = super record
299
+ initialize_booleans
300
+ object
301
+ end
302
+ end
303
+
304
+
305
+ # validators
306
+ def validator_true
307
+ self.class.booleans_validators[true].each { |bool|
308
+ bd = @booleans_data[bool.to_s]
309
+ errors.add("Boolean #{bool}", "must be true") if bd && !(bd[1])
310
+ }
311
+ end
312
+
313
+ def validator_false
314
+ self.class.booleans_validators[false].each { |bool|
315
+ bd = @booleans_data[bool.to_s]
316
+ errors.add("Boolean #{bool}", "must be false") if bd && bd[1]
317
+ }
318
+ end
319
+
320
+ end
321
+
322
+ # activate class methods
323
+ def self.included(base) #:nodoc:
324
+ base.class_eval do
325
+ extend ClassMethods
326
+ end
327
+ end
328
+ end
329
+
330
+ # activate instance methods
331
+ ActiveRecord::Base.send :include, HasManyBooleans
332
+
333
+ # Copyright (c) 2010 Jan Lelis http://rbjl.net, released under the MIT license
334
+ # available at http://github.com/janlelis/has_many_booleans
335
+