tenacity 0.1.1 → 0.2.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.
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