mschuerig-enumerate_by 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,109 @@
1
+ == master
2
+
3
+ == 0.4.2 / 2009-05-03
4
+
5
+ * Fix bootstrapping without ids not working on update for certain database adapters
6
+
7
+ == 0.4.1 / 2009-05-01
8
+
9
+ * Improve #fast_bootstrap speed by 50% by using the connection directly
10
+
11
+ == 0.4.0 / 2009-04-30
12
+
13
+ * Allow cache to be cleared on a per-enumeration basis
14
+ * Add #fast_bootstrap for bootstrapping large numbers of records
15
+ * Don't require an id during bootstrap
16
+ * Allow bootstrapping to be easily added to non-enumerations
17
+ * Cache results for #exists? / #calculate
18
+ * Add the ability to skip the internal cache via Model#uncached
19
+ * Add Model#find_all_by_enumerator
20
+ * Don't internally rely on Model#[] being available since it may conflict with other plugins
21
+ * Enable caching by default for all enumerations
22
+ * Allow caching to be turned off on an application-wide basis
23
+ * Allow cache store to be configurable
24
+ * Automatically trigger in-memory caching of the enumeration's table when bootstrapping
25
+ * Add #bootstrap for automatically synchronizing the records in an enumeration's table
26
+ * Improve serialization performance
27
+ * No longer use tableless models
28
+ * Re-brand under the enumerate_by name
29
+
30
+ == 0.3.0 / 2008-12-14
31
+
32
+ * Remove the PluginAWeek namespace
33
+
34
+ == 0.2.6 / 2008-11-29
35
+
36
+ * Fix enumeration collections not being able to convert to JSON
37
+ * Add support for multiple enumeration values in finder conditions, e.g. Car.find_all_by_color(%w(red blue))
38
+
39
+ == 0.2.5 / 2008-10-26
40
+
41
+ * Fix non-ActiveRecord associations (e.g. ActiveResource) failing
42
+ * Fix reloading of associations not working
43
+ * Raise an exception if equality is performed with an invalid enumeration identifier
44
+ * Change how the base module is included to prevent namespacing conflicts
45
+
46
+ == 0.2.4 / 2008-08-31
47
+
48
+ * Add support for serialization in JSON/XML
49
+
50
+ == 0.2.3 / 2008-06-29
51
+
52
+ * Fix named scope for enumerations that belong_to other enumerations
53
+
54
+ == 0.2.2 / 2008-06-23
55
+
56
+ * Remove log files from gems
57
+
58
+ == 0.2.1 / 2008-06-22
59
+
60
+ * Improve documentation
61
+
62
+ == 0.2.0 / 2008-06-22
63
+
64
+ * Improve performance by disabling unnecessary ActiveRecord hooks including callbacks, dirty attributes, timestamps, and transactions (important for enumerations with large sets of values)
65
+ * Don't let #create silently fail
66
+ * Remove ability to reset the cache
67
+ * Improve performance by adding pre-indexing of enumeration attributes (important for enumerations with large sets of values)
68
+ * Remove support for array comparison
69
+ * Remove support for multiple enumeration attributes
70
+
71
+ == 0.1.2 / 2008-06-15
72
+
73
+ * Avoid string evaluation for dynamic methods
74
+ * Fix has_many/has_one associations improperly loading classes too early
75
+ * Add support for string and array comparison
76
+ * Use after_create/after_destroy callbacks instead of defining the callback method itself
77
+
78
+ == 0.1.1 / 2008-05-14
79
+
80
+ * Fix automatically clearing association cache when it shouldn't be
81
+
82
+ == 0.1.0 / 2008-05-05
83
+
84
+ * Add support for overriding the unique attribute that defines an enumeration e.g.
85
+
86
+ acts_as_enumeration :title
87
+ acts_as_enumeration :controller, :action
88
+
89
+ * Add support for using enumerations in has_many/has_one associations
90
+ * Add support for Rails 2.0
91
+ * Use has_finder to auto-generate finders for each enumeration value after defining a belongs_to association
92
+ * Removed support for database-backed enumerations in favor of always using virtual enumerations
93
+ * Fix enumerations failing when being reloaded
94
+ * Fix problems with setting enumeration attributes to nil
95
+ * Add inheritance support for virtual enumerations
96
+ * Add support for converting unsafe identifier names (like "red!") to their safe symbol equivalent ("red")
97
+ * Add ability to use truth accessors for determing the identifier name
98
+ * Add support for virtual enumerations that don't need to be backed by the database
99
+
100
+ == 0.0.2 / 2007-09-26
101
+
102
+ * Move test fixtures out of the test application root directory
103
+ * Convert dos newlines to unix newlines
104
+
105
+ == 0.0.1 / 2007-08-04
106
+
107
+ * Initial public release
108
+ * Add/refactor unit tests
109
+ * 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.
@@ -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
@@ -0,0 +1,96 @@
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.2'
9
+ s.platform = Gem::Platform::RUBY
10
+ s.summary = 'Adds support for declaring an ActiveRecord class as an enumeration'
11
+ s.description = s.summary
12
+
13
+ s.files = FileList['{lib,test}/**/*'] + %w(CHANGELOG.rdoc rails/init.rb LICENSE Rakefile README.rdoc) - FileList['test/app_root/{log,log/*,script,script/*}']
14
+ s.require_path = 'lib'
15
+ s.has_rdoc = true
16
+ s.test_files = Dir['test/**/*_test.rb']
17
+
18
+ s.author = 'Aaron Pfeifer'
19
+ s.email = 'aaron@pluginaweek.org'
20
+ s.homepage = 'http://www.pluginaweek.org'
21
+ s.rubyforge_project = 'pluginaweek'
22
+ end
23
+
24
+ desc 'Default: run all tests.'
25
+ task :default => :test
26
+
27
+ desc "Test the #{spec.name} plugin."
28
+ Rake::TestTask.new(:test) do |t|
29
+ t.libs << 'lib'
30
+ t.test_files = spec.test_files
31
+ t.verbose = true
32
+ end
33
+
34
+ begin
35
+ require 'rcov/rcovtask'
36
+ namespace :test do
37
+ desc "Test the #{spec.name} plugin with Rcov."
38
+ Rcov::RcovTask.new(:rcov) do |t|
39
+ t.libs << 'lib'
40
+ t.test_files = spec.test_files
41
+ t.rcov_opts << '--exclude="^(?!lib/)"'
42
+ t.verbose = true
43
+ end
44
+ end
45
+ rescue LoadError
46
+ end
47
+
48
+ desc "Generate documentation for the #{spec.name} plugin."
49
+ Rake::RDocTask.new(:rdoc) do |rdoc|
50
+ rdoc.rdoc_dir = 'rdoc'
51
+ rdoc.title = spec.name
52
+ rdoc.template = '../rdoc_template.rb'
53
+ rdoc.options << '--line-numbers' << '--inline-source'
54
+ rdoc.rdoc_files.include('README.rdoc', 'CHANGELOG.rdoc', 'LICENSE', 'lib/**/*.rb')
55
+ end
56
+
57
+ Rake::GemPackageTask.new(spec) do |p|
58
+ p.gem_spec = spec
59
+ p.need_tar = true
60
+ p.need_zip = true
61
+ end
62
+
63
+ desc 'Write gemspec file'
64
+ task :gemspec do
65
+ File.open(File.join(File.dirname(__FILE__), "#{spec.name}.gemspec"), 'w') do |f|
66
+ f.puts spec.to_yaml
67
+ end
68
+ end
69
+
70
+ desc 'Publish the beta gem.'
71
+ task :pgem => [:package] do
72
+ Rake::SshFilePublisher.new('aaron@pluginaweek.org', '/home/aaron/gems.pluginaweek.org/public/gems', 'pkg', "#{spec.name}-#{spec.version}.gem").upload
73
+ end
74
+
75
+ desc 'Publish the API documentation.'
76
+ task :pdoc => [:rdoc] do
77
+ Rake::SshDirPublisher.new('aaron@pluginaweek.org', "/home/aaron/api.pluginaweek.org/public/#{spec.name}", 'rdoc').upload
78
+ end
79
+
80
+ desc 'Publish the API docs and gem'
81
+ task :publish => [:pgem, :pdoc, :release]
82
+
83
+ desc 'Publish the release files to RubyForge.'
84
+ task :release => [:gem, :package] do
85
+ require 'rubyforge'
86
+
87
+ ruby_forge = RubyForge.new.configure
88
+ ruby_forge.login
89
+
90
+ %w(gem tgz zip).each do |ext|
91
+ file = "pkg/#{spec.name}-#{spec.version}.#{ext}"
92
+ puts "Releasing #{File.basename(file)}..."
93
+
94
+ ruby_forge.add_release(spec.rubyforge_project, spec.name, spec.version, file)
95
+ end
96
+ end
@@ -0,0 +1,395 @@
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.to_s})
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.map(&:to_s)})
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
+ ids = records.map {|record| record[:id]}.compact
257
+ delete_all(ids.any? ? ['id NOT IN (?)', ids] : nil)
258
+
259
+ # Find remaining existing records (to be updated)
260
+ existing = all.inject({}) {|existing, record| existing[record.id] = record; existing}
261
+
262
+ records.map! do |attributes|
263
+ attributes.symbolize_keys!
264
+ defaults = attributes.delete(:defaults)
265
+
266
+ # Update with new attributes
267
+ record =
268
+ if record = existing[attributes[:id]]
269
+ attributes.merge!(defaults.delete_if {|attribute, value| record.send("#{attribute}?")}) if defaults
270
+ record.attributes = attributes
271
+ record
272
+ else
273
+ attributes.merge!(defaults) if defaults
274
+ new(attributes)
275
+ end
276
+ record.id = attributes[:id]
277
+
278
+ # Force failed saves to stop execution
279
+ record.save!
280
+ record
281
+ end
282
+
283
+ records
284
+ end
285
+ end
286
+
287
+ # Quickly synchronizes the given records with the existing ones. This
288
+ # skips ActiveRecord altogether, interacting directly with the connection
289
+ # instead. As a result, certain features are not available when being
290
+ # bootstrapped, including:
291
+ # * Callbacks
292
+ # * Validations
293
+ # * Transactions
294
+ # * Timestamps
295
+ # * Dirty attributes
296
+ #
297
+ # Also note that records are created directly without creating instances
298
+ # of the model. As a result, all of the attributes for the record must
299
+ # be specified.
300
+ #
301
+ # This produces a significant performance increase when bootstrapping more
302
+ # than several hundred records.
303
+ #
304
+ # See EnumerateBy::Bootstrapped#bootstrap for information about usage.
305
+ def fast_bootstrap(*records)
306
+ # Remove records that are no longer being used
307
+ records.flatten!
308
+ ids = records.map {|record| record[:id]}.compact
309
+ delete_all(ids.any? ? ['id NOT IN (?)', ids] : nil)
310
+
311
+ # Find remaining existing records (to be updated)
312
+ quoted_table_name = self.quoted_table_name
313
+ existing = connection.select_all("SELECT * FROM #{quoted_table_name}").inject({}) {|existing, record| existing[record['id'].to_i] = record; existing}
314
+
315
+ records.each do |attributes|
316
+ attributes.stringify_keys!
317
+ if defaults = attributes.delete('defaults')
318
+ defaults.stringify_keys!
319
+ end
320
+
321
+ id = attributes['id']
322
+ if existing_attributes = existing[id]
323
+ # Record exists: Update attributes
324
+ attributes.delete('id')
325
+ attributes.merge!(defaults.delete_if {|attribute, value| !existing_attributes[attribute].nil?}) if defaults
326
+ update_all(attributes, :id => id)
327
+ else
328
+ # Record doesn't exist: create new one
329
+ attributes.merge!(defaults) if defaults
330
+ column_names = []
331
+ values = []
332
+
333
+ attributes.each do |column_name, value|
334
+ column_names << connection.quote_column_name(column_name)
335
+ values << connection.quote(value, columns_hash[column_name])
336
+ end
337
+
338
+ connection.insert(
339
+ "INSERT INTO #{quoted_table_name} (#{column_names * ', '}) VALUES(#{values * ', '})",
340
+ "#{name} Create", primary_key, id, sequence_name
341
+ )
342
+ end
343
+ end
344
+
345
+ true
346
+ end
347
+ end
348
+
349
+ module InstanceMethods
350
+ # Whether or not this record is equal to the given value. If the value is
351
+ # a String, then it is compared against the enumerator. Otherwise,
352
+ # ActiveRecord's default equality comparator is used.
353
+ def ==(arg)
354
+ arg.is_a?(String) ? self == self.class.find_by_enumerator!(arg) : super
355
+ end
356
+
357
+ # Determines whether this enumeration is in the given list.
358
+ #
359
+ # For example,
360
+ #
361
+ # color = Color.find_by_name('red') # => #<Color id: 1, name: "red">
362
+ # color.in?('green') # => false
363
+ # color.in?('red', 'green') # => true
364
+ def in?(*list)
365
+ list.any? {|item| self === item}
366
+ end
367
+
368
+ # A helper method for getting the current value of the enumerator
369
+ # attribute for this record. For example, if this record's model is
370
+ # enumerated by the attribute +name+, then this will return the current
371
+ # value for +name+.
372
+ def enumerator
373
+ send(enumerator_attribute)
374
+ end
375
+
376
+ # Stringifies the record typecasted to the enumerator value.
377
+ #
378
+ # For example,
379
+ #
380
+ # color = Color.find_by_name('red') # => #<Color id: 1, name: "red">
381
+ # color.to_s # => "red"
382
+ def to_s
383
+ to_str
384
+ end
385
+
386
+ # Add support for equality comparison with strings
387
+ def to_str
388
+ enumerator.to_s
389
+ end
390
+ end
391
+ end
392
+
393
+ ActiveRecord::Base.class_eval do
394
+ extend EnumerateBy::MacroMethods
395
+ end