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
data/.gitignore CHANGED
@@ -1,7 +1,10 @@
1
1
  *.swp
2
2
  *.swo
3
+ .yardoc
4
+ .rvmrc
3
5
  .bundle
4
6
  rdoc/
7
+ doc/
5
8
  coverage/
6
9
  pkg/
7
10
  Gemfile.lock
@@ -30,7 +30,8 @@ return nil.
30
30
  _t_find_bulk(ids=[])
31
31
 
32
32
  Find many objects by the specified ids, and return them in an array.
33
- If no objects could be found, return an empty array.
33
+ If no objects could be found, return an empty array. If only some of
34
+ the objects could be found, then simply return those objects in the array.
34
35
 
35
36
  _t_find_first_by_associate(property, id)
36
37
 
@@ -41,6 +42,15 @@ and return it. If no object could be found, return nil.
41
42
 
42
43
  Find all objects by the specified property name, with the specified id, and
43
44
  return them in an array. If no objects could be found, return an empty array.
45
+ If only some of the objects could be found, then simply return those objects
46
+ in the array.
47
+
48
+ _t_delete(ids, run_callbacks=true)
49
+
50
+ Delete all objects with the specified ids. If <tt>run_callbacks</tt> is true, the
51
+ objects should be deleted in such a way that their delete callback methods
52
+ are run. If false, the objects should be deleted in such a way that their
53
+ delete callback meethods are not run. Return nothing.
44
54
 
45
55
  _t_initialize_has_many_association(association)
46
56
 
@@ -83,8 +83,11 @@ much the same way, supporting many of the same options.
83
83
  == Supported Database Clients
84
84
 
85
85
  * ActiveRecord
86
- * MongoMapper
87
86
  * CouchRest (CouchModel and ExtendedDocument)
87
+ * DataMapper
88
+ * MongoMapper
89
+ * Mongoid
90
+ * Sequel
88
91
 
89
92
  See EXTEND.rdoc for information on extending Tenacity to work with other database clients.
90
93
 
data/Rakefile CHANGED
@@ -10,16 +10,21 @@ rescue Bundler::BundlerError => e
10
10
  $stderr.puts "Run `bundle install` to install missing gems"
11
11
  exit e.status_code
12
12
  end
13
- require 'rake'
14
13
 
14
+ require 'rake'
15
15
  require 'rake/testtask'
16
+ require 'rcov/rcovtask'
17
+ require 'rake/rdoctask'
18
+ require 'yard'
19
+
20
+ task :default => :test
21
+
16
22
  Rake::TestTask.new(:test) do |test|
17
23
  test.libs << 'lib' << 'test' << 'test/fixtures'
18
- test.pattern = 'test/**/*_test.rb'
24
+ test.pattern = ENV['TEST'] || "test/**/*_test.rb"
19
25
  test.verbose = true
20
26
  end
21
27
 
22
- require 'rcov/rcovtask'
23
28
  Rcov::RcovTask.new do |test|
24
29
  test.libs << 'test' << 'test/fixtures'
25
30
  test.pattern = 'test/**/*_test.rb'
@@ -27,12 +32,8 @@ Rcov::RcovTask.new do |test|
27
32
  test.rcov_opts << '--exclude "gems/*"'
28
33
  end
29
34
 
30
- task :default => :test
31
-
32
- require 'rake/rdoctask'
33
35
  Rake::RDocTask.new do |rdoc|
34
36
  version = File.exist?('VERSION') ? File.read('VERSION') : ""
35
-
36
37
  rdoc.rdoc_dir = 'rdoc'
37
38
  rdoc.title = "tenacity #{version}"
38
39
  rdoc.rdoc_files.include('README*')
@@ -40,6 +41,16 @@ Rake::RDocTask.new do |rdoc|
40
41
  rdoc.rdoc_files.include('lib/**/*.rb')
41
42
  end
42
43
 
43
- desc 'Delete rcov, rdoc, and other generated files'
44
- task :clobber => [:clobber_rcov, :clobber_rdoc]
44
+ YARD::Rake::YardocTask.new do |t|
45
+ t.files = ['lib/**/*.rb', '-', 'EXTEND.rdoc']
46
+ end
47
+
48
+ desc 'Delete rcov, rdoc, yard, and other generated files'
49
+ task :clobber => [:clobber_rcov, :clobber_rdoc, :clobber_yard]
50
+
51
+ desc 'Delete yard generated files'
52
+ task :clobber_yard do
53
+ puts 'rm -rf doc .yardoc'
54
+ FileUtils.rm_rf ['doc', '.yardoc']
55
+ end
45
56
 
@@ -1,3 +1,24 @@
1
+ == 0.3.0
2
+
3
+ * Major enhancements
4
+
5
+ * Added support for Mongoid
6
+ * Added support for DataMapper
7
+ * Added support for Sequel
8
+
9
+ * Minor enhancements
10
+
11
+ * Automatically save object when added to a t_has_many association, unless the
12
+ parent object is not yet saved itself.
13
+ * Added suport for destroy_all to the t_has_many association
14
+ * Added suport for delete_all to the t_has_many association
15
+ * Added support for push and concat to t_has_many association
16
+
17
+ * Bug fixes
18
+
19
+ * Found and fixed many minor bugs thanks to a new test suite that tests all
20
+ associations against all supported database clients.
21
+
1
22
  == 0.2.0
2
23
 
3
24
  * Major enhancements
@@ -1,5 +1,6 @@
1
1
  require File.join('active_support', 'inflector')
2
2
 
3
+ require File.join(File.dirname(__FILE__), 'tenacity', 'associates_proxy')
3
4
  require File.join(File.dirname(__FILE__), 'tenacity', 'association')
4
5
  require File.join(File.dirname(__FILE__), 'tenacity', 'class_methods')
5
6
  require File.join(File.dirname(__FILE__), 'tenacity', 'instance_methods')
@@ -7,11 +8,11 @@ require File.join(File.dirname(__FILE__), 'tenacity', 'associations', 'belongs_t
7
8
  require File.join(File.dirname(__FILE__), 'tenacity', 'associations', 'has_many')
8
9
  require File.join(File.dirname(__FILE__), 'tenacity', 'associations', 'has_one')
9
10
  require File.join(File.dirname(__FILE__), 'tenacity', 'orm_ext', 'activerecord')
10
- require File.join(File.dirname(__FILE__), 'tenacity', 'orm_ext', 'couchrest', 'tenacity_class_methods')
11
- require File.join(File.dirname(__FILE__), 'tenacity', 'orm_ext', 'couchrest', 'tenacity_instance_methods')
12
- require File.join(File.dirname(__FILE__), 'tenacity', 'orm_ext', 'couchrest', 'couchrest_extended_document')
13
- require File.join(File.dirname(__FILE__), 'tenacity', 'orm_ext', 'couchrest', 'couchrest_model')
11
+ require File.join(File.dirname(__FILE__), 'tenacity', 'orm_ext', 'couchrest')
12
+ require File.join(File.dirname(__FILE__), 'tenacity', 'orm_ext', 'datamapper')
14
13
  require File.join(File.dirname(__FILE__), 'tenacity', 'orm_ext', 'mongo_mapper')
14
+ require File.join(File.dirname(__FILE__), 'tenacity', 'orm_ext', 'mongoid')
15
+ require File.join(File.dirname(__FILE__), 'tenacity', 'orm_ext', 'sequel')
15
16
 
16
17
  module Tenacity #:nodoc:
17
18
  include InstanceMethods
@@ -21,6 +22,13 @@ module Tenacity #:nodoc:
21
22
  include HasOne
22
23
 
23
24
  def self.included(model)
25
+ ActiveRecord.setup(model)
26
+ CouchRest.setup(model)
27
+ DataMapper.setup(model)
28
+ MongoMapper.setup(model)
29
+ Mongoid.setup(model)
30
+ Sequel.setup(model)
31
+
24
32
  raise "Tenacity does not support the database client used by #{model}" unless model.respond_to?(:_t_find)
25
33
  model.extend(ClassMethods)
26
34
  end
@@ -0,0 +1,67 @@
1
+ module Tenacity
2
+ class AssociatesProxy #:nodoc:
3
+ alias_method :proxy_respond_to?, :respond_to?
4
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ }
5
+
6
+ def initialize(parent, target, association)
7
+ @parent = parent
8
+ @target = target
9
+ @association = association
10
+ end
11
+
12
+ def respond_to?(*args)
13
+ proxy_respond_to?(*args) || @target.respond_to?(*args)
14
+ end
15
+
16
+ # Explicitly proxy === because the instance method removal above doesn't catch it.
17
+ def ===(other)
18
+ other === @target
19
+ end
20
+
21
+ def <<(object)
22
+ object.save unless @parent.id.nil?
23
+ @target << object
24
+ end
25
+
26
+ def push(*objects)
27
+ objects.each { |object| object.save } unless @parent.id.nil?
28
+ @target.push(*objects)
29
+ end
30
+
31
+ def concat(objects)
32
+ objects.each { |object| object.save } unless @parent.id.nil?
33
+ @target.concat(objects)
34
+ end
35
+
36
+ def destroy_all
37
+ ids = prepare_for_delete
38
+ @association.associate_class._t_delete(ids)
39
+ end
40
+
41
+ def delete_all
42
+ ids = prepare_for_delete
43
+ @association.associate_class._t_delete(ids, false)
44
+ end
45
+
46
+ def inspect
47
+ @target.inspect
48
+ end
49
+
50
+ private
51
+
52
+ def prepare_for_delete
53
+ ids = @parent._t_get_associate_ids(@association)
54
+ @parent._t_remove_associates(@association)
55
+ @parent.save
56
+ ids
57
+ end
58
+
59
+ def method_missing(method, *args)
60
+ if block_given?
61
+ @target.send(method, *args) { |*block_args| yield(*block_args) }
62
+ else
63
+ @target.send(method, *args)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -1,5 +1,10 @@
1
1
  module Tenacity
2
+
3
+ # The Associaiton class represents a Tenacity association. Using this class,
4
+ # you can retrieve all sorts of information about the association, including
5
+ # it name, type, source, target class, etc.
2
6
  class Association
7
+
3
8
  # Type type of the association (<tt>:t_has_one</tt>, <tt>:t_has_many</tt>, or <tt>:t_belongs_to</tt>)
4
9
  attr_reader :type
5
10
 
@@ -62,21 +67,29 @@ module Tenacity
62
67
 
63
68
  # Get the name of the join table used by this association
64
69
  def join_table
65
- if @join_table || @source.respond_to?(:table_name)
66
- @join_table || (name.to_s < @source.table_name ? "#{name}_#{@source.table_name}" : "#{@source.table_name}_#{name}")
67
- end
70
+ table_name = fetch_table_name
71
+ @join_table || (name.to_s < table_name ? "#{name}_#{table_name}" : "#{table_name}_#{name}")
68
72
  end
69
73
 
70
74
  # Get the name of the column in the join table that represents this object
71
75
  def association_key
72
- if @association_key || @source.respond_to?(:table_name)
73
- @association_key || @source.table_name.singularize + '_id'
74
- end
76
+ table_name = fetch_table_name
77
+ @association_key || table_name.singularize + '_id'
75
78
  end
76
79
 
77
80
  # Get the name of the column in the join table that represents the associated object
78
81
  def association_foreign_key
79
82
  @association_foreign_key || name.to_s.singularize + '_id'
80
83
  end
84
+
85
+ private
86
+
87
+ def fetch_table_name
88
+ if @source.respond_to?(:table_name)
89
+ @source.table_name.to_s
90
+ else
91
+ "#{ActiveSupport::Inflector.underscore(@source)}s"
92
+ end
93
+ end
81
94
  end
82
95
  end
@@ -1,6 +1,11 @@
1
1
  module Tenacity
2
2
  module HasMany #:nodoc:
3
3
 
4
+ def _t_remove_associates(association)
5
+ instance_variable_set _t_ivar_name(association), []
6
+ _t_clear_associates(association)
7
+ end
8
+
4
9
  private
5
10
 
6
11
  def has_many_associates(association)
@@ -39,6 +44,7 @@ module Tenacity
39
44
 
40
45
  associates = (record.instance_variable_get record._t_ivar_name(association)) || []
41
46
  associates.each do |associate|
47
+ associate._t_reload
42
48
  associate.send("#{association.foreign_key(record.class)}=", record.id.to_s)
43
49
  save_associate(associate)
44
50
  end
@@ -7,6 +7,7 @@ module Tenacity
7
7
  # methods.
8
8
  #
9
9
  # class Project
10
+ # include SupportedDatabaseClient
10
11
  # include Tenacity
11
12
  #
12
13
  # t_belongs_to :portfolio
@@ -57,6 +58,47 @@ module Tenacity
57
58
  # t_belongs_to :manager # foreign key - manager_id
58
59
  # end
59
60
  #
61
+ # == Is it a +t_belongs_to+ or +t_has_one+ association?
62
+ #
63
+ # Both express a 1-1 relationship. The difference is mostly where to place
64
+ # the foreign key, which is owned by the class declaring the +t_belongs_to+
65
+ # relationship. Example:
66
+ #
67
+ # class Employee < ActiveRecord::Base
68
+ # include Tenacity
69
+ # t_has_one :office
70
+ # end
71
+ #
72
+ # class Office
73
+ # include MongoMapper::Document
74
+ # include Tenacity
75
+ # t_belongs_to :employee
76
+ # end
77
+ #
78
+ # In this example, the foreign key, <tt>employee_id</tt>, would belong to the
79
+ # Office class. If possible, tenacity will define the property to hold the
80
+ # foreign key. When it cannot, it assumes that the foreign key has been
81
+ # defined. See the documentation for the respective database client
82
+ # extension to see if tenacity will declare the foreign_key property.
83
+ #
84
+ # == Unsaved objects and associations
85
+ #
86
+ # You can manipulate objects and associations before they are saved to the database, but there is some special behavior you should be
87
+ # aware of, mostly involving the saving of associated objects.
88
+ #
89
+ # === One-to-one associations
90
+ #
91
+ # * Assigning an object to a +t_has_one+ association automatically saves that object and the object being replaced (if there is one), in
92
+ # order to update their primary keys - except if the parent object is not yet stored in the database.
93
+ # * Assigning an object to a +t_belongs_to+ association does not save the object, since the foreign key field belongs on the parent. It
94
+ # does not save the parent either.
95
+ #
96
+ # === Collections
97
+ #
98
+ # * Adding an object to a collection (+t_has_many+) automatically saves that object, except if the parent object
99
+ # (the owner of the collection) is not yet stored in the database.
100
+ # * All unsaved members of the collection are automatically saved when the parent is saved.
101
+ #
60
102
  # == Caching
61
103
  #
62
104
  # All of the methods are built on a simple caching principle that will keep the result
@@ -218,9 +260,18 @@ module Tenacity
218
260
  # An empty array is returned if none are found.
219
261
  # [collection<<(object, ...)]
220
262
  # Adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
221
- # Note that this operation does not update the association until the parent object is saved.
263
+ # [collection.push(object, ...)]
264
+ # Adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
265
+ # [collection.concat(other_array)]
266
+ # Adds the objects in the other array to the collection by setting their foreign keys to the collection's primary key.
222
267
  # [collection.delete(object, ...)]
223
268
  # Removes one or more objects from the collection.
269
+ # [collection.destroy_all]
270
+ # Removes all objects from the collection, and deletes them from their respective
271
+ # database. If the deleted objects have any delete callbacks defined, they will be called.
272
+ # [collection.delete_all]
273
+ # Removes all objects from the collection, and deletes them from their respective
274
+ # database. No delete callbacks will be called, regardless of whether or not they are defined.
224
275
  # [collection=objects]
225
276
  # Replaces the collections content by setting it to the list of specified objects.
226
277
  # [collection_singular_ids]
@@ -8,11 +8,11 @@ module Tenacity
8
8
  private
9
9
 
10
10
  def get_associate(association, params)
11
- _t_reload
11
+ _t_reload unless id.nil?
12
12
  force_reload = params.first unless params.empty?
13
- value = instance_variable_get _t_ivar_name(association)
13
+ value = create_proxy(instance_variable_get(_t_ivar_name(association)), association)
14
14
  if value.nil? || force_reload
15
- value = yield
15
+ value = create_proxy(yield, association)
16
16
  instance_variable_set _t_ivar_name(association), value
17
17
  end
18
18
  value
@@ -23,5 +23,9 @@ module Tenacity
23
23
  instance_variable_set _t_ivar_name(association), associate
24
24
  end
25
25
 
26
+ def create_proxy(value, association)
27
+ value.respond_to?(:each) ? AssociatesProxy.new(self, value, association) : value
28
+ end
29
+
26
30
  end
27
31
  end
@@ -1,6 +1,4 @@
1
- begin
2
- require 'active_record'
3
-
1
+ module Tenacity
4
2
  # Tenacity relationships on ActiveRecord objects require that certain columns
5
3
  # exist on the associated table, and that join tables exist for one-to-many
6
4
  # relationships. Take the following class for example:
@@ -43,33 +41,53 @@ begin
43
41
  # end
44
42
  #
45
43
  module ActiveRecord
46
- class Base #:nodoc:
47
44
 
48
- def self._t_find(id)
45
+ def self.setup(model)
46
+ require 'active_record'
47
+ if model.ancestors.include?(::ActiveRecord::Base)
48
+ model.send :include, ActiveRecord::InstanceMethods
49
+ model.extend ActiveRecord::ClassMethods
50
+ end
51
+ rescue LoadError
52
+ # ActiveRecord not available
53
+ end
54
+
55
+ module ClassMethods #:nodoc:
56
+ def _t_find(id)
49
57
  find_by_id(id)
50
58
  end
51
59
 
52
- def self._t_find_bulk(ids)
60
+ def _t_find_bulk(ids)
53
61
  return [] if ids.nil? || ids.empty?
54
62
  find(:all, :conditions => ["id in (?)", ids])
55
63
  end
56
64
 
57
- def self._t_find_first_by_associate(property, id)
65
+ def _t_find_first_by_associate(property, id)
58
66
  find(:first, :conditions => ["#{property} = ?", id.to_s])
59
67
  end
60
68
 
61
- def self._t_find_all_by_associate(property, id)
69
+ def _t_find_all_by_associate(property, id)
62
70
  find(:all, :conditions => ["#{property} = ?", id.to_s])
63
71
  end
64
72
 
65
- def self._t_initialize_has_many_association(association)
73
+ def _t_initialize_has_many_association(association)
66
74
  after_save { |record| record.class._t_save_associates(record, association) }
67
75
  end
68
76
 
69
- def self._t_initialize_belongs_to_association(association)
77
+ def _t_initialize_belongs_to_association(association)
70
78
  before_save { |record| record.class._t_stringify_belongs_to_value(record, association) }
71
79
  end
72
80
 
81
+ def _t_delete(ids, run_callbacks=true)
82
+ if run_callbacks
83
+ destroy_all(["id in (?)", ids])
84
+ else
85
+ delete_all(["id in (?)", ids])
86
+ end
87
+ end
88
+ end
89
+
90
+ module InstanceMethods #:nodoc:
73
91
  def _t_reload
74
92
  reload
75
93
  end
@@ -88,12 +106,11 @@ begin
88
106
  end
89
107
 
90
108
  def _t_get_associate_ids(association)
109
+ return [] if self.id.nil?
91
110
  rows = self.connection.execute("select #{association.association_foreign_key} from #{association.join_table} where #{association.association_key} = #{self.id}")
92
111
  ids = []; rows.each { |r| ids << r[0] }; ids
93
112
  end
94
-
95
113
  end
114
+
96
115
  end
97
- rescue LoadError
98
- # ActiveRecord not available
99
116
  end