enumerate_by 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,101 @@
1
+ == master
2
+
3
+ == 0.4.0 / 2009-04-30
4
+
5
+ * Allow cache to be cleared on a per-enumeration basis
6
+ * Add #fast_bootstrap for bootstrapping large numbers of records
7
+ * Don't require an id during bootstrap
8
+ * Allow bootstrapping to be easily added to non-enumerations
9
+ * Cache results for #exists? / #calculate
10
+ * Add the ability to skip the internal cache via Model#uncached
11
+ * Add Model#find_all_by_enumerator
12
+ * Don't internally rely on Model#[] being available since it may conflict with other plugins
13
+ * Enable caching by default for all enumerations
14
+ * Allow caching to be turned off on an application-wide basis
15
+ * Allow cache store to be configurable
16
+ * Automatically trigger in-memory caching of the enumeration's table when bootstrapping
17
+ * Add #bootstrap for automatically synchronizing the records in an enumeration's table
18
+ * Improve serialization performance
19
+ * No longer use tableless models
20
+ * Re-brand under the enumerate_by name
21
+
22
+ == 0.3.0 / 2008-12-14
23
+
24
+ * Remove the PluginAWeek namespace
25
+
26
+ == 0.2.6 / 2008-11-29
27
+
28
+ * Fix enumeration collections not being able to convert to JSON
29
+ * Add support for multiple enumeration values in finder conditions, e.g. Car.find_all_by_color(%w(red blue))
30
+
31
+ == 0.2.5 / 2008-10-26
32
+
33
+ * Fix non-ActiveRecord associations (e.g. ActiveResource) failing
34
+ * Fix reloading of associations not working
35
+ * Raise an exception if equality is performed with an invalid enumeration identifier
36
+ * Change how the base module is included to prevent namespacing conflicts
37
+
38
+ == 0.2.4 / 2008-08-31
39
+
40
+ * Add support for serialization in JSON/XML
41
+
42
+ == 0.2.3 / 2008-06-29
43
+
44
+ * Fix named scope for enumerations that belong_to other enumerations
45
+
46
+ == 0.2.2 / 2008-06-23
47
+
48
+ * Remove log files from gems
49
+
50
+ == 0.2.1 / 2008-06-22
51
+
52
+ * Improve documentation
53
+
54
+ == 0.2.0 / 2008-06-22
55
+
56
+ * Improve performance by disabling unnecessary ActiveRecord hooks including callbacks, dirty attributes, timestamps, and transactions (important for enumerations with large sets of values)
57
+ * Don't let #create silently fail
58
+ * Remove ability to reset the cache
59
+ * Improve performance by adding pre-indexing of enumeration attributes (important for enumerations with large sets of values)
60
+ * Remove support for array comparison
61
+ * Remove support for multiple enumeration attributes
62
+
63
+ == 0.1.2 / 2008-06-15
64
+
65
+ * Avoid string evaluation for dynamic methods
66
+ * Fix has_many/has_one associations improperly loading classes too early
67
+ * Add support for string and array comparison
68
+ * Use after_create/after_destroy callbacks instead of defining the callback method itself
69
+
70
+ == 0.1.1 / 2008-05-14
71
+
72
+ * Fix automatically clearing association cache when it shouldn't be
73
+
74
+ == 0.1.0 / 2008-05-05
75
+
76
+ * Add support for overriding the unique attribute that defines an enumeration e.g.
77
+
78
+ acts_as_enumeration :title
79
+ acts_as_enumeration :controller, :action
80
+
81
+ * Add support for using enumerations in has_many/has_one associations
82
+ * Add support for Rails 2.0
83
+ * Use has_finder to auto-generate finders for each enumeration value after defining a belongs_to association
84
+ * Removed support for database-backed enumerations in favor of always using virtual enumerations
85
+ * Fix enumerations failing when being reloaded
86
+ * Fix problems with setting enumeration attributes to nil
87
+ * Add inheritance support for virtual enumerations
88
+ * Add support for converting unsafe identifier names (like "red!") to their safe symbol equivalent ("red")
89
+ * Add ability to use truth accessors for determing the identifier name
90
+ * Add support for virtual enumerations that don't need to be backed by the database
91
+
92
+ == 0.0.2 / 2007-09-26
93
+
94
+ * Move test fixtures out of the test application root directory
95
+ * Convert dos newlines to unix newlines
96
+
97
+ == 0.0.1 / 2007-08-04
98
+
99
+ * Initial public release
100
+ * Add/refactor unit tests
101
+ * Add documentation
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2006-2009 Aaron Pfeifer
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,117 @@
1
+ = enumerate_by
2
+
3
+ +enumerate_by+ adds support for declaring an ActiveRecord class as an
4
+ enumeration.
5
+
6
+ == Resources
7
+
8
+ API
9
+
10
+ * http://api.pluginaweek.org/enumerate_by
11
+
12
+ Bugs
13
+
14
+ * http://pluginaweek.lighthouseapp.com/projects/13265-enumerate_by
15
+
16
+ Development
17
+
18
+ * http://github.com/pluginaweek/enumerate_by
19
+
20
+ Source
21
+
22
+ * git://github.com/pluginaweek/enumerate_by.git
23
+
24
+ == Description
25
+
26
+ Support for enumerations is dependent on the type of database you use. For
27
+ example, MySQL has native support for the enum data type. However, there is
28
+ no native Rails support for defining enumerations and the associations between
29
+ it and other models in the application.
30
+
31
+ In addition, enumerations may often have more complex data and/or functionality
32
+ associated with it that cannot simply be describe in a single column.
33
+
34
+ enumerate_by adds support for pseudo-enumerations in Rails by allowing the
35
+ enumeration's records to be defined in code, but continuing to express the
36
+ relationship between an enumeration and other models using ActiveRecord
37
+ associations. The important thing to remember, however, is that while the
38
+ associations exist, the enumerator is always used outside of the application,
39
+ while either the enumerator or the enumerator's record would be referenced
40
+ *within* the application. This means that you would reference a Color record
41
+ via it's enumerator (such as "red") everywhere in the code (conditions,
42
+ assigning associations, forms, etc.), but it would always be stored in the
43
+ database as a true association with the integer value of 1.
44
+
45
+ == Usage
46
+
47
+ class Color < ActiveRecord::Base
48
+ enumerate_by :name
49
+
50
+ belongs_to :group, :class_name => 'ColorGroup'
51
+ has_many :cars
52
+
53
+ bootstrap(
54
+ {:id => 1, :name => 'red', :group => 'RGB'},
55
+ {:id => 2, :name => 'blue', :group => 'RGB'},
56
+ {:id => 3, :name => 'green', :group => 'RGB'},
57
+ {:id => 4, :name => 'cyan', :group => 'CMYK'}
58
+ )
59
+ end
60
+
61
+ class ColorGroup < ActiveRecord::Base
62
+ enumerate_by :name
63
+
64
+ has_many :colors, :foreign_key => 'group_id'
65
+
66
+ bootstrap(
67
+ {:id => 1, :name => 'RGB'},
68
+ {:id => 2, :name => 'CMYK'}
69
+ )
70
+ end
71
+
72
+ class Car < ActiveRecord::Base
73
+ belongs_to :color
74
+ end
75
+
76
+ Each of the above models is backed by the database with its own table. Both
77
+ the Color and ColorGroup enumerations automatically synchronize with the
78
+ records in the database based on what's define in their bootstrap data.
79
+
80
+ The enumerations and their associations can be then be used like so:
81
+
82
+ car = Car.create(:color => 'red') # => #<Car id: 1, color_id: 1>
83
+ car.color # => #<Color id: 1, name: "red">
84
+ car.color == 'red' # => true
85
+ car.color.name # => "red"
86
+
87
+ car.color = 'blue'
88
+ car.save # => true
89
+ car # => #<Car id: 1, color_id: 2>
90
+
91
+ # Serialization
92
+ car.to_json # => "{id: 1, color: \"blue\"}"
93
+ car.to_xml # => "<car><id type=\"integer\">1</id><color>blue</color></car>"
94
+
95
+ # Lookup
96
+ Car.with_color('blue') # => [#<Car id: 1, color_id: 2>]
97
+ car = Car.find_by_color('blue') # => #<Car id: 1, color_id: 2>
98
+ car.color == Color['blue'] # => true
99
+
100
+ As mentioned previously, the important thing to note from the above example is
101
+ that from an external perspective, "color" is simply an attribute on the Car.
102
+ However, it's backed by a more complex association and model that allows Color
103
+ to include advanced functionality that would normally not be possible with a
104
+ simple attribute.
105
+
106
+ == Testing
107
+
108
+ Before you can run any tests, the following gem must be installed:
109
+ * plugin_test_helper[http://github.com/pluginaweek/plugin_test_helper]
110
+
111
+ To run against a specific version of Rails:
112
+
113
+ rake test RAILS_FRAMEWORK_ROOT=/path/to/rails
114
+
115
+ == Dependencies
116
+
117
+ * Rails 2.1 or later
data/Rakefile ADDED
@@ -0,0 +1,88 @@
1
+ require 'rake/testtask'
2
+ require 'rake/rdoctask'
3
+ require 'rake/gempackagetask'
4
+ require 'rake/contrib/sshpublisher'
5
+
6
+ spec = Gem::Specification.new do |s|
7
+ s.name = 'enumerate_by'
8
+ s.version = '0.4.0'
9
+ s.platform = Gem::Platform::RUBY
10
+ s.summary = 'Adds support for declaring an ActiveRecord class as an enumeration'
11
+
12
+ s.files = FileList['{lib,test}/**/*'] + %w(CHANGELOG.rdoc init.rb LICENSE Rakefile README.rdoc) - FileList['test/app_root/{log,log/*,script,script/*}']
13
+ s.require_path = 'lib'
14
+ s.has_rdoc = true
15
+ s.test_files = Dir['test/**/*_test.rb']
16
+
17
+ s.author = 'Aaron Pfeifer'
18
+ s.email = 'aaron@pluginaweek.org'
19
+ s.homepage = 'http://www.pluginaweek.org'
20
+ s.rubyforge_project = 'pluginaweek'
21
+ end
22
+
23
+ desc 'Default: run all tests.'
24
+ task :default => :test
25
+
26
+ desc "Test the #{spec.name} plugin."
27
+ Rake::TestTask.new(:test) do |t|
28
+ t.libs << 'lib'
29
+ t.test_files = spec.test_files
30
+ t.verbose = true
31
+ end
32
+
33
+ begin
34
+ require 'rcov/rcovtask'
35
+ namespace :test do
36
+ desc "Test the #{spec.name} plugin with Rcov."
37
+ Rcov::RcovTask.new(:rcov) do |t|
38
+ t.libs << 'lib'
39
+ t.test_files = spec.test_files
40
+ t.rcov_opts << '--exclude="^(?!lib/)"'
41
+ t.verbose = true
42
+ end
43
+ end
44
+ rescue LoadError
45
+ end
46
+
47
+ desc "Generate documentation for the #{spec.name} plugin."
48
+ Rake::RDocTask.new(:rdoc) do |rdoc|
49
+ rdoc.rdoc_dir = 'rdoc'
50
+ rdoc.title = spec.name
51
+ rdoc.template = '../rdoc_template.rb'
52
+ rdoc.options << '--line-numbers' << '--inline-source'
53
+ rdoc.rdoc_files.include('README.rdoc', 'CHANGELOG.rdoc', 'LICENSE', 'lib/**/*.rb')
54
+ end
55
+
56
+ Rake::GemPackageTask.new(spec) do |p|
57
+ p.gem_spec = spec
58
+ p.need_tar = true
59
+ p.need_zip = true
60
+ end
61
+
62
+ desc 'Publish the beta gem.'
63
+ task :pgem => [:package] do
64
+ Rake::SshFilePublisher.new('aaron@pluginaweek.org', '/home/aaron/gems.pluginaweek.org/public/gems', 'pkg', "#{spec.name}-#{spec.version}.gem").upload
65
+ end
66
+
67
+ desc 'Publish the API documentation.'
68
+ task :pdoc => [:rdoc] do
69
+ Rake::SshDirPublisher.new('aaron@pluginaweek.org', "/home/aaron/api.pluginaweek.org/public/#{spec.name}", 'rdoc').upload
70
+ end
71
+
72
+ desc 'Publish the API docs and gem'
73
+ task :publish => [:pgem, :pdoc, :release]
74
+
75
+ desc 'Publish the release files to RubyForge.'
76
+ task :release => [:gem, :package] do
77
+ require 'rubyforge'
78
+
79
+ ruby_forge = RubyForge.new.configure
80
+ ruby_forge.login
81
+
82
+ %w(gem tgz zip).each do |ext|
83
+ file = "pkg/#{spec.name}-#{spec.version}.#{ext}"
84
+ puts "Releasing #{File.basename(file)}..."
85
+
86
+ ruby_forge.add_release(spec.rubyforge_project, spec.name, spec.version, file)
87
+ end
88
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'enumerate_by'
@@ -0,0 +1,366 @@
1
+ require 'enumerate_by/extensions/associations'
2
+ require 'enumerate_by/extensions/base_conditions'
3
+ require 'enumerate_by/extensions/serializer'
4
+ require 'enumerate_by/extensions/xml_serializer'
5
+
6
+ # An enumeration defines a finite set of enumerators which (often) have no
7
+ # numerical order. This extension provides a general technique for using
8
+ # ActiveRecord classes to define enumerations.
9
+ module EnumerateBy
10
+ # Whether to enable enumeration caching (default is true)
11
+ mattr_accessor :perform_caching
12
+ self.perform_caching = true
13
+
14
+ module MacroMethods
15
+ def self.extended(base) #:nodoc:
16
+ base.class_eval do
17
+ # Tracks which associations are backed by an enumeration
18
+ # {"foreign key" => "association name"}
19
+ class_inheritable_accessor :enumeration_associations
20
+ self.enumeration_associations = {}
21
+ end
22
+ end
23
+
24
+ # Indicates that this class is an enumeration.
25
+ #
26
+ # The default attribute used to enumerate the class is +name+. You can
27
+ # override this by specifying a custom attribute that will be used to
28
+ # *uniquely* reference a record.
29
+ #
30
+ # *Note* that a presence and uniqueness validation is automatically
31
+ # defined for the given attribute since all records must have this value
32
+ # in order to be properly enumerated.
33
+ #
34
+ # Configuration options:
35
+ # * <tt>:cache</tt> - Whether to cache all finder queries for this
36
+ # enumeration. Default is true.
37
+ #
38
+ # == Defining enumerators
39
+ #
40
+ # The enumerators of the class uniquely identify each record in the
41
+ # table. The enumerator value is based on the attribute described above.
42
+ # In scenarios where the records are managed in code (like colors,
43
+ # countries, states, etc.), records can be automatically synchronized
44
+ # via #bootstrap.
45
+ #
46
+ # == Accessing records
47
+ #
48
+ # The actual records for an enumeration can be accessed via shortcut
49
+ # helpers like so:
50
+ #
51
+ # Color['red'] # => #<Color id: 1, name: "red">
52
+ # Color['green'] # => #<Color id: 2, name: "green">
53
+ #
54
+ # When caching is enabled, these lookup queries are cached so that there
55
+ # is no performance hit.
56
+ #
57
+ # == Associations
58
+ #
59
+ # When using enumerations together with +belongs_to+ associations, the
60
+ # enumerator value can be used as a shortcut for assigning the
61
+ # association.
62
+ #
63
+ # In addition, the enumerator value is automatically used during
64
+ # serialization (xml and json) of the associated record instead of the
65
+ # foreign key for the association.
66
+ #
67
+ # For more information about how to use enumerations with associations,
68
+ # see EnumerateBy::Extensions::Associations and EnumerateBy::Extensions::Serializer.
69
+ #
70
+ # === Finders
71
+ #
72
+ # In order to be consistent by always using enumerators to reference
73
+ # records, a set of finder extensions are added to allow searching
74
+ # for records like so:
75
+ #
76
+ # class Car < ActiveRecord::Base
77
+ # belongs_to :color
78
+ # end
79
+ #
80
+ # Car.find_by_color('red')
81
+ # Car.all(:conditions => {:color => 'red'})
82
+ #
83
+ # For more information about finders, see EnumerateBy::Extensions::BaseConditions.
84
+ def enumerate_by(attribute = :name, options = {})
85
+ options.reverse_merge!(:cache => true)
86
+ options.assert_valid_keys(:cache)
87
+
88
+ extend EnumerateBy::ClassMethods
89
+ extend EnumerateBy::Bootstrapped
90
+ include EnumerateBy::InstanceMethods
91
+
92
+ # The attribute representing a record's enumerator
93
+ cattr_accessor :enumerator_attribute
94
+ self.enumerator_attribute = attribute
95
+
96
+ # Whether to perform caching of enumerators within finder queries
97
+ cattr_accessor :perform_enumerator_caching
98
+ self.perform_enumerator_caching = options[:cache]
99
+
100
+ # The cache store to use for queries (default is a memory store)
101
+ cattr_accessor :enumerator_cache_store
102
+ self.enumerator_cache_store = ActiveSupport::Cache::MemoryStore.new
103
+
104
+ validates_presence_of attribute
105
+ validates_uniqueness_of attribute
106
+ end
107
+
108
+ # Does this class define an enumeration? Always false.
109
+ def enumeration?
110
+ false
111
+ end
112
+ end
113
+
114
+ module ClassMethods
115
+ # Does this class define an enumeration? Always true.
116
+ def enumeration?
117
+ true
118
+ end
119
+
120
+ # Finds the record that is associated with the given enumerator. The
121
+ # attribute that defines the enumerator is based on what was specified
122
+ # when calling +enumerate_by+.
123
+ #
124
+ # For example,
125
+ #
126
+ # Color.find_by_enumerator('red') # => #<Color id: 1, name: "red">
127
+ # Color.find_by_enumerator('invalid') # => nil
128
+ def find_by_enumerator(enumerator)
129
+ first(:conditions => {enumerator_attribute => enumerator})
130
+ end
131
+
132
+ # Finds the record that is associated with the given enumerator. If no
133
+ # record is found, then an ActiveRecord::RecordNotFound exception is
134
+ # raised.
135
+ #
136
+ # For example,
137
+ #
138
+ # Color['red'] # => #<Color id: 1, name: "red">
139
+ # Color['invalid'] # => ActiveRecord::RecordNotFound: Couldn't find Color with name "red"
140
+ #
141
+ # To avoid raising an exception on invalid enumerators, use +find_by_enumerator+.
142
+ def find_by_enumerator!(enumerator)
143
+ find_by_enumerator(enumerator) || raise(ActiveRecord::RecordNotFound, "Couldn't find #{name} with #{enumerator_attribute} #{enumerator.inspect}")
144
+ end
145
+ alias_method :[], :find_by_enumerator!
146
+
147
+ # Finds records with the given enumerators.
148
+ #
149
+ # For example,
150
+ #
151
+ # Color.find_all_by_enumerator('red', 'green') # => [#<Color id: 1, name: "red">, #<Color id: 1, name: "green">]
152
+ # Color.find_all_by_enumerator('invalid') # => []
153
+ def find_all_by_enumerator(enumerators)
154
+ all(:conditions => {enumerator_attribute => enumerators})
155
+ end
156
+
157
+ # Finds records with the given enumerators. If no record is found for a
158
+ # particular enumerator, then an ActiveRecord::RecordNotFound exception
159
+ # is raised.
160
+ #
161
+ # For Example,
162
+ #
163
+ # Color.find_all_by_enumerator!('red', 'green') # => [#<Color id: 1, name: "red">, #<Color id: 1, name: "green">]
164
+ # Color.find_all_by_enumerator!('invalid') # => ActiveRecord::RecordNotFound: Couldn't find Color with name(s) "invalid"
165
+ #
166
+ # To avoid raising an exception on invalid enumerators, use +find_all_by_enumerator+.
167
+ def find_all_by_enumerator!(enumerators)
168
+ records = find_all_by_enumerator(enumerators)
169
+ missing = [enumerators].flatten - records.map(&:enumerator)
170
+ missing.empty? ? records : raise(ActiveRecord::RecordNotFound, "Couldn't find #{name} with #{enumerator_attribute}(s) #{missing.map(&:inspect).to_sentence}")
171
+ end
172
+
173
+ # Adds support for looking up results from the enumeration cache for
174
+ # before querying the database.
175
+ #
176
+ # This allows for enumerations to permanently cache find queries, avoiding
177
+ # unnecessary lookups in the database.
178
+ [:find_by_sql, :exists?, :calculate].each do |method|
179
+ define_method(method) do |*args|
180
+ if EnumerateBy.perform_caching && perform_enumerator_caching
181
+ enumerator_cache_store.fetch([method] + args) { super }
182
+ else
183
+ super
184
+ end
185
+ end
186
+ end
187
+
188
+ # Temporarily disables the enumeration cache (as well as the query cache)
189
+ # within the context of the given block if the enumeration is configured
190
+ # to allow caching.
191
+ def uncached
192
+ old = perform_enumerator_caching
193
+ self.perform_enumerator_caching = false
194
+ super
195
+ ensure
196
+ self.perform_enumerator_caching = old
197
+ end
198
+ end
199
+
200
+ module Bootstrapped
201
+ # Synchronizes the given records with existing ones. This ensures that
202
+ # only the correct and most up-to-date records exist in the database.
203
+ # The sync process is as follows:
204
+ # * Any existing record that doesn't match is deleted
205
+ # * Existing records with matches are updated based on the given attributes for that record
206
+ # * Records that don't exist are created
207
+ #
208
+ # To create records that can be referenced elsewhere in the database, an
209
+ # id should always be specified. Otherwise, records may change id each
210
+ # time they are bootstrapped.
211
+ #
212
+ # == Examples
213
+ #
214
+ # class Color < ActiveRecord::Base
215
+ # enumerate_by :name
216
+ #
217
+ # bootstrap(
218
+ # {:id => 1, :name => 'red'},
219
+ # {:id => 2, :name => 'blue'},
220
+ # {:id => 3, :name => 'green'}
221
+ # )
222
+ # end
223
+ #
224
+ # In the above model, the +colors+ table will be synchronized with the 3
225
+ # records passed into the +bootstrap+ helper. Any existing records that
226
+ # do not match those 3 are deleted. Otherwise, they are either created or
227
+ # updated with the attributes specified.
228
+ #
229
+ # == Defaults
230
+ #
231
+ # In addition to *always* synchronizing certain attributes, an additional
232
+ # +defaults+ option can be given to indicate that certain attributes
233
+ # should only be synchronized if they haven't been modified in the
234
+ # database.
235
+ #
236
+ # For example,
237
+ #
238
+ # class Color < ActiveRecord::Base
239
+ # enumerate_by :name
240
+ #
241
+ # bootstrap(
242
+ # {:id => 1, :name => 'red', :defaults => {:html => '#f00'}},
243
+ # {:id => 2, :name => 'blue', :defaults => {:html => '#0f0'}},
244
+ # {:id => 3, :name => 'green', :defaults => {:html => '#00f'}}
245
+ # )
246
+ # end
247
+ #
248
+ # In the above model, the +name+ attribute will always be updated on
249
+ # existing records in the database. However, the +html+ attribute will
250
+ # only be synchronized if the attribute is nil in the database.
251
+ # Otherwise, any changes to that column remain there.
252
+ def bootstrap(*records)
253
+ uncached do
254
+ # Remove records that are no longer being used
255
+ records.flatten!
256
+ delete_all(['id NOT IN (?)', records.map {|record| record[:id]}])
257
+ existing = all.inject({}) {|existing, record| existing[record.id] = record; existing}
258
+
259
+ records.map! do |attributes|
260
+ attributes.symbolize_keys!
261
+ defaults = attributes.delete(:defaults)
262
+
263
+ # Update with new attributes
264
+ record = !existing.include?(attributes[:id]) ? new(attributes) : begin
265
+ record = existing[attributes[:id]]
266
+ record.attributes = attributes
267
+ record
268
+ end
269
+ record.id = attributes[:id]
270
+
271
+ # Only update defaults if they aren't already specified
272
+ defaults.each {|attribute, value| record[attribute] = value unless record.send("#{attribute}?")} if defaults
273
+
274
+ # Force failed saves to stop execution
275
+ record.save!
276
+ record
277
+ end
278
+
279
+ records
280
+ end
281
+ end
282
+
283
+ # Quickly synchronizes the given records with the existing ones. This
284
+ # disables certain features of ActiveRecord in order to provide a speed
285
+ # boost, including:
286
+ # * Callbacks
287
+ # * Validations
288
+ # * Timestamps
289
+ # * Dirty attributes
290
+ #
291
+ # This produces a noticeable performance increase when bootstrapping more
292
+ # than several hundred records.
293
+ #
294
+ # See EnumerateBy::Bootstrapped#bootstrap for information about usage.
295
+ def fast_bootstrap(*records)
296
+ features = {:callbacks => %w(create create_or_update valid?), :dirty => %w(write_attribute), :validation => %w(save save!)}
297
+ features.each do |feature, methods|
298
+ methods.each do |method|
299
+ method, punctuation = method.sub(/([?!=])$/, ''), $1
300
+ alias_method "#{method}_without_bootstrap#{punctuation}", "#{method}#{punctuation}"
301
+ alias_method "#{method}#{punctuation}", "#{method}_without_#{feature}#{punctuation}"
302
+ end
303
+ end
304
+ original_record_timestamps = self.record_timestamps
305
+ self.record_timestamps = false
306
+
307
+ bootstrap(*records)
308
+ ensure
309
+ features.each do |feature, methods|
310
+ methods.each do |method|
311
+ method, punctuation = method.sub(/([?!=])$/, ''), $1
312
+ alias_method "#{method}_without_#{feature}#{punctuation}", "#{method}#{punctuation}"
313
+ alias_method "#{method}#{punctuation}", "#{method}_without_bootstrap#{punctuation}"
314
+ end
315
+ end
316
+ self.record_timestamps = original_record_timestamps
317
+ end
318
+ end
319
+
320
+ module InstanceMethods
321
+ # Whether or not this record is equal to the given value. If the value is
322
+ # a String, then it is compared against the enumerator. Otherwise,
323
+ # ActiveRecord's default equality comparator is used.
324
+ def ==(arg)
325
+ arg.is_a?(String) ? self == self.class.find_by_enumerator!(arg) : super
326
+ end
327
+
328
+ # Determines whether this enumeration is in the given list.
329
+ #
330
+ # For example,
331
+ #
332
+ # color = Color.find_by_name('red') # => #<Color id: 1, name: "red">
333
+ # color.in?('green') # => false
334
+ # color.in?('red', 'green') # => true
335
+ def in?(*list)
336
+ list.any? {|item| self === item}
337
+ end
338
+
339
+ # A helper method for getting the current value of the enumerator
340
+ # attribute for this record. For example, if this record's model is
341
+ # enumerated by the attribute +name+, then this will return the current
342
+ # value for +name+.
343
+ def enumerator
344
+ send(enumerator_attribute)
345
+ end
346
+
347
+ # Stringifies the record typecasted to the enumerator value.
348
+ #
349
+ # For example,
350
+ #
351
+ # color = Color.find_by_name('red') # => #<Color id: 1, name: "red">
352
+ # color.to_s # => "red"
353
+ def to_s
354
+ to_str
355
+ end
356
+
357
+ # Add support for equality comparison with strings
358
+ def to_str
359
+ enumerator.to_s
360
+ end
361
+ end
362
+ end
363
+
364
+ ActiveRecord::Base.class_eval do
365
+ extend EnumerateBy::MacroMethods
366
+ end