enumerate_by 0.4.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/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