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 +101 -0
- data/LICENSE +20 -0
- data/README.rdoc +117 -0
- data/Rakefile +88 -0
- data/init.rb +1 -0
- data/lib/enumerate_by.rb +366 -0
- data/lib/enumerate_by/extensions/associations.rb +109 -0
- data/lib/enumerate_by/extensions/base_conditions.rb +130 -0
- data/lib/enumerate_by/extensions/serializer.rb +117 -0
- data/lib/enumerate_by/extensions/xml_serializer.rb +41 -0
- data/test/app_root/app/models/car.rb +4 -0
- data/test/app_root/app/models/color.rb +3 -0
- data/test/app_root/db/migrate/001_create_colors.rb +12 -0
- data/test/app_root/db/migrate/002_create_cars.rb +13 -0
- data/test/factory.rb +48 -0
- data/test/test_helper.rb +28 -0
- data/test/unit/assocations_test.rb +70 -0
- data/test/unit/base_conditions_test.rb +46 -0
- data/test/unit/enumerate_by_test.rb +350 -0
- data/test/unit/json_serializer_test.rb +18 -0
- data/test/unit/serializer_test.rb +166 -0
- data/test/unit/xml_serializer_test.rb +71 -0
- metadata +87 -0
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'
|
data/lib/enumerate_by.rb
ADDED
@@ -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
|