tenacity 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/.gitignore +1 -0
  2. data/EXTEND.rdoc +32 -22
  3. data/LICENSE.txt +1 -1
  4. data/README.rdoc +14 -16
  5. data/Rakefile +0 -37
  6. data/history.txt +21 -0
  7. data/lib/tenacity.rb +2 -1
  8. data/lib/tenacity/association.rb +82 -0
  9. data/lib/tenacity/associations/belongs_to.rb +9 -9
  10. data/lib/tenacity/associations/has_many.rb +19 -31
  11. data/lib/tenacity/associations/has_one.rb +7 -23
  12. data/lib/tenacity/class_methods.rb +136 -33
  13. data/lib/tenacity/instance_methods.rb +9 -13
  14. data/lib/tenacity/orm_ext/activerecord.rb +13 -30
  15. data/lib/tenacity/orm_ext/couchrest/couchrest_extended_document.rb +1 -4
  16. data/lib/tenacity/orm_ext/couchrest/couchrest_model.rb +1 -4
  17. data/lib/tenacity/orm_ext/couchrest/tenacity_class_methods.rb +8 -17
  18. data/lib/tenacity/orm_ext/couchrest/tenacity_instance_methods.rb +6 -6
  19. data/lib/tenacity/orm_ext/mongo_mapper.rb +15 -25
  20. data/lib/tenacity/version.rb +1 -1
  21. data/tenacity.gemspec +3 -3
  22. data/test/associations/belongs_to_test.rb +17 -1
  23. data/test/associations/has_many_test.rb +22 -0
  24. data/test/associations/has_one_test.rb +15 -0
  25. data/test/fixtures/active_record_car.rb +4 -0
  26. data/test/fixtures/active_record_engine.rb +5 -0
  27. data/test/fixtures/couch_rest_door.rb +10 -0
  28. data/test/fixtures/couch_rest_windshield.rb +10 -0
  29. data/test/fixtures/mongo_mapper_ash_tray.rb +8 -0
  30. data/test/fixtures/mongo_mapper_dashboard.rb +3 -0
  31. data/test/fixtures/mongo_mapper_vent.rb +8 -0
  32. data/test/fixtures/mongo_mapper_wheel.rb +1 -1
  33. data/test/helpers/active_record_test_helper.rb +34 -4
  34. data/test/orm_ext/activerecord_test.rb +15 -8
  35. data/test/orm_ext/couchrest_test.rb +15 -8
  36. data/test/orm_ext/mongo_mapper_test.rb +14 -8
  37. data/test/test_helper.rb +5 -2
  38. metadata +16 -11
  39. data/Gemfile.lock +0 -70
@@ -3,35 +3,19 @@ module Tenacity
3
3
 
4
4
  private
5
5
 
6
- def has_one_associate(association_id)
7
- clazz = associate_class(association_id)
8
- clazz._t_find_first_by_associate(property_name, self.id.to_s)
6
+ def has_one_associate(association)
7
+ clazz = association.associate_class
8
+ clazz._t_find_first_by_associate(association.foreign_key(self.class), self.id.to_s)
9
9
  end
10
10
 
11
- def set_has_one_associate(association_id, associate)
12
- associate.send "#{property_name}=", self.id.to_s
11
+ def set_has_one_associate(association, associate)
12
+ associate.send "#{association.foreign_key(self.class)}=", self.id.to_s
13
13
  associate.save
14
14
  end
15
15
 
16
- def property_name
17
- "#{ActiveSupport::Inflector.underscore(self.class.to_s)}_id"
18
- end
19
-
20
16
  module ClassMethods #:nodoc:
21
- def initialize_has_one_association(association_id)
22
- begin
23
- require association_id.to_s
24
- rescue Exception => e
25
- puts "ERROR: #{association_id.to_s} does not appear to be in the load path. Please make sure all model files are in the load path."
26
- raise
27
- end
28
-
29
- clazz = associate_class(association_id)
30
- clazz._t_initialize_has_one_association(ActiveSupport::Inflector.underscore(self.to_s)) if clazz.respond_to?(:_t_initialize_has_one_association)
31
- end
32
-
33
- def _t_stringify_has_one_value(record, association_id)
34
- record.send "#{association_id}_id=", record.send("#{association_id}_id").to_s
17
+ def initialize_has_one_association(association)
18
+ _t_initialize_has_one_association(association) if respond_to?(:_t_initialize_has_one_association)
35
19
  end
36
20
  end
37
21
 
@@ -70,6 +70,32 @@ module Tenacity
70
70
  # project.milestones(true).size # fetches milestones from the database
71
71
  # project.milestones # uses the milestone cache
72
72
  #
73
+ # == Join Tables
74
+ #
75
+ # One-to-many assocations that contain a relational database backed object as one of
76
+ # the assocaites are implemented using an intermediate join table. This differs from
77
+ # ActiveRecord::Associations, where only many-to-many relationships are implemented
78
+ # using an intermediate join table.
79
+ #
80
+ # Tenacity will not create the join table. It assume one exists, and is named properly.
81
+ # Unless the join table is explicitly specified as an option, it is guessed using the
82
+ # lexical order of the class names. So a join between Developer and Project will give
83
+ # the default join table name of "developers_projects" because "D" outranks "P". Note
84
+ # that this precedence is calculated using the < operator for String. This means that
85
+ # if the strings are of different lengths, and the strings are equal when compared up
86
+ # to the shortest length, then the longer string is considered of higher lexical
87
+ # precedence than the shorter one. For example, one would expect the tables "paper_boxes"
88
+ # and "papers" to generate a join table name of "papers_paper_boxes" because of the
89
+ # length of the name "paper_boxes", but it in fact generates a join table name of
90
+ # "paper_boxes_papers". Be aware of this caveat, and use the custom :join_table option
91
+ # if you need to.
92
+ #
93
+ # The column names used in the join table are guessed to be the names of the associated
94
+ # classes, suffixed with "_id". For example, the "developers_projects" join table
95
+ # mentioned above is expected to have a column named "developer_id" and a column named
96
+ # "project_id". The <tt>:associate_key</tt> and <tt>:associate_foreign_key</tt> options
97
+ # can be used to override these defaults.
98
+ #
73
99
  module ClassMethods
74
100
 
75
101
  # Specifies a one-to-one association with another class. This method should only be used
@@ -93,19 +119,34 @@ module Tenacity
93
119
  # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.find(:first, :conditions => "account_id = #{id}")</tt>)
94
120
  # * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
95
121
  #
96
- def t_has_one(association_id, args={})
122
+ # === Supported options
123
+ # [:class_name]
124
+ # Specify the class name of the association. Use it only if that name can't be inferred
125
+ # from the association name. So <tt>t_has_one :manager</tt> will by default be linked to the Manager class, but
126
+ # if the real class name is Person, you'll have to specify it with this option.
127
+ # [:foreign_key]
128
+ # Specify the foreign key used for the association. By default this is guessed to be the name
129
+ # of this class in lower-case and "_id" suffixed. So a Person class that makes a +t_has_one+ association
130
+ # will use "person_id" as the default <tt>:foreign_key</tt>.
131
+ #
132
+ # Option examples:
133
+ # t_has_one :project_manager, :class_name => "Person"
134
+ # t_has_one :project_manager, :foreign_key => "project_id" # within class named SecretProject
135
+ #
136
+ def t_has_one(name, options={})
97
137
  extend(HasOne::ClassMethods)
98
- initialize_has_one_association(association_id)
138
+ association = Association.new(:t_has_one, name, self, options)
139
+ initialize_has_one_association(association)
99
140
 
100
- define_method(association_id) do |*params|
101
- get_associate(association_id, params) do
102
- has_one_associate(association_id)
141
+ define_method(association.name) do |*params|
142
+ get_associate(association, params) do
143
+ has_one_associate(association)
103
144
  end
104
145
  end
105
146
 
106
- define_method("#{association_id}=") do |associate|
107
- set_associate(association_id, associate) do
108
- set_has_one_associate(association_id, associate)
147
+ define_method("#{association.name}=") do |associate|
148
+ set_associate(association, associate) do
149
+ set_has_one_associate(association, associate)
109
150
  end
110
151
  end
111
152
  end
@@ -131,25 +172,46 @@ module Tenacity
131
172
  # * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>)
132
173
  # * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
133
174
  #
134
- def t_belongs_to(association_id, args={})
175
+ # === Supported options
176
+ # [:class_name]
177
+ # Specify the class name of the association. Use it only if that name can't be inferred
178
+ # from the association name. So <tt>t_belongs_to :manager</tt> will by default be linked to the Manager class, but
179
+ # if the real class name is Person, you'll have to specify it with this option.
180
+ # [:foreign_key]
181
+ # Specify the foreign key used for the association. By default this is guessed to be the name
182
+ # of the association with an "_id" suffix. So a class that defines a <tt>t_belongs_to :person</tt>
183
+ # association will use "person_id" as the default <tt>:foreign_key</tt>. Similarly,
184
+ # <tt>t_belongs_to :favorite_person, :class_name => "Person"</tt> will use a foreign key
185
+ # of "favorite_person_id".
186
+ #
187
+ # Option examples:
188
+ # t_belongs_to :project_manager, :class_name => "Person"
189
+ # t_belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id"
190
+ #
191
+ def t_belongs_to(name, options={})
135
192
  extend(BelongsTo::ClassMethods)
136
- initialize_belongs_to_association(association_id)
193
+ association = Association.new(:t_belongs_to, name, self, options)
194
+ initialize_belongs_to_association(association)
137
195
 
138
- define_method(association_id) do |*params|
139
- get_associate(association_id, params) do
140
- belongs_to_associate(association_id)
196
+ define_method(association.name) do |*params|
197
+ get_associate(association, params) do
198
+ belongs_to_associate(association)
141
199
  end
142
200
  end
143
201
 
144
- define_method("#{association_id}=") do |associate|
145
- set_associate(association_id, associate) do
146
- set_belongs_to_associate(association_id, associate)
202
+ define_method("#{association.name}=") do |associate|
203
+ set_associate(association, associate) do
204
+ set_belongs_to_associate(association, associate)
147
205
  end
148
206
  end
149
207
  end
150
208
 
151
- # Specifies a one-to-many association. The following methods for retrieval and query of
152
- # collections of associated objects will be added:
209
+ # Specifies a one-to-many association. One-to-many associations that contain a
210
+ # relational database backed object as one of the associates are implemented
211
+ # using an intermediate join table. See the Join Tables section at the top
212
+ # for more information.
213
+ #
214
+ # The following methods for retrieval and query of collections of associated objects will be added:
153
215
  #
154
216
  # [collection(force_reload = false)]
155
217
  # Returns an array of all the associated objects.
@@ -188,26 +250,71 @@ module Tenacity
188
250
  # * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
189
251
  # * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
190
252
  #
191
- def t_has_many(association_id, args={})
253
+ # === Supported options
254
+ # [:class_name]
255
+ # Specify the class name of the association. Use it only if that name can't be inferred
256
+ # from the association name. So <tt>t_has_many :products</tt> will by default be linked
257
+ # to the Product class, but if the real class name is SpecialProduct, you'll have to
258
+ # specify it with this option.
259
+ # [:foreign_key]
260
+ # Specify the foreign key used for the association. By default this is guessed to be the name
261
+ # of this class in lower-case and "_id" suffixed. So a Person class that makes a +t_has_many+
262
+ # association will use "person_id" as the default <tt>:foreign_key</tt>.
263
+ # [:foreign_keys_property]
264
+ # Specify the name of the property that stores the ids of the associated objects. By default
265
+ # this is guessed to be the name of the association with a "t_" prefix and an "_ids" suffix.
266
+ # So a class that defines a <tt>t_has_many :people</tt> association will use t_people_ids as
267
+ # the property to store the ids of the associated People objects. This option is only valid
268
+ # for objects that store associated ids in an array instaed of a join table (CouchRest,
269
+ # MongoMapper, etc). <b>WARNING:</b> The name of the association with an "_ids" suffix should
270
+ # not be used as the property name, since tenacity adds a method with this name to the object.
271
+ # [:join_table]
272
+ # Specify the name of the join table if the default based on lexical order isn't what you want.
273
+ # This option is only valid if one of the models in the association is backed by a relational
274
+ # database.
275
+ # [:association_foreign_key]
276
+ # Specify the foreign key in the join table used for the association on the receiving side of
277
+ # the association. By default this is guessed to be the name of the associated class in
278
+ # lower-case and "_id" suffixed. So if a Person class makes a +t_has_many+ association to
279
+ # Project, the association will use "project_id" as the default <tt>:association_foreign_key</tt>.
280
+ # This option is only valid if one of the associated objects is backed by a relational
281
+ # database.
282
+ # [:association_key]
283
+ # Specify the key in the join table used for the association on the declaring side of
284
+ # the association. By default this is guessed to be the name of this class in lower-case and
285
+ # "_id" suffixed. So if a Person class makes a +t_has_many+ association to Project, the
286
+ # association will use "person_id" as the default <tt>:association_key</tt>. This option is
287
+ # only valid if one of the associated objects is backed by a relational database.
288
+ #
289
+ # Option examples:
290
+ # t_has_many :products, :class_name => "SpecialProduct"
291
+ # t_has_many :engineers, :foreign_key => "project_id" # within class named SecretProject
292
+ # t_has_many :engineers, :foreign_keys_property => "worker_ids"
293
+ # t_has_many :managers, :join_table => "project_managers_and_projects"
294
+ # t_has_many :managers, :join_table => "project_managers_and_projects",
295
+ # :association_foreign_key => "mgr_id", :association_key => "proj_id"
296
+ #
297
+ def t_has_many(name, options={})
192
298
  extend(HasMany::ClassMethods)
193
- initialize_has_many_association(association_id)
299
+ association = Association.new(:t_has_many, name, self, options)
300
+ initialize_has_many_association(association)
194
301
 
195
- define_method(association_id) do |*params|
196
- get_associate(association_id, params) do
197
- has_many_associates(association_id)
302
+ define_method(association.name) do |*params|
303
+ get_associate(association, params) do
304
+ has_many_associates(association)
198
305
  end
199
306
  end
200
307
 
201
- define_method("#{association_id}=") do |associates|
202
- set_associate(association_id, associates)
308
+ define_method("#{association.name}=") do |associates|
309
+ set_associate(association, associates)
203
310
  end
204
311
 
205
- define_method("#{ActiveSupport::Inflector.singularize(association_id.to_s)}_ids") do
206
- has_many_associate_ids(association_id)
312
+ define_method("#{ActiveSupport::Inflector.singularize(association.name)}_ids") do
313
+ has_many_associate_ids(association)
207
314
  end
208
315
 
209
- define_method("#{ActiveSupport::Inflector.singularize(association_id.to_s)}_ids=") do |associate_ids|
210
- set_has_many_associate_ids(association_id, associate_ids)
316
+ define_method("#{ActiveSupport::Inflector.singularize(association.name)}_ids=") do |associate_ids|
317
+ set_has_many_associate_ids(association, associate_ids)
211
318
  end
212
319
 
213
320
  private
@@ -217,10 +324,6 @@ module Tenacity
217
324
  end
218
325
  end
219
326
 
220
- def associate_class(association_id) #:nodoc:
221
- Kernel.const_get(association_id.to_s.singularize.camelcase.to_sym)
222
- end
223
-
224
327
  end
225
328
  end
226
329
 
@@ -1,30 +1,26 @@
1
1
  module Tenacity
2
2
  module InstanceMethods #:nodoc:
3
3
 
4
+ def _t_ivar_name(association)
5
+ "@_t_" + association.name.to_s
6
+ end
7
+
4
8
  private
5
9
 
6
- def get_associate(association_id, params)
10
+ def get_associate(association, params)
7
11
  _t_reload
8
12
  force_reload = params.first unless params.empty?
9
- value = instance_variable_get ivar_name(association_id)
13
+ value = instance_variable_get _t_ivar_name(association)
10
14
  if value.nil? || force_reload
11
15
  value = yield
12
- instance_variable_set ivar_name(association_id), value
16
+ instance_variable_set _t_ivar_name(association), value
13
17
  end
14
18
  value
15
19
  end
16
20
 
17
- def set_associate(association_id, associate)
21
+ def set_associate(association, associate)
18
22
  yield if block_given?
19
- instance_variable_set ivar_name(association_id), associate
20
- end
21
-
22
- def ivar_name(association_id)
23
- "@_t_" + association_id.to_s
24
- end
25
-
26
- def associate_class(association_id)
27
- self.class.associate_class(association_id)
23
+ instance_variable_set _t_ivar_name(association), associate
28
24
  end
29
25
 
30
26
  end
@@ -62,53 +62,36 @@ begin
62
62
  find(:all, :conditions => ["#{property} = ?", id.to_s])
63
63
  end
64
64
 
65
- def self._t_initialize_has_many_association(association_id)
66
- after_save { |record| record.class._t_save_associates(record, association_id) }
65
+ def self._t_initialize_has_many_association(association)
66
+ after_save { |record| record.class._t_save_associates(record, association) }
67
67
  end
68
68
 
69
- def self._t_initialize_belongs_to_association(association_id)
70
- before_save { |record| record.class._t_stringify_belongs_to_value(record, association_id) }
69
+ def self._t_initialize_belongs_to_association(association)
70
+ before_save { |record| record.class._t_stringify_belongs_to_value(record, association) }
71
71
  end
72
72
 
73
73
  def _t_reload
74
74
  reload
75
75
  end
76
76
 
77
- def _t_clear_associates(association_id)
78
- t_join_table_name = self.class._t_join_table_name(association_id)
79
- self.connection.execute("delete from #{t_join_table_name} where #{self.class._t_my_id_column} = #{self.id}")
77
+ def _t_clear_associates(association)
78
+ self.connection.execute("delete from #{association.join_table} where #{association.association_key} = #{self.id}")
80
79
  end
81
80
 
82
- def _t_associate_many(association_id, associate_ids)
83
- t_join_table_name = self.class._t_join_table_name(association_id)
84
- values = associate_ids.map { |associate_id| "(#{self.id}, '#{associate_id}')" }.join(',')
85
-
81
+ def _t_associate_many(association, associate_ids)
86
82
  self.transaction do
87
- _t_clear_associates(association_id)
88
- self.connection.execute("insert into #{t_join_table_name} (#{self.class._t_my_id_column}, #{self.class._t_associate_id_column(association_id)}) values #{values}")
83
+ _t_clear_associates(association)
84
+ associate_ids.each do |associate_id|
85
+ self.connection.execute("insert into #{association.join_table} (#{association.association_key}, #{association.association_foreign_key}) values (#{self.id}, '#{associate_id}')")
86
+ end
89
87
  end
90
88
  end
91
89
 
92
- def _t_get_associate_ids(association_id)
93
- t_join_table_name = self.class._t_join_table_name(association_id)
94
- rows = self.connection.execute("select #{self.class._t_associate_id_column(association_id)} from #{t_join_table_name} where #{self.class._t_my_id_column} = #{self.id}")
90
+ def _t_get_associate_ids(association)
91
+ rows = self.connection.execute("select #{association.association_foreign_key} from #{association.join_table} where #{association.association_key} = #{self.id}")
95
92
  ids = []; rows.each { |r| ids << r[0] }; ids
96
93
  end
97
94
 
98
- private
99
-
100
- def self._t_my_id_column
101
- table_name.singularize + '_id'
102
- end
103
-
104
- def self._t_associate_id_column(association_id)
105
- association_id.to_s.singularize + '_id'
106
- end
107
-
108
- def self._t_join_table_name(association_id)
109
- association_id.to_s < table_name ? "#{association_id}_#{table_name}" : "#{table_name}_#{association_id}"
110
- end
111
-
112
95
  end
113
96
  end
114
97
  rescue LoadError
@@ -24,10 +24,7 @@ begin
24
24
  # == t_has_one
25
25
  #
26
26
  # The +t_has_one+ association will not define any new properties on the object, since
27
- # the associated object holds the foreign key. If the CouchRest::ExtendedDocument class
28
- # is the target of a t_has_one association from another class, then a property
29
- # named after the association will be created on the CouchRest::ExtendedDocument object to
30
- # hold the foreign key to the other object.
27
+ # the associated object holds the foreign key.
31
28
  #
32
29
  #
33
30
  # == t_has_many
@@ -25,10 +25,7 @@ begin
25
25
  # == t_has_one
26
26
  #
27
27
  # The +t_has_one+ association will not define any new properties on the object, since
28
- # the associated object holds the foreign key. If the CouchRest::Model class
29
- # is the target of a t_has_one association from another class, then a property
30
- # named after the association will be created on the CouchRest::Model object to
31
- # hold the foreign key to the other object.
28
+ # the associated object holds the foreign key.
32
29
  #
33
30
  #
34
31
  # == t_has_many
@@ -23,29 +23,20 @@ module CouchRest
23
23
  self.send("by_#{property}", :key => id.to_s)
24
24
  end
25
25
 
26
- def _t_initialize_has_many_association(association_id)
27
- unless self.respond_to?(has_many_property_name(association_id))
28
- property has_many_property_name(association_id), :type => [String]
29
- view_by has_many_property_name(association_id)
30
- after_save { |record| record.class._t_save_associates(record, association_id) if record.class.respond_to?(:_t_save_associates) }
26
+ def _t_initialize_has_many_association(association)
27
+ unless self.respond_to?(association.foreign_keys_property)
28
+ property association.foreign_keys_property, :type => [String]
29
+ view_by association.foreign_keys_property
30
+ after_save { |record| record.class._t_save_associates(record, association) if record.class.respond_to?(:_t_save_associates) }
31
31
  end
32
32
  end
33
33
 
34
- def _t_initialize_belongs_to_association(association_id)
35
- property_name = "#{association_id}_id"
34
+ def _t_initialize_belongs_to_association(association)
35
+ property_name = association.foreign_key
36
36
  unless self.respond_to?(property_name)
37
37
  property property_name, :type => String
38
38
  view_by property_name
39
- before_save { |record| _t_stringify_belongs_to_value(record, association_id) if self.respond_to?(:_t_stringify_belongs_to_value) }
40
- end
41
- end
42
-
43
- def _t_initialize_has_one_association(association_id)
44
- property_name = "#{association_id}_id"
45
- unless self.respond_to?(property_name)
46
- property property_name, :type => String
47
- view_by property_name
48
- before_save { |record| _t_stringify_has_one_value(record, association_id) if self.respond_to?(:_t_stringify_has_one_value) }
39
+ before_save { |record| _t_stringify_belongs_to_value(record, association) if self.respond_to?(:_t_stringify_belongs_to_value) }
49
40
  end
50
41
  end
51
42
  end
@@ -6,16 +6,16 @@ module CouchRest
6
6
  new_doc.each { |k,v| self[k] = new_doc[k] }
7
7
  end
8
8
 
9
- def _t_associate_many(association_id, associate_ids)
10
- self.send(has_many_property_name(association_id) + '=', associate_ids.map { |associate_id| associate_id.to_s })
9
+ def _t_associate_many(association, associate_ids)
10
+ self.send(association.foreign_keys_property + '=', associate_ids.map { |associate_id| associate_id.to_s })
11
11
  end
12
12
 
13
- def _t_get_associate_ids(association_id)
14
- self.send(has_many_property_name(association_id)) || []
13
+ def _t_get_associate_ids(association)
14
+ self.send(association.foreign_keys_property) || []
15
15
  end
16
16
 
17
- def _t_clear_associates(association_id)
18
- self.send(has_many_property_name(association_id) + '=', [])
17
+ def _t_clear_associates(association)
18
+ self.send(association.foreign_keys_property + '=', [])
19
19
  end
20
20
  end
21
21
  end