tenacity 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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