tenacity 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
data/.gitignore CHANGED
@@ -4,3 +4,4 @@
4
4
  rdoc/
5
5
  coverage/
6
6
  pkg/
7
+ Gemfile.lock
@@ -1,16 +1,23 @@
1
- For Tenacity to interact with an ORM, the ORM needs to be extended to support
2
- the methods listed below. Beyond that, no additional configuration or code is
3
- necessary. Tenacity communicates with the ORM using these methods only, so
4
- as long as they have been implemented and are available on the model object,
5
- Tenacity will be able to manage the object's relationships.
1
+ For Tenacity to interact with a database client, the client needs to be extended
2
+ to support the methods listed below. Beyond that, no additional configuration
3
+ or code is necessary. Tenacity communicates with the client using these methods
4
+ only, so as long as they have been implemented and are available on the model
5
+ object, Tenacity will be able to manage the object's relationships.
6
6
 
7
7
 
8
8
  == A note about IDs
9
9
 
10
10
  An ID can be an integer, a string, or an object, depending on the database
11
- and ORM you are using. To be as compatible as possible, Tenacity treats
12
- all IDs as strings. So, all ORM extensions should accept strings for IDs
13
- as input parameters, and return strings for IDs.
11
+ and database client you are using. To be as compatible as possible, Tenacity
12
+ treats all IDs as strings. So, all database client extensions should accept
13
+ strings for IDs as input parameters, and return strings for IDs.
14
+
15
+
16
+ == The Association Class
17
+
18
+ A few of the methods take an association as a parameter. This association
19
+ is an instance of the Tenacity::Association class, which can be found at
20
+ lib/tenacity/association.rb.
14
21
 
15
22
 
16
23
  == Class Methods
@@ -35,21 +42,22 @@ and return it. If no object could be found, return nil.
35
42
  Find all objects by the specified property name, with the specified id, and
36
43
  return them in an array. If no objects could be found, return an empty array.
37
44
 
38
- _t_initialize_has_many_association(association_id)
45
+ _t_initialize_has_many_association(association)
39
46
 
40
- Perform any ORM specific initialization necessary to support a has many
41
- association. This could include defining properties, or callback methods,
47
+ Perform any database client specific initialization necessary to support a has
48
+ many association. This could include defining properties, or callback methods,
42
49
  on the object. This method is optional, and does not need to be defined.
43
50
 
44
- _t_initialize_belongs_to_association(association_id)
51
+ _t_initialize_belongs_to_association(association)
45
52
 
46
- Perform any ORM specific initialization necessary to support a belongs to
47
- association. This could include defining properties, or callback methods,
48
- on the object. This method is optional, and does not need to be defined.
53
+ Perform any database client specific initialization necessary to support a
54
+ belongs to association. This could include defining properties, or callback
55
+ methods, on the object. This method is optional, and does not need to be
56
+ defined.
49
57
 
50
- _t_initialize_has_one_association(association_id)
58
+ _t_initialize_has_one_association(association)
51
59
 
52
- Perform any ORM specific initialization necessary to support a has one
60
+ Perform any database client specific initialization necessary to support a has one
53
61
  association. This could include defining properties, or callback methods,
54
62
  on the object. This method is optional, and does not need to be defined.
55
63
 
@@ -61,18 +69,20 @@ on the object. This method is optional, and does not need to be defined.
61
69
  Reload the object from the database, overwriting the objects properties with
62
70
  the data fetched from the database. Return nothing.
63
71
 
64
- _t_associate_many(association_id, associate_ids)
72
+ _t_associate_many(association, associate_ids)
65
73
 
66
74
  Create has_many associations between this object and the objects with ids
67
75
  specified in the array of associate_ids. This method could involve writing
68
- the associate_ids into a join table, or into an object's hash.
76
+ the associate_ids into a join table, or into one of the object's properties.
77
+ Return nothing.
69
78
 
70
- _t_get_associate_ids(association_id)
79
+ _t_get_associate_ids(association)
71
80
 
72
81
  Get the ids of the objects associated with this object through the specified
73
- association, and return them in an array.
82
+ association, and return them in an array. Return an empty array if there
83
+ are no associated objects.
74
84
 
75
- _t_clear_associates(association_id)
85
+ _t_clear_associates(association)
76
86
 
77
87
  Destroy the association between this object and its current associates through
78
88
  the specified association. Return nothing.
@@ -1,4 +1,4 @@
1
- Copyright (c) 2010 John Wood
1
+ Copyright (c) 2011 John Wood
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
@@ -1,15 +1,16 @@
1
1
  = Tenacity
2
2
 
3
- An ORM independent way of specifying simple relationships between models
3
+ A database client independent way of managing relationships between models
4
4
  backed by different databases.
5
5
 
6
6
  It is sometimes necessary, or advantageous, to use more than one database in a
7
- given application (polyglot persistence). However, most ORMs do not support
8
- inter-database relationships. While supporting such relationships isn't difficult,
9
- it can add quite a bit of boilerplate code to your project.
7
+ given application (polyglot persistence). However, most database clients do not
8
+ support inter-database relationships. So, writing code that manages relationships
9
+ between objects backed by different databases hasn’t been nearly as easy as
10
+ writing code to manage relationships between objects in the same database.
10
11
 
11
- Tenacity aims to address this by providing an ORM independent way of specifying
12
- simple relationships between models backed by different databases.
12
+ Tenacity aims to address this by providing a database client independent way
13
+ of managing relationships between models backed by different databases.
13
14
 
14
15
  Tenacity is heavily based on ActiveRecord's associations, and aims to behave in
15
16
  much the same way, supporting many of the same options.
@@ -79,17 +80,18 @@ much the same way, supporting many of the same options.
79
80
  * The directories that contain your model classes must be in your load path in order for Tenacity to find them.
80
81
 
81
82
 
82
- == Supported ORMs
83
+ == Supported Database Clients
83
84
 
84
85
  * ActiveRecord
85
86
  * MongoMapper
86
87
  * CouchRest (CouchModel and ExtendedDocument)
87
88
 
88
- See EXTEND.rdoc for information on extendeding Tenacity to work with other ORMs.
89
+ See EXTEND.rdoc for information on extending Tenacity to work with other database clients.
89
90
 
90
91
 
91
92
  == Documentation
92
- * http://rdoc.info/github/jwood/tenacity/master/frames
93
+ * http://rdoc.info/github/jwood/tenacity/master/Tenacity/ClassMethods - Documentation on the associations supported, and their respective options
94
+ * http://rdoc.info/github/jwood/tenacity/master/frames - Full API documentation
93
95
 
94
96
 
95
97
  == Contributing to Tenacity
@@ -105,14 +107,10 @@ See EXTEND.rdoc for information on extendeding Tenacity to work with other ORMs.
105
107
 
106
108
  == Development
107
109
 
108
- * MySQL, MongoDB, and CouchDB must be installed and configured.
110
+ * SQLite, MongoDB, and CouchDB must be installed and configured.
109
111
  * Install the dependencies
110
112
 
111
- bundler install
112
-
113
- * Setup the test databases
114
-
115
- rake test:prepare
113
+ bundle install
116
114
 
117
115
  * Run the tests
118
116
 
@@ -123,5 +121,5 @@ See EXTEND.rdoc for information on extendeding Tenacity to work with other ORMs.
123
121
 
124
122
  == Copyright
125
123
 
126
- Copyright (c) 2010 John Wood. See LICENSE.txt for further details.
124
+ Copyright (c) 2011 John Wood. See LICENSE.txt for further details.
127
125
 
data/Rakefile CHANGED
@@ -43,40 +43,3 @@ end
43
43
  desc 'Delete rcov, rdoc, and other generated files'
44
44
  task :clobber => [:clobber_rcov, :clobber_rdoc]
45
45
 
46
- begin
47
- require 'test/helpers/active_record_test_helper'
48
- namespace :test do
49
- desc "Setup the test databases"
50
- task :prepare do
51
- system "mysqladmin -u root drop -f tenacity_test"
52
- system "mysqladmin -u root create tenacity_test"
53
-
54
- ActiveRecord::Schema.define :version => 0 do
55
-
56
- create_table :active_record_cars, :force => true do |t|
57
- end
58
-
59
- create_table :active_record_climate_control_units, :force => true do |t|
60
- t.string :mongo_mapper_dashboard_id
61
- end
62
-
63
- create_table :active_record_cars_mongo_mapper_wheels, :force => true do |t|
64
- t.integer :active_record_car_id
65
- t.string :mongo_mapper_wheel_id
66
- end
67
-
68
- create_table :active_record_nuts, :force => true do |t|
69
- t.string :mongo_mapper_wheel_id
70
- end
71
-
72
- create_table :active_record_nuts_mongo_mapper_wheels, :force => true do |t|
73
- t.integer :active_record_nut_id
74
- t.string :mongo_mapper_wheel_id
75
- end
76
-
77
- end
78
- end
79
- end
80
- rescue LoadError
81
- # No ActiveRecord
82
- end
@@ -1,3 +1,24 @@
1
+ == 0.2.0
2
+
3
+ * Major enhancements
4
+
5
+ * Added options to override assumptions on names of classes, foreign keys,
6
+ tables, and columns.
7
+
8
+ * Minor enhancements
9
+
10
+ * Added support for the :class_name option to all associations
11
+ * Added support for the :foreign_key option to all associations
12
+ * Added support for the :foreign_keys_property option to the t_has_many association
13
+ * Added support for the :join_table option to the t_has_many association
14
+ * Added support for the :association_foreign_key option to the t_has_many association
15
+ * Added support for the :association_key option to the t_has_many association
16
+
17
+ * Bug fixes
18
+
19
+ * t_has_one association was being initialized on the wrong class in the association
20
+ * Fixed bug that causing t_has_many associations not to work with SQLite
21
+
1
22
  == 0.1.1
2
23
 
3
24
  * Bug fixes
@@ -1,5 +1,6 @@
1
1
  require File.join('active_support', 'inflector')
2
2
 
3
+ require File.join(File.dirname(__FILE__), 'tenacity', 'association')
3
4
  require File.join(File.dirname(__FILE__), 'tenacity', 'class_methods')
4
5
  require File.join(File.dirname(__FILE__), 'tenacity', 'instance_methods')
5
6
  require File.join(File.dirname(__FILE__), 'tenacity', 'associations', 'belongs_to')
@@ -20,7 +21,7 @@ module Tenacity #:nodoc:
20
21
  include HasOne
21
22
 
22
23
  def self.included(model)
23
- raise "Tenacity does not support the ORM used by #{model}" unless model.respond_to?(:_t_find)
24
+ raise "Tenacity does not support the database client used by #{model}" unless model.respond_to?(:_t_find)
24
25
  model.extend(ClassMethods)
25
26
  end
26
27
  end
@@ -0,0 +1,82 @@
1
+ module Tenacity
2
+ class Association
3
+ # Type type of the association (<tt>:t_has_one</tt>, <tt>:t_has_many</tt>, or <tt>:t_belongs_to</tt>)
4
+ attr_reader :type
5
+
6
+ # The name of the association
7
+ attr_reader :name
8
+
9
+ # The class defining the association
10
+ attr_reader :source
11
+
12
+ # The name of the associated class
13
+ attr_reader :class_name
14
+
15
+ def initialize(type, name, source, options={})
16
+ @type = type
17
+ @name = name
18
+ @source = source
19
+
20
+ if options[:class_name]
21
+ @class_name = options[:class_name]
22
+ else
23
+ @class_name = name.to_s.singularize.camelcase
24
+ end
25
+
26
+ @foreign_key = options[:foreign_key]
27
+ @foreign_keys_property = options[:foreign_keys_property]
28
+ @join_table = options[:join_table]
29
+ @association_key = options[:association_key]
30
+ @association_foreign_key = options[:association_foreign_key]
31
+
32
+ if @foreign_keys_property
33
+ if @foreign_keys_property.to_s == ActiveSupport::Inflector.singularize(name) + "_ids"
34
+ raise "#{ActiveSupport::Inflector.singularize(name) + "_ids"} is an invalid foreign keys property name"
35
+ end
36
+ end
37
+ end
38
+
39
+ # Get the associated class
40
+ def associate_class
41
+ @clazz ||= Kernel.const_get(@class_name)
42
+ end
43
+
44
+ # Get the foreign key used by this association. <tt>t_has_one</tt> and
45
+ # <tt>t_has_many</tt> associations need the class of the associated object
46
+ # to be specified in order to properly determine the name of the foreign key.
47
+ def foreign_key(clazz=nil)
48
+ @foreign_key || begin
49
+ if @type == :t_belongs_to
50
+ @class_name.underscore + "_id"
51
+ elsif @type == :t_has_one || @type == :t_has_many
52
+ raise "The class of the associate must be provided in order to determine the name of the foreign key" if clazz.nil?
53
+ "#{ActiveSupport::Inflector.underscore(clazz)}_id"
54
+ end
55
+ end
56
+ end
57
+
58
+ # Get the property name used to store the foreign key
59
+ def foreign_keys_property
60
+ @foreign_keys_property || "t_" + ActiveSupport::Inflector.singularize(name) + "_ids"
61
+ end
62
+
63
+ # Get the name of the join table used by this association
64
+ 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
68
+ end
69
+
70
+ # Get the name of the column in the join table that represents this object
71
+ def association_key
72
+ if @association_key || @source.respond_to?(:table_name)
73
+ @association_key || @source.table_name.singularize + '_id'
74
+ end
75
+ end
76
+
77
+ # Get the name of the column in the join table that represents the associated object
78
+ def association_foreign_key
79
+ @association_foreign_key || name.to_s.singularize + '_id'
80
+ end
81
+ end
82
+ end
@@ -3,23 +3,23 @@ module Tenacity
3
3
 
4
4
  private
5
5
 
6
- def belongs_to_associate(association_id)
7
- associate_id = self.send("#{association_id}_id")
8
- clazz = associate_class(association_id)
6
+ def belongs_to_associate(association)
7
+ associate_id = self.send(association.foreign_key)
8
+ clazz = association.associate_class
9
9
  clazz._t_find(associate_id)
10
10
  end
11
11
 
12
- def set_belongs_to_associate(association_id, associate)
13
- self.send "#{association_id}_id=", associate.id.to_s
12
+ def set_belongs_to_associate(association, associate)
13
+ self.send "#{association.foreign_key}=", associate.id.to_s
14
14
  end
15
15
 
16
16
  module ClassMethods #:nodoc:
17
- def initialize_belongs_to_association(association_id)
18
- _t_initialize_belongs_to_association(association_id) if self.respond_to?(:_t_initialize_belongs_to_association)
17
+ def initialize_belongs_to_association(association)
18
+ _t_initialize_belongs_to_association(association) if self.respond_to?(:_t_initialize_belongs_to_association)
19
19
  end
20
20
 
21
- def _t_stringify_belongs_to_value(record, association_id)
22
- record.send "#{association_id}_id=", record.send("#{association_id}_id").to_s
21
+ def _t_stringify_belongs_to_value(record, association)
22
+ record.send "#{association.foreign_key}=", record.send(association.foreign_key).to_s
23
23
  end
24
24
  end
25
25
 
@@ -3,19 +3,19 @@ module Tenacity
3
3
 
4
4
  private
5
5
 
6
- def has_many_associates(association_id)
7
- ids = _t_get_associate_ids(association_id)
8
- clazz = associate_class(association_id)
6
+ def has_many_associates(association)
7
+ ids = _t_get_associate_ids(association)
8
+ clazz = association.associate_class
9
9
  clazz._t_find_bulk(ids)
10
10
  end
11
11
 
12
- def has_many_associate_ids(association_id)
13
- _t_get_associate_ids(association_id)
12
+ def has_many_associate_ids(association)
13
+ _t_get_associate_ids(association)
14
14
  end
15
15
 
16
- def set_has_many_associate_ids(association_id, associate_ids)
17
- clazz = associate_class(association_id)
18
- instance_variable_set ivar_name(association_id), clazz._t_find_bulk(associate_ids)
16
+ def set_has_many_associate_ids(association, associate_ids)
17
+ clazz = association.associate_class
18
+ instance_variable_set _t_ivar_name(association), clazz._t_find_bulk(associate_ids)
19
19
  end
20
20
 
21
21
  def save_without_callback
@@ -25,38 +25,34 @@ module Tenacity
25
25
  @perform_save_associates_callback = true
26
26
  end
27
27
 
28
- def has_many_property_name(association_id)
29
- self.class.has_many_property_name(association_id)
30
- end
31
-
32
28
  module ClassMethods #:nodoc:
33
- def initialize_has_many_association(association_id)
34
- _t_initialize_has_many_association(association_id) if self.respond_to?(:_t_initialize_has_many_association)
29
+ def initialize_has_many_association(association)
30
+ _t_initialize_has_many_association(association) if self.respond_to?(:_t_initialize_has_many_association)
35
31
 
36
32
  attr_accessor :perform_save_associates_callback
37
33
  end
38
34
 
39
- def _t_save_associates(record, association_id)
35
+ def _t_save_associates(record, association)
40
36
  return if record.perform_save_associates_callback == false
41
37
 
42
- _t_clear_old_associations(record, association_id)
38
+ _t_clear_old_associations(record, association)
43
39
 
44
- associates = (record.instance_variable_get "@_t_#{association_id.to_s}") || []
40
+ associates = (record.instance_variable_get record._t_ivar_name(association)) || []
45
41
  associates.each do |associate|
46
- associate.send("#{property_name_for_record(record)}=", record.id.to_s)
42
+ associate.send("#{association.foreign_key(record.class)}=", record.id.to_s)
47
43
  save_associate(associate)
48
44
  end
49
45
 
50
46
  unless associates.blank?
51
47
  associate_ids = associates.map { |associate| associate.id.to_s }
52
- record._t_associate_many(association_id, associate_ids)
48
+ record._t_associate_many(association, associate_ids)
53
49
  save_associate(record)
54
50
  end
55
51
  end
56
52
 
57
- def _t_clear_old_associations(record, association_id)
58
- clazz = associate_class(association_id)
59
- property_name = property_name_for_record(record)
53
+ def _t_clear_old_associations(record, association)
54
+ clazz = association.associate_class
55
+ property_name = association.foreign_key(record.class)
60
56
 
61
57
  old_associates = clazz._t_find_all_by_associate(property_name, record.id.to_s)
62
58
  old_associates.each do |old_associate|
@@ -64,18 +60,10 @@ module Tenacity
64
60
  save_associate(old_associate)
65
61
  end
66
62
 
67
- record._t_clear_associates(association_id)
63
+ record._t_clear_associates(association)
68
64
  save_associate(record)
69
65
  end
70
66
 
71
- def property_name_for_record(record)
72
- "#{ActiveSupport::Inflector.underscore(record.class.to_s)}_id"
73
- end
74
-
75
- def has_many_property_name(association_id)
76
- "t_" + ActiveSupport::Inflector.singularize(association_id) + "_ids"
77
- end
78
-
79
67
  def save_associate(associate)
80
68
  associate.respond_to?(:_t_save_without_callback) ? associate._t_save_without_callback : associate.save
81
69
  end