tenacity 0.2.0 → 0.3.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 (69) hide show
  1. data/.gitignore +3 -0
  2. data/EXTEND.rdoc +11 -1
  3. data/README.rdoc +4 -1
  4. data/Rakefile +20 -9
  5. data/history.txt +21 -0
  6. data/lib/tenacity.rb +12 -4
  7. data/lib/tenacity/associates_proxy.rb +67 -0
  8. data/lib/tenacity/association.rb +19 -6
  9. data/lib/tenacity/associations/has_many.rb +6 -0
  10. data/lib/tenacity/class_methods.rb +52 -1
  11. data/lib/tenacity/instance_methods.rb +7 -3
  12. data/lib/tenacity/orm_ext/activerecord.rb +30 -13
  13. data/lib/tenacity/orm_ext/couchrest.rb +140 -0
  14. data/lib/tenacity/orm_ext/datamapper.rb +139 -0
  15. data/lib/tenacity/orm_ext/mongo_mapper.rb +88 -80
  16. data/lib/tenacity/orm_ext/mongoid.rb +108 -0
  17. data/lib/tenacity/orm_ext/sequel.rb +134 -0
  18. data/lib/tenacity/version.rb +1 -1
  19. data/tenacity.gemspec +14 -3
  20. data/test/association_features/belongs_to_test.rb +42 -0
  21. data/test/association_features/has_many_test.rb +110 -0
  22. data/test/association_features/has_one_test.rb +41 -0
  23. data/test/associations/belongs_to_test.rb +36 -127
  24. data/test/associations/has_many_test.rb +77 -196
  25. data/test/associations/has_one_test.rb +22 -84
  26. data/test/core/classmethods_test.rb +24 -22
  27. data/test/fixtures/active_record_has_many_target.rb +10 -0
  28. data/test/fixtures/active_record_has_one_target.rb +10 -0
  29. data/test/fixtures/{active_record_nuts.rb → active_record_nut.rb} +0 -0
  30. data/test/fixtures/active_record_object.rb +17 -0
  31. data/test/fixtures/couch_rest_door.rb +0 -2
  32. data/test/fixtures/couch_rest_has_many_target.rb +12 -0
  33. data/test/fixtures/couch_rest_has_one_target.rb +12 -0
  34. data/test/fixtures/couch_rest_object.rb +19 -0
  35. data/test/fixtures/couch_rest_windshield.rb +0 -2
  36. data/test/fixtures/data_mapper_has_many_target.rb +19 -0
  37. data/test/fixtures/data_mapper_has_one_target.rb +19 -0
  38. data/test/fixtures/data_mapper_object.rb +20 -0
  39. data/test/fixtures/mongo_mapper_ash_tray.rb +0 -2
  40. data/test/fixtures/mongo_mapper_dashboard.rb +0 -2
  41. data/test/fixtures/mongo_mapper_has_many_target.rb +11 -0
  42. data/test/fixtures/mongo_mapper_has_one_target.rb +11 -0
  43. data/test/fixtures/mongo_mapper_object.rb +18 -0
  44. data/test/fixtures/mongo_mapper_vent.rb +0 -2
  45. data/test/fixtures/mongo_mapper_wheel.rb +0 -2
  46. data/test/fixtures/mongoid_has_many_target.rb +13 -0
  47. data/test/fixtures/mongoid_has_one_target.rb +13 -0
  48. data/test/fixtures/mongoid_object.rb +20 -0
  49. data/test/fixtures/sequel_has_many_target.rb +10 -0
  50. data/test/fixtures/sequel_has_one_target.rb +10 -0
  51. data/test/fixtures/sequel_object.rb +17 -0
  52. data/test/helpers/active_record_test_helper.rb +51 -0
  53. data/test/helpers/data_mapper_test_helper.rb +44 -0
  54. data/test/helpers/mongoid_test_helper.rb +21 -0
  55. data/test/helpers/sequel_test_helper.rb +60 -0
  56. data/test/orm_ext/activerecord_test.rb +55 -35
  57. data/test/orm_ext/couchrest_test.rb +66 -46
  58. data/test/orm_ext/datamapper_test.rb +112 -0
  59. data/test/orm_ext/mongo_mapper_test.rb +64 -44
  60. data/test/orm_ext/mongoid_test.rb +121 -0
  61. data/test/orm_ext/sequel_test.rb +113 -0
  62. data/test/test_helper.rb +87 -11
  63. metadata +159 -59
  64. data/lib/tenacity/orm_ext/couchrest/couchrest_extended_document.rb +0 -42
  65. data/lib/tenacity/orm_ext/couchrest/couchrest_model.rb +0 -44
  66. data/lib/tenacity/orm_ext/couchrest/tenacity_class_methods.rb +0 -43
  67. data/lib/tenacity/orm_ext/couchrest/tenacity_instance_methods.rb +0 -21
  68. data/test/fixtures/couch_rest_radio.rb +0 -10
  69. data/test/fixtures/mongo_mapper_button.rb +0 -6
@@ -0,0 +1,108 @@
1
+ module Tenacity
2
+ # Tenacity relationships on Mongoid 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
7
+ # include Mongoid::Document
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 key named after the association.
18
+ # The example above will create a key named <tt>:driver_id</tt>
19
+ #
20
+ #
21
+ # == t_has_one
22
+ #
23
+ # The +t_has_one+ association will not define any new keys 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 key named after the association.
30
+ # The example above will create a key named <tt>:wheels_ids</tt>
31
+ #
32
+ module Mongoid
33
+
34
+ def self.setup(model) #:nodoc:
35
+ require 'mongoid'
36
+ if model.included_modules.include?(::Mongoid::Document)
37
+ model.send :include, Mongoid::InstanceMethods
38
+ model.extend Mongoid::ClassMethods
39
+ end
40
+ rescue LoadError
41
+ # Mongoid not available
42
+ end
43
+
44
+ module ClassMethods #:nodoc:
45
+ def _t_find(id)
46
+ (id.nil? || id.to_s.strip == "") ? nil : find(id)
47
+ rescue ::Mongoid::Errors::DocumentNotFound
48
+ nil
49
+ end
50
+
51
+ def _t_find_bulk(ids)
52
+ find(ids)
53
+ rescue ::Mongoid::Errors::DocumentNotFound
54
+ []
55
+ end
56
+
57
+ def _t_find_first_by_associate(property, id)
58
+ find(:first, :conditions => { property => id })
59
+ end
60
+
61
+ def _t_find_all_by_associate(property, id)
62
+ find(:all, :conditions => { property => id })
63
+ end
64
+
65
+ def _t_initialize_has_many_association(association)
66
+ unless self.respond_to?(association.foreign_keys_property)
67
+ field association.foreign_keys_property, :type => Array
68
+ after_save { |record| self.class._t_save_associates(record, association) }
69
+ end
70
+ end
71
+
72
+ def _t_initialize_belongs_to_association(association)
73
+ unless self.respond_to?(association.foreign_key)
74
+ field association.foreign_key, :type => String
75
+ before_save { |record| self.class._t_stringify_belongs_to_value(record, association) }
76
+ end
77
+ end
78
+
79
+ def _t_delete(ids, run_callbacks=true)
80
+ docs = _t_find_bulk(ids)
81
+ if run_callbacks
82
+ docs.each { |doc| doc.destroy }
83
+ else
84
+ docs.each { |doc| doc.delete }
85
+ end
86
+ end
87
+ end
88
+
89
+ module InstanceMethods #:nodoc:
90
+ def _t_reload
91
+ reload
92
+ end
93
+
94
+ def _t_associate_many(association, associate_ids)
95
+ self.send(association.foreign_keys_property + '=', associate_ids.map { |associate_id| associate_id.to_s })
96
+ end
97
+
98
+ def _t_get_associate_ids(association)
99
+ self.send(association.foreign_keys_property)
100
+ end
101
+
102
+ def _t_clear_associates(association)
103
+ self.send(association.foreign_keys_property + '=', [])
104
+ end
105
+ end
106
+
107
+ end
108
+ end
@@ -0,0 +1,134 @@
1
+ module Tenacity
2
+ # Tenacity relationships on Sequel 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 < Sequel::Model
7
+ # include Tenacity
8
+ #
9
+ # t_has_many :wheels
10
+ # t_has_one :dashboard
11
+ # t_belongs_to :driver
12
+ # end
13
+ #
14
+ #
15
+ # == t_belongs_to
16
+ #
17
+ # The +t_belongs_to+ association requires that a property exist in the table
18
+ # to hold the id of the assoicated object.
19
+ #
20
+ # DB.create_table :cars do
21
+ # primary_key :id
22
+ # String :driver_id
23
+ # end
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
+ # DB.create_table :cars_wheels do
40
+ # Integer :car_id
41
+ # String :wheel_id
42
+ # end
43
+ #
44
+ module Sequel
45
+
46
+ def self.setup(model)
47
+ require 'sequel'
48
+ if model.ancestors.include?(::Sequel::Model)
49
+ model.send :include, Sequel::InstanceMethods
50
+ model.extend Sequel::ClassMethods
51
+ end
52
+ rescue LoadError
53
+ # Sequel not available
54
+ end
55
+
56
+ module ClassMethods #:nodoc:
57
+ attr_accessor :_t_has_many_associations
58
+ attr_accessor :_t_belongs_to_associations
59
+
60
+ def _t_find(id)
61
+ self[id]
62
+ end
63
+
64
+ def _t_find_bulk(ids)
65
+ return [] if ids.nil? || ids.empty?
66
+ filter(:id => ids)
67
+ end
68
+
69
+ def _t_find_first_by_associate(property, id)
70
+ first(property.to_sym => id)
71
+ end
72
+
73
+ def _t_find_all_by_associate(property, id)
74
+ filter(property => id)
75
+ end
76
+
77
+ def _t_initialize_has_many_association(association)
78
+ @_t_has_many_associations ||= []
79
+ @_t_has_many_associations << association
80
+ end
81
+
82
+ def _t_initialize_belongs_to_association(association)
83
+ @_t_belongs_to_associations ||= []
84
+ @_t_belongs_to_associations << association
85
+ end
86
+
87
+ def _t_delete(ids, run_callbacks=true)
88
+ if run_callbacks
89
+ filter(:id => ids).destroy
90
+ else
91
+ filter(:id => ids).delete
92
+ end
93
+ end
94
+ end
95
+
96
+ module InstanceMethods #:nodoc:
97
+ def before_save
98
+ associations = self.class._t_belongs_to_associations || []
99
+ associations.each { |association| self.class._t_stringify_belongs_to_value(self, association) }
100
+ super
101
+ end
102
+
103
+ def after_save
104
+ associations = self.class._t_has_many_associations || []
105
+ associations.each { |association| self.class._t_save_associates(self, association) }
106
+ super
107
+ end
108
+
109
+ def _t_reload
110
+ reload
111
+ end
112
+
113
+ def _t_clear_associates(association)
114
+ db["delete from #{association.join_table} where #{association.association_key} = #{self.id}"].delete
115
+ end
116
+
117
+ def _t_associate_many(association, associate_ids)
118
+ db.transaction do
119
+ _t_clear_associates(association)
120
+ associate_ids.each do |associate_id|
121
+ db["insert into #{association.join_table} (#{association.association_key}, #{association.association_foreign_key}) values (#{self.id}, '#{associate_id}')"].insert
122
+ end
123
+ end
124
+ end
125
+
126
+ def _t_get_associate_ids(association)
127
+ return [] if self.id.nil?
128
+ rows = db["select #{association.association_foreign_key} from #{association.join_table} where #{association.association_key} = #{self.id}"].all
129
+ rows.map { |row| row[association.association_foreign_key.to_sym] }
130
+ end
131
+ end
132
+
133
+ end
134
+ end
@@ -1,3 +1,3 @@
1
1
  module Tenacity
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -22,12 +22,23 @@ Gem::Specification.new do |s|
22
22
  s.add_development_dependency "rake", "~> 0.8.7"
23
23
  s.add_development_dependency "rcov", "~> 0.9.9"
24
24
  s.add_development_dependency "shoulda", "~> 2.11.3"
25
+ s.add_development_dependency "yard", "~> 0.6.4"
26
+
27
+ # Relational DBs
28
+ s.add_development_dependency "sqlite3-ruby", "~> 1.3.1"
29
+ s.add_development_dependency "activerecord", "~> 3.0.0"
30
+ s.add_development_dependency "datamapper", "~> 1.0.2"
31
+ s.add_development_dependency "dm-sqlite-adapter", "~> 1.0.2"
32
+ s.add_development_dependency "sequel", "~> 3.19.0"
33
+
34
+ # MongoDB
25
35
  s.add_development_dependency "mongo_mapper", "~> 0.8.6"
26
36
  s.add_development_dependency "bson_ext", "~> 1.1.3"
27
- s.add_development_dependency "activerecord", "~> 3.0.0"
28
- s.add_development_dependency "sqlite3-ruby", "~> 1.3.1"
37
+ s.add_development_dependency "mongoid", "~> 2.0.0.beta"
38
+
39
+ # CouchDB
29
40
  s.add_development_dependency "couchrest", "~> 1.0.0"
30
- s.add_development_dependency "couchrest_model"
41
+ s.add_development_dependency "couchrest_model", "~> 1.0.0.beta"
31
42
 
32
43
  s.files = `git ls-files`.split("\n")
33
44
  s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
@@ -0,0 +1,42 @@
1
+ require 'test_helper'
2
+
3
+ class BelongsToTest < Test::Unit::TestCase
4
+
5
+ context "A class with a belongs_to association to another class" do
6
+ setup do
7
+ setup_fixtures
8
+ @car = ActiveRecordCar.create
9
+ @wheel = MongoMapperWheel.create(:active_record_car => @car)
10
+ end
11
+
12
+ should "memoize the association" do
13
+ assert_equal @car, @wheel.active_record_car
14
+
15
+ other_car = ActiveRecordCar.create
16
+ assert_equal @car, MongoMapperWheel.find(@wheel.id).active_record_car
17
+ MongoMapperWheel.update(@wheel.id, :active_record_car => other_car)
18
+ assert_equal other_car, MongoMapperWheel.find(@wheel.id).active_record_car
19
+
20
+ assert_equal @car, @wheel.active_record_car
21
+ assert_equal other_car, @wheel.active_record_car(true)
22
+ end
23
+
24
+ should "be able to specify the class name of the associated class" do
25
+ dashboard = MongoMapperDashboard.create
26
+ ash_tray = MongoMapperAshTray.create(:dashboard => dashboard)
27
+ assert_equal dashboard, ash_tray.dashboard
28
+ end
29
+
30
+ should "be able to specify the foreign key to use for the associated class" do
31
+ car = ActiveRecordCar.create
32
+ windshield = CouchRestWindshield.create(:active_record_car => car)
33
+ assert_equal car.id.to_s, windshield.car_id
34
+ assert !windshield.respond_to?(:active_record_car_id)
35
+
36
+ engine = ActiveRecordEngine.create(:active_record_car => car)
37
+ assert_equal car.id, engine.car_id
38
+ assert !engine.respond_to?(:active_record_car_id)
39
+ end
40
+ end
41
+
42
+ end
@@ -0,0 +1,110 @@
1
+ require 'test_helper'
2
+
3
+ class HasManyTest < Test::Unit::TestCase
4
+
5
+ context "A class with a belongs_to association to another class" do
6
+ setup do
7
+ setup_fixtures
8
+ @car = ActiveRecordCar.create
9
+ @wheels = [MongoMapperWheel.create, MongoMapperWheel.create, MongoMapperWheel.create]
10
+
11
+ @car.mongo_mapper_wheels = @wheels
12
+ @car.save
13
+ end
14
+
15
+ should "memoize the association" do
16
+ assert_equal @wheels, @car.mongo_mapper_wheels
17
+
18
+ other_wheels = [MongoMapperWheel.create, MongoMapperWheel.create, MongoMapperWheel.create]
19
+ assert_equal @wheels, ActiveRecordCar.find(@car.id).mongo_mapper_wheels
20
+ ActiveRecordCar.find(@car.id).update_attribute(:mongo_mapper_wheels, other_wheels)
21
+ assert_equal other_wheels, ActiveRecordCar.find(@car.id).mongo_mapper_wheels
22
+
23
+ assert_equal @wheels, @car.mongo_mapper_wheels
24
+ assert_equal other_wheels, @car.mongo_mapper_wheels(true)
25
+ end
26
+
27
+ should "be able to specify the class name of the associated class" do
28
+ vent_1 = MongoMapperVent.create
29
+ vent_2 = MongoMapperVent.create
30
+ vent_3 = MongoMapperVent.create
31
+ dashboard = MongoMapperDashboard.create
32
+ dashboard.vents = [vent_1, vent_2, vent_3]
33
+ dashboard.save
34
+ assert_set_equal [vent_1, vent_2, vent_3], MongoMapperDashboard.find(dashboard.id).vents
35
+ end
36
+
37
+ should "be able to specify the foreign key to use for the class" do
38
+ car = ActiveRecordCar.create
39
+ door_1 = CouchRestDoor.create({})
40
+ door_2 = CouchRestDoor.create({})
41
+ door_3 = CouchRestDoor.create({})
42
+ car.couch_rest_doors = [door_1, door_2, door_3]
43
+ car.save
44
+
45
+ assert_set_equal [door_1, door_2, door_3], ActiveRecordCar.find(car.id).couch_rest_doors
46
+ assert_set_equal [door_1.id.to_s, door_2.id.to_s, door_3.id.to_s], ActiveRecordCar.find(car.id).couch_rest_door_ids
47
+ end
48
+
49
+ should "save the associate object when it is added as an associate if the parent object is saved" do
50
+ car = ActiveRecordCar.create
51
+ door_1 = CouchRestDoor.new({})
52
+ assert_nil door_1.id
53
+
54
+ old_count = CouchRestDoor.count
55
+ car.couch_rest_doors << door_1
56
+ assert_equal old_count + 1, CouchRestDoor.count
57
+ end
58
+
59
+ should "not save the associate object when it is added as an associate if the parent object is not saved" do
60
+ car = ActiveRecordCar.new
61
+ door_1 = CouchRestDoor.new({})
62
+ assert_nil door_1.id
63
+
64
+ old_count = CouchRestDoor.count
65
+ car.couch_rest_doors << door_1
66
+ assert_equal old_count, CouchRestDoor.count
67
+ end
68
+
69
+ should "save all unsaved associates when the parent object is saved" do
70
+ car = ActiveRecordCar.new
71
+ door_1 = CouchRestDoor.new({})
72
+ assert_nil door_1.id
73
+
74
+ old_count = CouchRestDoor.count
75
+ car.couch_rest_doors << door_1
76
+ assert_equal old_count, CouchRestDoor.count
77
+ car.save
78
+ assert_equal old_count + 1, CouchRestDoor.count
79
+ assert_set_equal [door_1], ActiveRecordCar.find(car.id).couch_rest_doors
80
+ end
81
+
82
+ context "with a set of associates that need to be deleted" do
83
+ setup do
84
+ @new_car = ActiveRecordCar.create
85
+ @door_1 = CouchRestDoor.create({})
86
+ @door_2 = CouchRestDoor.create({})
87
+ @door_3 = CouchRestDoor.create({})
88
+ @new_car.couch_rest_doors = [@door_1, @door_2, @door_3]
89
+ @new_car.save
90
+ end
91
+
92
+ should "be able to delete all associates" do
93
+ assert_set_equal [@door_1, @door_2, @door_3], ActiveRecordCar.find(@new_car.id).couch_rest_doors
94
+ old_count = CouchRestDoor.count
95
+ @new_car.couch_rest_doors.delete_all
96
+ assert_equal old_count - 3, CouchRestDoor.count
97
+ assert_set_equal [], ActiveRecordCar.find(@new_car.id).couch_rest_doors
98
+ end
99
+
100
+ should "be able to destroy all associates" do
101
+ assert_set_equal [@door_1, @door_2, @door_3], ActiveRecordCar.find(@new_car.id).couch_rest_doors
102
+ old_count = CouchRestDoor.count
103
+ @new_car.couch_rest_doors.destroy_all
104
+ assert_equal old_count - 3, CouchRestDoor.count
105
+ assert_set_equal [], ActiveRecordCar.find(@new_car.id).couch_rest_doors
106
+ end
107
+ end
108
+ end
109
+
110
+ end
@@ -0,0 +1,41 @@
1
+ require 'test_helper'
2
+
3
+ class HasOneTest < Test::Unit::TestCase
4
+
5
+ context "A class with a has_one association to another class" do
6
+ setup do
7
+ setup_fixtures
8
+ @climate_control_unit = ActiveRecordClimateControlUnit.create
9
+ @dashboard = MongoMapperDashboard.create(:active_record_climate_control_unit => @climate_control_unit)
10
+ end
11
+
12
+ should "memoize the association" do
13
+ assert_equal @climate_control_unit, @dashboard.active_record_climate_control_unit
14
+
15
+ other_climate_control_unit = ActiveRecordClimateControlUnit.create
16
+ assert_equal @climate_control_unit, MongoMapperDashboard.find(@dashboard.id).active_record_climate_control_unit
17
+ ActiveRecordClimateControlUnit.update(@climate_control_unit.id, :mongo_mapper_dashboard_id => nil)
18
+ ActiveRecordClimateControlUnit.update(other_climate_control_unit.id, :mongo_mapper_dashboard_id => @dashboard.id)
19
+ assert_equal other_climate_control_unit, MongoMapperDashboard.find(@dashboard.id).active_record_climate_control_unit
20
+
21
+ assert_equal @climate_control_unit, @dashboard.active_record_climate_control_unit
22
+ assert_equal other_climate_control_unit, @dashboard.active_record_climate_control_unit(true)
23
+ end
24
+
25
+ should "be able to specify the class name of the associated class" do
26
+ ash_tray = MongoMapperAshTray.create
27
+ dashboard = MongoMapperDashboard.create(:ash_tray => ash_tray)
28
+ assert_equal ash_tray, dashboard.ash_tray
29
+ end
30
+
31
+ should "be able to specify the foreign key to use for the class" do
32
+ car = ActiveRecordCar.create
33
+ windshield = CouchRestWindshield.create(:active_record_car => car)
34
+ assert_equal windshield, car.couch_rest_windshield
35
+
36
+ engine = ActiveRecordEngine.create(:active_record_car => car)
37
+ assert_equal engine, car.active_record_engine
38
+ end
39
+ end
40
+
41
+ end