mschuerig-enumerate_by 0.4.2

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