inverse_of 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 George Ogata
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/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 [name of plugin creator]
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.markdown ADDED
@@ -0,0 +1,72 @@
1
+ # Inverse Of
2
+
3
+ Backport of ActiveRecord 2.3.6's inverse associations feature.
4
+
5
+ class Parent < ActiveRecord::Base
6
+ has_one :child, :inverse_of => :parent
7
+ end
8
+
9
+ class Child < ActiveRecord::Base
10
+ belongs_to :parent, :inverse_of => :child
11
+ end
12
+
13
+ Now:
14
+
15
+ parent = Parent.first
16
+ parent.child.parent.equal?(parent) # true
17
+
18
+ Without inverse associations, the last line is false. Although
19
+ ActiveRecord does perform database query caching, you still suffer the
20
+ overhead of creating unnecessary ActiveRecord objects.
21
+
22
+ Inverse associations are also necessary to support validations on the
23
+ parent which must fire before the parent is saved. For example:
24
+
25
+ class Parent < ActiveRecord::Base
26
+ has_one :child
27
+ accepts_nested_attributes_for :child
28
+ end
29
+
30
+ class Child < ActiveRecord::Base
31
+ belongs_to :parent
32
+ validates_presence_of :parent_id
33
+ end
34
+
35
+ Parent.new(:child_attributes => {:name => 'child name'}).valid?
36
+
37
+ Here, the parent object fails validation, because the parent_id is not
38
+ set yet. If inverses are declared, and the validation runs on the
39
+ association rather than the ID, the validation passes.
40
+
41
+ class Parent < ActiveRecord::Base
42
+ has_one :child, :inverse_of => :parent
43
+ accepts_nested_attributes_for :child
44
+ end
45
+
46
+ class Child < ActiveRecord::Base
47
+ belongs_to :parent
48
+ validates_presence_of :parent
49
+ end
50
+
51
+ Note that running the Child validations will now require loading the
52
+ Parent if it's not already loaded. On the upside, it will now also
53
+ validate that the parent_id actually points to an existing record.
54
+ However, you may wish to define a custom validation to check only the
55
+ ID if the association is not set to avoid the extra database hit.
56
+
57
+ Inverse Of supports `has_one`, `has_many`, and `belongs_to`
58
+ associations, including polymorphic `belongs_to`. Behavior should
59
+ exactly match Rails 2.3.6; please file any differences
60
+ [as a bug](http://github.com/oggy/inverse_of/issues).
61
+
62
+ ## Contributing
63
+
64
+ * Bug reports: http://github.com/oggy/inverse_of/issues
65
+ * Source: http://github.com/oggy/inverse_of
66
+ * Patches: Fork on Github, send pull request.
67
+ * Ensure patch includes tests.
68
+ * Leave the version alone, or bump it in a separate commit.
69
+
70
+ ## Copyright
71
+
72
+ Copyright (c) 2009-2010 George Ogata, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,38 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "inverse_of"
8
+ gem.summary = "Backport of ActiveRecord 2.3.6's inverse associations."
9
+ gem.description = "Backport of ActiveRecord 2.3.6's inverse associations."
10
+ gem.email = "george.ogata@gmail.com"
11
+ gem.homepage = "http://github.com/oggy/inverse_of"
12
+ gem.authors = ["George Ogata"]
13
+ end
14
+ Jeweler::GemcutterTasks.new
15
+ rescue LoadError
16
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
17
+ end
18
+
19
+ require 'rake/testtask'
20
+
21
+ desc "Run the test/unit tests for inverse associations, backported from ActiveRecord 2.3.6."
22
+ Rake::TestTask.new(:test => :check_dependencies) do |test|
23
+ test.libs << 'lib' << 'test'
24
+ test.pattern = 'test/**/*_test.rb'
25
+ test.verbose = true
26
+ end
27
+
28
+ task :default => :test
29
+
30
+ require 'rake/rdoctask'
31
+ Rake::RDocTask.new do |rdoc|
32
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
33
+
34
+ rdoc.rdoc_dir = 'rdoc'
35
+ rdoc.title = "inverse_of #{version}"
36
+ rdoc.rdoc_files.include('README*')
37
+ rdoc.rdoc_files.include('lib/**/*.rb')
38
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
data/install.rb ADDED
@@ -0,0 +1 @@
1
+ # Install hook code here
@@ -0,0 +1,76 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{inverse_of}
8
+ s.version = "0.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["George Ogata"]
12
+ s.date = %q{2010-02-05}
13
+ s.description = %q{Backport of ActiveRecord 2.3.6's inverse associations.}
14
+ s.email = %q{george.ogata@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.markdown"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "MIT-LICENSE",
24
+ "README.markdown",
25
+ "Rakefile",
26
+ "VERSION",
27
+ "install.rb",
28
+ "inverse_of.gemspec",
29
+ "lib/inverse_of.rb",
30
+ "rails/init.rb",
31
+ "tasks/inverse_of_tasks.rake",
32
+ "test/cases/helper.rb",
33
+ "test/cases/inverse_associations_test.rb",
34
+ "test/fixtures/clubs.yml",
35
+ "test/fixtures/faces.yml",
36
+ "test/fixtures/interests.yml",
37
+ "test/fixtures/men.yml",
38
+ "test/fixtures/sponsors.yml",
39
+ "test/fixtures/zines.yml",
40
+ "test/models/club.rb",
41
+ "test/models/face.rb",
42
+ "test/models/interest.rb",
43
+ "test/models/man.rb",
44
+ "test/models/sponsor.rb",
45
+ "test/models/zine.rb",
46
+ "test/schema/schema.rb",
47
+ "uninstall.rb"
48
+ ]
49
+ s.homepage = %q{http://github.com/oggy/inverse_of}
50
+ s.rdoc_options = ["--charset=UTF-8"]
51
+ s.require_paths = ["lib"]
52
+ s.rubygems_version = %q{1.3.5}
53
+ s.summary = %q{Backport of ActiveRecord 2.3.6's inverse associations.}
54
+ s.test_files = [
55
+ "test/cases/helper.rb",
56
+ "test/cases/inverse_associations_test.rb",
57
+ "test/models/club.rb",
58
+ "test/models/face.rb",
59
+ "test/models/interest.rb",
60
+ "test/models/man.rb",
61
+ "test/models/sponsor.rb",
62
+ "test/models/zine.rb",
63
+ "test/schema/schema.rb"
64
+ ]
65
+
66
+ if s.respond_to? :specification_version then
67
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
68
+ s.specification_version = 3
69
+
70
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
71
+ else
72
+ end
73
+ else
74
+ end
75
+ end
76
+
data/lib/inverse_of.rb ADDED
@@ -0,0 +1,293 @@
1
+ if ([ActiveRecord::VERSION::MAJOR, ActiveRecord::VERSION::MINOR, ActiveRecord::VERSION::TINY] <=> [2, 3, 6]) < 0
2
+ module InverseOf
3
+ class InverseOfAssociationNotFoundError < ActiveRecord::ActiveRecordError #:nodoc:
4
+ def initialize(reflection, associated_class = nil)
5
+ super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})")
6
+ end
7
+ end
8
+
9
+ module Reflection
10
+ def self.included(base)
11
+ base::AssociationReflection.send :include, AssociationReflection
12
+ base::ThroughReflection.send :include, ThroughReflection
13
+ end
14
+
15
+ module AssociationReflection
16
+ def self.included(base)
17
+ base.alias_method_chain :check_validity!, :inverse_of
18
+ end
19
+
20
+ def check_validity_with_inverse_of!
21
+ check_validity_of_inverse!
22
+ check_validity_without_inverse_of!
23
+ end
24
+
25
+ def check_validity_of_inverse!
26
+ unless options[:polymorphic]
27
+ if has_inverse? && inverse_of.nil?
28
+ raise InverseOfAssociationNotFoundError.new(self)
29
+ end
30
+ end
31
+ end
32
+
33
+ def has_inverse?
34
+ !@options[:inverse_of].nil?
35
+ end
36
+
37
+ def inverse_of
38
+ if has_inverse?
39
+ @inverse_of ||= klass.reflect_on_association(options[:inverse_of])
40
+ end
41
+ end
42
+
43
+ def polymorphic_inverse_of(associated_class)
44
+ if has_inverse?
45
+ if inverse_relationship = associated_class.reflect_on_association(options[:inverse_of])
46
+ inverse_relationship
47
+ else
48
+ raise InverseOfAssociationNotFoundError.new(self, associated_class)
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ module ThroughReflection
55
+ def self.included(base)
56
+ base.alias_method_chain :check_validity!, :inverse_of
57
+ end
58
+
59
+ def check_validity_with_inverse_of!
60
+ check_validity_of_inverse!
61
+ check_validity_without_inverse_of!
62
+ end
63
+ end
64
+ end
65
+
66
+ module Associations
67
+ module AssociationCollection
68
+ def self.included(base)
69
+ base.alias_method_chain :find_target, :inverse_of
70
+ base.alias_method_chain :add_record_to_target_with_callbacks, :inverse_of
71
+ end
72
+
73
+ def find_target_with_inverse_of
74
+ records = find_target_without_inverse_of
75
+ records.each do |record|
76
+ set_inverse_instance(record, @owner)
77
+ end
78
+ records
79
+ end
80
+
81
+ def add_record_to_target_with_callbacks_with_inverse_of(record, &block)
82
+ record = add_record_to_target_with_callbacks_without_inverse_of(record, &block)
83
+ set_inverse_instance(record, @owner)
84
+ record
85
+ end
86
+ end
87
+
88
+ module AssociationProxy
89
+ def self.included(base)
90
+ base.alias_method_chain :initialize, :inverse_of
91
+ end
92
+
93
+ def initialize_with_inverse_of(owner, reflection)
94
+ reflection.check_validity!
95
+ initialize_without_inverse_of(owner, reflection)
96
+ end
97
+
98
+ private
99
+
100
+ def set_inverse_instance(record, instance)
101
+ return if record.nil? || !we_can_set_the_inverse_on_this?(record)
102
+ inverse_relationship = @reflection.inverse_of
103
+ unless inverse_relationship.nil?
104
+ record.send(:"set_#{inverse_relationship.name}_target", instance)
105
+ end
106
+ end
107
+
108
+ # Override in subclasses
109
+ def we_can_set_the_inverse_on_this?(record)
110
+ false
111
+ end
112
+ end
113
+
114
+ module BelongsToAssociation
115
+ def self.included(base)
116
+ base.alias_method_chain :replace, :inverse_of
117
+ base.alias_method_chain :find_target, :inverse_of
118
+ end
119
+
120
+ def replace_with_inverse_of(record)
121
+ replace_without_inverse_of(record)
122
+ set_inverse_instance(record, @owner)
123
+ record
124
+ end
125
+
126
+ def find_target_with_inverse_of
127
+ target = find_target_without_inverse_of and
128
+ set_inverse_instance(target, @owner)
129
+ target
130
+ end
131
+
132
+ # NOTE - for now, we're only supporting inverse setting from belongs_to back onto
133
+ # has_one associations.
134
+ def we_can_set_the_inverse_on_this?(record)
135
+ @reflection.has_inverse? && @reflection.inverse_of.macro == :has_one
136
+ end
137
+ end
138
+
139
+ module BelongsToPolymorphicAssociation
140
+ def self.included(base)
141
+ base.alias_method_chain :replace, :inverse_of
142
+ base.alias_method_chain :find_target, :inverse_of
143
+ end
144
+
145
+ def replace_with_inverse_of(record)
146
+ replace_without_inverse_of(record)
147
+ set_inverse_instance(record, @owner)
148
+ record
149
+ end
150
+
151
+ def find_target_with_inverse_of
152
+ target = find_target_without_inverse_of
153
+ set_inverse_instance(target, @owner)
154
+ target
155
+ end
156
+
157
+ # NOTE - for now, we're only supporting inverse setting from belongs_to back onto
158
+ # has_one associations.
159
+ def we_can_set_the_inverse_on_this?(record)
160
+ if @reflection.has_inverse?
161
+ inverse_association = @reflection.polymorphic_inverse_of(record.class)
162
+ inverse_association && inverse_association.macro == :has_one
163
+ else
164
+ false
165
+ end
166
+ end
167
+
168
+ def set_inverse_instance(record, instance)
169
+ return if record.nil? || !we_can_set_the_inverse_on_this?(record)
170
+ inverse_relationship = @reflection.polymorphic_inverse_of(record.class)
171
+ unless inverse_relationship.nil?
172
+ record.send(:"set_#{inverse_relationship.name}_target", instance)
173
+ end
174
+ end
175
+ end
176
+
177
+ module HasManyAssociation
178
+ def we_can_set_the_inverse_on_this?(record)
179
+ inverse = @reflection.inverse_of
180
+ return !inverse.nil?
181
+ end
182
+ end
183
+
184
+ module HasManyThroughAssociation
185
+ def initialize(owner, reflection)
186
+ super
187
+ end
188
+
189
+ # NOTE - not sure that we can actually cope with inverses here
190
+ def we_can_set_the_inverse_on_this?(record)
191
+ false
192
+ end
193
+ end
194
+
195
+ module HasOneAssociation
196
+ def self.included(base)
197
+ base.alias_method_chain :find_target, :inverse_of
198
+ base.alias_method_chain :new_record, :inverse_of
199
+ base.alias_method_chain :replace, :inverse_of
200
+ end
201
+
202
+ def find_target_with_inverse_of
203
+ target = find_target_without_inverse_of
204
+ set_inverse_instance(target, @owner)
205
+ target
206
+ end
207
+
208
+ def replace_with_inverse_of(record, dont_save = false)
209
+ value = replace_without_inverse_of(record, dont_save)
210
+ set_inverse_instance(record, @owner)
211
+ value
212
+ end
213
+
214
+ private
215
+
216
+ def new_record_with_inverse_of(replace_existing, &block)
217
+ record = new_record_without_inverse_of(replace_existing, &block)
218
+ set_inverse_instance(record, @owner) unless replace_existing
219
+ record
220
+ end
221
+
222
+ def we_can_set_the_inverse_on_this?(record)
223
+ inverse = @reflection.inverse_of
224
+ return !inverse.nil?
225
+ end
226
+ end
227
+
228
+ module ClassMethods
229
+ module JoinDependency
230
+ def self.included(base)
231
+ base.alias_method_chain :construct_association, :inverse_of
232
+ end
233
+
234
+ def construct_association_with_inverse_of(record, join, row)
235
+ association = construct_association_without_inverse_of(record, join, row) or
236
+ return nil
237
+ association_proxy = record.send(join.reflection.name)
238
+ association_proxy.__send__(:set_inverse_instance, association, record)
239
+ end
240
+ end
241
+ end
242
+ end
243
+
244
+ module AssociationPreload
245
+ def self.included(base)
246
+ base.extend ClassMethods
247
+ base.metaclass.alias_method_chain :add_preloaded_records_to_collection, :inverse_of
248
+ base.metaclass.alias_method_chain :set_association_single_records, :inverse_of
249
+ end
250
+
251
+ module ClassMethods
252
+ def add_preloaded_records_to_collection_with_inverse_of(parent_records, reflection_name, associated_record)
253
+ value = add_preloaded_records_to_collection_without_inverse_of(parent_records, reflection_name, associated_record)
254
+ parent_records.each do |parent_record|
255
+ association_proxy = parent_record.send(reflection_name)
256
+ association_proxy.__send__(:set_inverse_instance, associated_record, parent_record)
257
+ end
258
+ value
259
+ end
260
+
261
+ def set_association_single_records_with_inverse_of(id_to_record_map, reflection_name, associated_records, key)
262
+ value = set_association_single_records_without_inverse_of(id_to_record_map, reflection_name, associated_records, key)
263
+ associated_records.each do |associated_record|
264
+ mapped_records = id_to_record_map[associated_record[key].to_s]
265
+ mapped_records.each do |mapped_record|
266
+ association_proxy = mapped_record.send(reflection_name)
267
+ association_proxy.__send__(:set_inverse_instance, associated_record, mapped_record)
268
+ end
269
+ end
270
+ value
271
+ end
272
+ end
273
+ end
274
+ end
275
+
276
+ ActiveRecord::InverseOfAssociationNotFoundError = InverseOf::InverseOfAssociationNotFoundError
277
+ ActiveRecord::Associations::AssociationCollection.send :include, InverseOf::Associations::AssociationCollection
278
+ ActiveRecord::Associations::AssociationProxy.send :include, InverseOf::Associations::AssociationProxy
279
+ ActiveRecord::Associations::BelongsToAssociation.send :include, InverseOf::Associations::BelongsToAssociation
280
+ ActiveRecord::Associations::BelongsToPolymorphicAssociation.send :include, InverseOf::Associations::BelongsToPolymorphicAssociation
281
+ ActiveRecord::Associations::HasManyAssociation.send :include, InverseOf::Associations::HasManyAssociation
282
+ ActiveRecord::Associations::HasManyThroughAssociation.send :include, InverseOf::Associations::HasManyThroughAssociation
283
+ ActiveRecord::Associations::HasOneAssociation.send :include, InverseOf::Associations::HasOneAssociation
284
+ ActiveRecord::Associations::ClassMethods::JoinDependency.send :include, InverseOf::Associations::ClassMethods::JoinDependency
285
+ ActiveRecord::Reflection.send :include, InverseOf::Reflection
286
+ ActiveRecord::Base.send :include, InverseOf::AssociationPreload
287
+
288
+ module ActiveRecord::Associations::ClassMethods
289
+ @@valid_keys_for_has_many_association << :inverse_of
290
+ @@valid_keys_for_has_one_association << :inverse_of
291
+ @@valid_keys_for_belongs_to_association << :inverse_of
292
+ end
293
+ end
data/rails/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'inverse_of'
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :inverse_of do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,28 @@
1
+ require 'test/unit'
2
+ require 'ruby-debug'
3
+ require 'active_record'
4
+ require 'active_record/test_case'
5
+ require 'active_record/fixtures'
6
+
7
+ ROOT = File.dirname(File.dirname(File.dirname(__FILE__)))
8
+
9
+ require "#{ROOT}/rails/init"
10
+
11
+ class ActiveSupport::TestCase
12
+ include ActiveRecord::TestFixtures
13
+
14
+ self.fixture_path = "#{ROOT}/test/fixtures"
15
+ self.use_instantiated_fixtures = false
16
+ self.use_transactional_fixtures = true
17
+
18
+ def create_fixtures(*table_names, &block)
19
+ Fixtures.create_fixtures(ActiveSupport::TestCase.fixture_path, table_names, {}, &block)
20
+ end
21
+ end
22
+
23
+ ActiveRecord::Base.configurations = {'test' => {'adapter' => "sqlite3", 'database' => ":memory:"}}
24
+ ActiveRecord::Base.establish_connection('test')
25
+ ActiveRecord::Base.connection.instance_eval do
26
+ eval File.read("#{ROOT}/test/schema/schema.rb")
27
+ end
28
+ Dir["#{ROOT}/test/models/*.rb"].each{|path| require path}