tenacity 0.3.0 → 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.
Files changed (70) hide show
  1. data/EXTEND.rdoc +9 -13
  2. data/Rakefile +6 -0
  3. data/history.txt +17 -0
  4. data/lib/tenacity.rb +13 -9
  5. data/lib/tenacity/associate_proxy.rb +56 -0
  6. data/lib/tenacity/associates_proxy.rb +5 -3
  7. data/lib/tenacity/association.rb +81 -9
  8. data/lib/tenacity/associations/belongs_to.rb +28 -16
  9. data/lib/tenacity/associations/has_many.rb +113 -53
  10. data/lib/tenacity/associations/has_one.rb +33 -14
  11. data/lib/tenacity/class_methods.rb +117 -9
  12. data/lib/tenacity/errors.rb +9 -0
  13. data/lib/tenacity/instance_methods.rb +40 -3
  14. data/lib/tenacity/orm_ext/activerecord.rb +114 -95
  15. data/lib/tenacity/orm_ext/couchrest.rb +132 -113
  16. data/lib/tenacity/orm_ext/datamapper.rb +138 -112
  17. data/lib/tenacity/orm_ext/helpers.rb +36 -0
  18. data/lib/tenacity/orm_ext/mongo_mapper.rb +102 -84
  19. data/lib/tenacity/orm_ext/mongoid.rb +106 -87
  20. data/lib/tenacity/orm_ext/sequel.rb +137 -110
  21. data/lib/tenacity/version.rb +1 -1
  22. data/tenacity.gemspec +2 -1
  23. data/test/association_features/belongs_to_test.rb +46 -1
  24. data/test/association_features/has_many_test.rb +187 -2
  25. data/test/association_features/has_one_test.rb +57 -2
  26. data/test/associations/belongs_to_test.rb +41 -7
  27. data/test/associations/has_many_test.rb +59 -10
  28. data/test/associations/has_one_test.rb +57 -2
  29. data/test/fixtures/active_record_car.rb +5 -4
  30. data/test/fixtures/active_record_climate_control_unit.rb +1 -0
  31. data/test/fixtures/active_record_engine.rb +1 -0
  32. data/test/fixtures/active_record_has_many_target.rb +7 -0
  33. data/test/fixtures/active_record_has_one_target.rb +7 -0
  34. data/test/fixtures/active_record_object.rb +19 -0
  35. data/test/fixtures/couch_rest_has_many_target.rb +7 -0
  36. data/test/fixtures/couch_rest_has_one_target.rb +7 -0
  37. data/test/fixtures/couch_rest_object.rb +14 -0
  38. data/test/fixtures/data_mapper_has_many_target.rb +23 -3
  39. data/test/fixtures/data_mapper_has_one_target.rb +23 -3
  40. data/test/fixtures/data_mapper_object.rb +14 -0
  41. data/test/fixtures/mongo_mapper_air_filter.rb +6 -0
  42. data/test/fixtures/mongo_mapper_alternator.rb +7 -0
  43. data/test/fixtures/mongo_mapper_autosave_false_has_many_target.rb +8 -0
  44. data/test/fixtures/mongo_mapper_autosave_false_has_one_target.rb +8 -0
  45. data/test/fixtures/mongo_mapper_autosave_true_has_many_target.rb +8 -0
  46. data/test/fixtures/mongo_mapper_autosave_true_has_one_target.rb +8 -0
  47. data/test/fixtures/mongo_mapper_circuit_board.rb +6 -0
  48. data/test/fixtures/mongo_mapper_dashboard.rb +5 -2
  49. data/test/fixtures/mongo_mapper_has_many_target.rb +7 -0
  50. data/test/fixtures/mongo_mapper_has_one_target.rb +7 -0
  51. data/test/fixtures/mongo_mapper_object.rb +14 -0
  52. data/test/fixtures/mongo_mapper_wheel.rb +2 -0
  53. data/test/fixtures/mongo_mapper_windows.rb +6 -0
  54. data/test/fixtures/mongoid_has_many_target.rb +7 -0
  55. data/test/fixtures/mongoid_has_one_target.rb +7 -0
  56. data/test/fixtures/mongoid_object.rb +14 -0
  57. data/test/fixtures/sequel_has_many_target.rb +7 -0
  58. data/test/fixtures/sequel_has_one_target.rb +7 -0
  59. data/test/fixtures/sequel_object.rb +14 -0
  60. data/test/helpers/active_record_test_helper.rb +87 -8
  61. data/test/helpers/couch_rest_test_helper.rb +7 -0
  62. data/test/helpers/data_mapper_test_helper.rb +41 -3
  63. data/test/helpers/sequel_test_helper.rb +65 -9
  64. data/test/orm_ext/activerecord_test.rb +1 -1
  65. data/test/orm_ext/datamapper_test.rb +33 -24
  66. data/test/orm_ext/mongo_mapper_test.rb +6 -6
  67. data/test/orm_ext/mongoid_test.rb +6 -6
  68. data/test/orm_ext/sequel_test.rb +1 -1
  69. data/test/test_helper.rb +18 -9
  70. metadata +119 -55
@@ -1,140 +1,159 @@
1
1
  module Tenacity
2
- # Tenacity relationships on CouchRest objects require no special keys
3
- # defined on the object. Tenacity will define the keys that it needs
4
- # to support the relationships. Take the following class for example:
5
- #
6
- # class Car < CouchRest::ExtendedDocument
7
- # include Tenacity
8
- #
9
- # t_has_many :wheels
10
- # t_has_one :dashboard
11
- # t_belongs_to :driver
12
- # end
13
- #
14
- # == t_belongs_to
15
- #
16
- # The +t_belongs_to+ association will define a property named after the association.
17
- # The example above will create a property named <tt>:driver_id</tt>
18
- #
19
- #
20
- # == t_has_one
21
- #
22
- # The +t_has_one+ association will not define any new properties on the object, since
23
- # the associated object holds the foreign key.
24
- #
25
- #
26
- # == t_has_many
27
- #
28
- # The +t_has_many+ association will define a property named after the association.
29
- # The example above will create a property named <tt>:wheels_ids</tt>
30
- #
31
- module CouchRest
32
-
33
- def self.setup(model) #:nodoc:
34
- begin
35
- require 'couchrest_model'
36
- if model.ancestors.include?(::CouchRest::Model::Base)
37
- model.send :include, CouchRest::InstanceMethods
38
- model.extend CouchRest::ClassMethods
39
- end
40
- rescue LoadError
41
- # CouchRest::Model not available
42
- end
2
+ module OrmExt
3
+ # Tenacity relationships on CouchRest objects require no special keys
4
+ # defined on the object. Tenacity will define the keys that it needs
5
+ # to support the relationships. Take the following class for example:
6
+ #
7
+ # class Car < CouchRest::ExtendedDocument
8
+ # include Tenacity
9
+ #
10
+ # t_has_many :wheels
11
+ # t_has_one :dashboard
12
+ # t_belongs_to :driver
13
+ # end
14
+ #
15
+ # == t_belongs_to
16
+ #
17
+ # The +t_belongs_to+ association will define a property named after the association.
18
+ # The example above will create a property named <tt>:driver_id</tt>
19
+ #
20
+ #
21
+ # == t_has_one
22
+ #
23
+ # The +t_has_one+ association will not define any new properties on the object, since
24
+ # the associated object holds the foreign key.
25
+ #
26
+ #
27
+ # == t_has_many
28
+ #
29
+ # The +t_has_many+ association will define a property named after the association.
30
+ # The example above will create a property named <tt>:wheels_ids</tt>
31
+ #
32
+ module CouchRest
33
+
34
+ def self.setup(model) #:nodoc:
35
+ begin
36
+ require 'couchrest_model'
37
+ if model.ancestors.include?(::CouchRest::Model::Base)
38
+ model.send :include, CouchRest::InstanceMethods
39
+ model.extend CouchRest::ClassMethods
40
+ end
41
+ rescue LoadError
42
+ # CouchRest::Model not available
43
+ end
43
44
 
44
- begin
45
- require 'couchrest_extended_document'
46
- if model.ancestors.include?(::CouchRest::ExtendedDocument)
47
- model.send :include, CouchRest::InstanceMethods
48
- model.extend CouchRest::ClassMethods
45
+ begin
46
+ require 'couchrest_extended_document'
47
+ if model.ancestors.include?(::CouchRest::ExtendedDocument)
48
+ model.send :include, CouchRest::InstanceMethods
49
+ model.extend CouchRest::ClassMethods
50
+ end
51
+ rescue LoadError
52
+ # CouchRest::ExtendedDocument not available
49
53
  end
50
- rescue LoadError
51
- # CouchRest::ExtendedDocument not available
52
- end
53
54
 
54
- # For pre 1.0 versions of couchrest
55
- begin
56
- require 'couchrest'
57
- if model.ancestors.include?(::CouchRest::ExtendedDocument)
58
- model.send :include, CouchRest::InstanceMethods
59
- model.extend CouchRest::ClassMethods
55
+ # For pre 1.0 versions of couchrest
56
+ begin
57
+ require 'couchrest'
58
+ if model.ancestors.include?(::CouchRest::ExtendedDocument)
59
+ model.send :include, CouchRest::InstanceMethods
60
+ model.extend CouchRest::ClassMethods
61
+ end
62
+ rescue LoadError
63
+ rescue NameError
64
+ # CouchRest::ExtendedDocument not available
60
65
  end
61
- rescue LoadError
62
- rescue NameError
63
- # CouchRest::ExtendedDocument not available
64
66
  end
65
- end
66
67
 
67
- module ClassMethods #:nodoc:
68
- def _t_find(id)
69
- (id.nil? || id.strip == "") ? nil : get(id)
70
- end
68
+ module ClassMethods #:nodoc:
69
+ include Tenacity::OrmExt::Helpers
71
70
 
72
- def _t_find_bulk(ids)
73
- return [] if ids.nil? || ids.empty?
71
+ def _t_id_type
72
+ String
73
+ end
74
74
 
75
- docs = []
76
- result = database.get_bulk ids
77
- result['rows'].each do |row|
78
- docs << (row['doc'].nil? ? nil : create_from_database(row['doc']))
75
+ def _t_find(id)
76
+ (id.nil? || id.strip == "") ? nil : get(_t_serialize(id))
79
77
  end
80
- docs.reject { |doc| doc.nil? }
81
- end
82
78
 
83
- def _t_find_first_by_associate(property, id)
84
- self.send("by_#{property}", :key => id.to_s).first
85
- end
79
+ def _t_find_bulk(ids)
80
+ return [] if ids.nil? || ids.empty?
81
+ ids = [ids] unless ids.class == Array
86
82
 
87
- def _t_find_all_by_associate(property, id)
88
- self.send("by_#{property}", :key => id.to_s)
89
- end
83
+ docs = []
84
+ result = database.get_bulk(_t_serialize_ids(ids))
85
+ result['rows'].each do |row|
86
+ docs << (row['doc'].nil? ? nil : create_from_database(row['doc']))
87
+ end
88
+ docs.reject { |doc| doc.nil? }
89
+ end
90
90
 
91
- def _t_initialize_has_many_association(association)
92
- unless self.respond_to?(association.foreign_keys_property)
93
- property association.foreign_keys_property, :type => [String]
94
- view_by association.foreign_keys_property
95
- after_save { |record| record.class._t_save_associates(record, association) if record.class.respond_to?(:_t_save_associates) }
91
+ def _t_find_first_by_associate(property, id)
92
+ self.send("by_#{property}", :key => _t_serialize(id)).first
96
93
  end
97
- end
98
94
 
99
- def _t_initialize_belongs_to_association(association)
100
- property_name = association.foreign_key
101
- unless self.respond_to?(property_name)
102
- property property_name, :type => String
103
- view_by property_name
104
- before_save { |record| _t_stringify_belongs_to_value(record, association) if self.respond_to?(:_t_stringify_belongs_to_value) }
95
+ def _t_find_all_by_associate(property, id)
96
+ self.send("by_#{property}", :key => _t_serialize(id))
105
97
  end
106
- end
107
98
 
108
- def _t_delete(ids, run_callbacks=true)
109
- docs = _t_find_bulk(ids)
110
- if run_callbacks
111
- docs.each { |doc| doc.destroy }
112
- else
113
- docs.each { |doc| database.delete_doc(doc) }
99
+ def _t_initialize_tenacity
100
+ after_save { |record| record._t_save_autosave_associations }
114
101
  end
115
- end
116
- end
117
102
 
118
- module InstanceMethods #:nodoc:
119
- def _t_reload
120
- return if self.id.nil?
121
- new_doc = database.get(self.id)
122
- self.clear
123
- new_doc.each { |k,v| self[k] = new_doc[k] }
124
- end
103
+ def _t_initialize_has_one_association(association)
104
+ before_destroy { |record| record._t_cleanup_has_one_association(association) }
105
+ end
125
106
 
126
- def _t_associate_many(association, associate_ids)
127
- self.send(association.foreign_keys_property + '=', associate_ids.map { |associate_id| associate_id.to_s })
128
- end
107
+ def _t_initialize_has_many_association(association)
108
+ unless self.respond_to?(association.foreign_keys_property)
109
+ property association.foreign_keys_property, :type => [id_class_for(association)]
110
+ view_by association.foreign_keys_property
111
+ after_save { |record| record.class._t_save_associates(record, association) if record.class.respond_to?(:_t_save_associates) }
112
+ after_destroy { |record| record._t_cleanup_has_many_association(association) }
113
+ end
114
+ end
115
+
116
+ def _t_initialize_belongs_to_association(association)
117
+ property_name = association.foreign_key
118
+ unless self.respond_to?(property_name)
119
+ property property_name, :type => id_class_for(association)
120
+ property association.polymorphic_type, :type => String if association.polymorphic?
121
+ view_by property_name
122
+ after_destroy { |record| record._t_cleanup_belongs_to_association(association) }
123
+ end
124
+ end
129
125
 
130
- def _t_get_associate_ids(association)
131
- self.send(association.foreign_keys_property) || []
126
+ def _t_delete(ids, run_callbacks=true)
127
+ docs = _t_find_bulk(ids)
128
+ if run_callbacks
129
+ docs.each { |doc| doc.destroy }
130
+ else
131
+ docs.each { |doc| database.delete_doc(doc) }
132
+ end
133
+ end
132
134
  end
133
135
 
134
- def _t_clear_associates(association)
135
- self.send(association.foreign_keys_property + '=', [])
136
+ module InstanceMethods #:nodoc:
137
+ def _t_reload
138
+ return if self.id.nil?
139
+ new_doc = database.get(self.id)
140
+ self.clear
141
+ new_doc.each { |k,v| self[k] = new_doc[k] }
142
+ end
143
+
144
+ def _t_associate_many(association, associate_ids)
145
+ self.send(association.foreign_keys_property + '=', associate_ids)
146
+ end
147
+
148
+ def _t_get_associate_ids(association)
149
+ self.send(association.foreign_keys_property) || []
150
+ end
151
+
152
+ def _t_clear_associates(association)
153
+ self.send(association.foreign_keys_property + '=', [])
154
+ end
136
155
  end
137
- end
138
156
 
157
+ end
139
158
  end
140
159
  end
@@ -1,139 +1,165 @@
1
1
  module Tenacity
2
- # Tenacity relationships on DataMapper objects require that certain columns
3
- # exist on the associated table, and that join tables exist for one-to-many
4
- # relationships. Take the following class for example:
5
- #
6
- # class Car
7
- # include DataMapper::Resource
8
- # include Tenacity
9
- #
10
- # property :id, Serial
11
- # property :driver_id, String
12
- #
13
- # t_has_many :wheels
14
- # t_has_one :dashboard
15
- # t_belongs_to :driver
16
- # end
17
- #
18
- #
19
- # == t_belongs_to
20
- #
21
- # The +t_belongs_to+ association requires that a property exist in the table
22
- # to hold the id of the assoicated object.
23
- #
24
- #
25
- # == t_has_one
26
- #
27
- # The +t_has_one+ association requires no special column in the table, since
28
- # the associated object holds the foreign key.
29
- #
30
- #
31
- # == t_has_many
32
- #
33
- # The +t_has_many+ association requires that a join table exist to store the
34
- # associations. The name of the join table follows ActiveRecord conventions.
35
- # The name of the join table in this example would be cars_wheels, since cars
36
- # comes before wheels when shorted alphabetically.
37
- #
38
- # create_table :car_wheels do
39
- # column :car_id, Integer
40
- # column :wheel_id, String
41
- # end
42
- #
43
- module DataMapper
44
-
45
- def self.setup(model)
46
- require 'datamapper'
47
- if model.included_modules.include?(::DataMapper::Resource)
48
- model.send :include, DataMapper::InstanceMethods
49
- model.extend DataMapper::ClassMethods
2
+ module OrmExt
3
+ # Tenacity relationships on DataMapper objects require that certain columns
4
+ # exist on the associated table, and that join tables exist for one-to-many
5
+ # relationships. Take the following class for example:
6
+ #
7
+ # class Car
8
+ # include DataMapper::Resource
9
+ # include Tenacity
10
+ #
11
+ # property :id, Serial
12
+ # property :driver_id, String
13
+ #
14
+ # t_has_many :wheels
15
+ # t_has_one :dashboard
16
+ # t_belongs_to :driver
17
+ # end
18
+ #
19
+ #
20
+ # == t_belongs_to
21
+ #
22
+ # The +t_belongs_to+ association requires that a property exist in the table
23
+ # to hold the id of the assoicated object.
24
+ #
25
+ #
26
+ # == t_has_one
27
+ #
28
+ # The +t_has_one+ association requires no special column in the table, since
29
+ # the associated object holds the foreign key.
30
+ #
31
+ #
32
+ # == t_has_many
33
+ #
34
+ # The +t_has_many+ association requires that a join table exist to store the
35
+ # associations. The name of the join table follows ActiveRecord conventions.
36
+ # The name of the join table in this example would be cars_wheels, since cars
37
+ # comes before wheels when shorted alphabetically.
38
+ #
39
+ # create_table :car_wheels do
40
+ # column :car_id, Integer
41
+ # column :wheel_id, String
42
+ # end
43
+ #
44
+ module DataMapper
45
+
46
+ def self.setup(model)
47
+ require 'datamapper'
48
+ if model.included_modules.include?(::DataMapper::Resource)
49
+ model.send :include, DataMapper::InstanceMethods
50
+ model.extend DataMapper::ClassMethods
51
+ end
52
+ rescue LoadError
53
+ # DataMapper not available
50
54
  end
51
- rescue LoadError
52
- # DataMapper not available
53
- end
54
55
 
55
- module ClassMethods #:nodoc:
56
- def _t_find(id)
57
- get(id)
58
- end
56
+ module ClassMethods #:nodoc:
57
+ include Tenacity::OrmExt::Helpers
59
58
 
60
- def _t_find_bulk(ids)
61
- return [] if ids.nil? || ids.empty?
62
- all(:id => ids)
63
- end
59
+ def _t_id_type
60
+ Integer
61
+ end
64
62
 
65
- def _t_find_first_by_associate(property, id)
66
- first(property => id.to_s)
67
- end
63
+ def _t_find(id)
64
+ get(_t_serialize(id))
65
+ end
68
66
 
69
- def _t_find_all_by_associate(property, id)
70
- all(property => id.to_s)
71
- end
67
+ def _t_find_bulk(ids)
68
+ return [] if ids.nil? || ids == []
69
+ all(:id => _t_serialize_ids(ids))
70
+ end
72
71
 
73
- def _t_initialize_has_many_association(association)
74
- after :save do |record|
75
- record.class._t_save_associates(record, association)
72
+ def _t_find_first_by_associate(property, id)
73
+ first(property => _t_serialize(id))
76
74
  end
77
- end
78
75
 
79
- def _t_initialize_belongs_to_association(association)
80
- before :save do |record|
81
- record.class._t_stringify_belongs_to_value(record, association)
76
+ def _t_find_all_by_associate(property, id)
77
+ all(property => _t_serialize(id))
82
78
  end
83
- end
84
79
 
85
- def _t_delete(ids, run_callbacks=true)
86
- objects = _t_find_bulk(ids)
87
- if run_callbacks
88
- objects.each { |object| object.destroy }
89
- else
90
- objects.each { |object| object.destroy! }
80
+ def _t_initialize_tenacity
81
+ after :save do |record|
82
+ record._t_save_autosave_associations
83
+ end
91
84
  end
92
- end
93
- end
94
85
 
95
- module InstanceMethods #:nodoc:
96
- # DataMapper will not issue callbacks unless at least one of the properties is dirty.
97
- # So, taint the object by marking one of the attributes as dirty, save the object,
98
- # and resture the persisted_state by reloading the object from the database.
99
- def save
100
- if clean?
101
- taint!; super; reload
102
- else
103
- super
86
+ def _t_initialize_has_one_association(association)
87
+ after :destroy do |record|
88
+ record._t_cleanup_has_one_association(association)
89
+ end
104
90
  end
105
- end
106
91
 
107
- def _t_reload
108
- reload
109
- end
92
+ def _t_initialize_has_many_association(association)
93
+ after :save do |record|
94
+ record.class._t_save_associates(record, association)
95
+ end
110
96
 
111
- def _t_clear_associates(association)
112
- self.repository.adapter.execute("delete from #{association.join_table} where #{association.association_key} = #{self.id}")
113
- end
97
+ after :destroy do |record|
98
+ record._t_cleanup_has_many_association(association)
99
+ end
100
+ end
114
101
 
115
- def _t_associate_many(association, associate_ids)
116
- self.transaction do
117
- _t_clear_associates(association)
118
- associate_ids.each do |associate_id|
119
- self.repository.adapter.execute("insert into #{association.join_table} (#{association.association_key}, #{association.association_foreign_key}) values (#{self.id}, '#{associate_id}')")
102
+ def _t_initialize_belongs_to_association(association)
103
+ after :destroy do |record|
104
+ record._t_cleanup_belongs_to_association(association)
120
105
  end
121
106
  end
122
- end
123
107
 
124
- def _t_get_associate_ids(association)
125
- return [] if self.id.nil?
126
- self.repository.adapter.select("select #{association.association_foreign_key} from #{association.join_table} where #{association.association_key} = #{self.id}")
108
+ def _t_delete(ids, run_callbacks=true)
109
+ objects = _t_find_bulk(ids)
110
+ if run_callbacks
111
+ objects.each { |object| object.destroy }
112
+ else
113
+ objects.each { |object| object.destroy! }
114
+ end
115
+ end
127
116
  end
128
117
 
129
- private
118
+ module InstanceMethods #:nodoc:
119
+ include Tenacity::OrmExt::Helpers
120
+
121
+ # DataMapper will not issue callbacks unless at least one of the properties is dirty.
122
+ # So, taint the object by marking one of the attributes as dirty, save the object,
123
+ # and resture the persisted_state by reloading the object from the database.
124
+ def save
125
+ if clean?
126
+ taint!; super; reload
127
+ else
128
+ super
129
+ end
130
+ end
131
+
132
+ def _t_reload
133
+ reload
134
+ end
135
+
136
+ def _t_clear_associates(association)
137
+ self.repository.adapter.execute("delete from #{association.join_table} where #{association.association_key} = #{_t_serialize_id_for_sql(self.id)}")
138
+ end
130
139
 
131
- def taint!
132
- property = properties.first.name
133
- self.persisted_state = ::DataMapper::Resource::State::Dirty.new(self) unless self.persisted_state.kind_of?(::DataMapper::Resource::State::Dirty)
134
- self.persisted_state.original_attributes[properties[property]] = self.send(property)
140
+ def _t_associate_many(association, associate_ids)
141
+ self.transaction do
142
+ _t_clear_associates(association)
143
+ associate_ids.each do |associate_id|
144
+ self.repository.adapter.execute("insert into #{association.join_table} (#{association.association_key}, #{association.association_foreign_key}) values (#{_t_serialize_id_for_sql(self.id)}, #{_t_serialize_id_for_sql(associate_id)})")
145
+ end
146
+ end
147
+ end
148
+
149
+ def _t_get_associate_ids(association)
150
+ return [] if self.id.nil?
151
+ self.repository.adapter.select("select #{association.association_foreign_key} from #{association.join_table} where #{association.association_key} = #{_t_serialize_id_for_sql(self.id)}")
152
+ end
153
+
154
+ private
155
+
156
+ def taint!
157
+ property = properties.first.name
158
+ self.persisted_state = ::DataMapper::Resource::State::Dirty.new(self) unless self.persisted_state.kind_of?(::DataMapper::Resource::State::Dirty)
159
+ self.persisted_state.original_attributes[properties[property]] = self.send(property)
160
+ end
135
161
  end
136
- end
137
162
 
163
+ end
138
164
  end
139
165
  end