tenacity 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/.gitignore +6 -0
  2. data/EXTEND.rdoc +79 -0
  3. data/Gemfile +4 -0
  4. data/Gemfile.lock +70 -0
  5. data/LICENSE.txt +20 -0
  6. data/README.rdoc +103 -0
  7. data/Rakefile +96 -0
  8. data/lib/tenacity/associations/belongs_to.rb +28 -0
  9. data/lib/tenacity/associations/has_many.rb +86 -0
  10. data/lib/tenacity/associations/has_one.rb +40 -0
  11. data/lib/tenacity/class_methods.rb +226 -0
  12. data/lib/tenacity/instance_methods.rb +31 -0
  13. data/lib/tenacity/orm_ext/activerecord.rb +116 -0
  14. data/lib/tenacity/orm_ext/couchrest/couchrest_extended_document.rb +45 -0
  15. data/lib/tenacity/orm_ext/couchrest/couchrest_model.rb +46 -0
  16. data/lib/tenacity/orm_ext/couchrest/tenacity_class_methods.rb +52 -0
  17. data/lib/tenacity/orm_ext/couchrest/tenacity_instance_methods.rb +21 -0
  18. data/lib/tenacity/orm_ext/mongo_mapper.rb +107 -0
  19. data/lib/tenacity/version.rb +3 -0
  20. data/lib/tenacity.rb +27 -0
  21. data/tenacity.gemspec +34 -0
  22. data/test/associations/belongs_to_test.rb +119 -0
  23. data/test/associations/has_many_test.rb +184 -0
  24. data/test/associations/has_one_test.rb +77 -0
  25. data/test/core/classmethods_test.rb +53 -0
  26. data/test/fixtures/active_record_car.rb +6 -0
  27. data/test/fixtures/active_record_climate_control_unit.rb +5 -0
  28. data/test/fixtures/active_record_nuts.rb +5 -0
  29. data/test/fixtures/couch_rest_radio.rb +10 -0
  30. data/test/fixtures/mongo_mapper_button.rb +6 -0
  31. data/test/fixtures/mongo_mapper_dashboard.rb +10 -0
  32. data/test/fixtures/mongo_mapper_wheel.rb +9 -0
  33. data/test/helpers/active_record_test_helper.rb +7 -0
  34. data/test/helpers/couch_rest_test_helper.rb +8 -0
  35. data/test/helpers/mongo_mapper_test_helper.rb +3 -0
  36. data/test/orm_ext/activerecord_test.rb +85 -0
  37. data/test/orm_ext/couchrest_test.rb +95 -0
  38. data/test/orm_ext/mongo_mapper_test.rb +93 -0
  39. data/test/test_helper.rb +47 -0
  40. metadata +253 -0
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ *.swp
2
+ *.swo
3
+ .bundle
4
+ rdoc/
5
+ coverage/
6
+ pkg/
data/EXTEND.rdoc ADDED
@@ -0,0 +1,79 @@
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.
6
+
7
+
8
+ == A note about IDs
9
+
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.
14
+
15
+
16
+ == Class Methods
17
+
18
+ _t_find(id)
19
+
20
+ Find an object by its id, and return it. If the object cannot be found,
21
+ return nil.
22
+
23
+ _t_find_bulk(ids=[])
24
+
25
+ Find many objects by the specified ids, and return them in an array.
26
+ If no objects could be found, return an empty array.
27
+
28
+ _t_find_first_by_associate(property, id)
29
+
30
+ Find the first object by the specified property name, with the specified id,
31
+ and return it. If no object could be found, return nil.
32
+
33
+ _t_find_all_by_associate(property, id)
34
+
35
+ Find all objects by the specified property name, with the specified id, and
36
+ return them in an array. If no objects could be found, return an empty array.
37
+
38
+ _t_initialize_has_many_association(association_id)
39
+
40
+ Perform any ORM specific initialization necessary to support a has many
41
+ association. This could include defining properties, or callback methods,
42
+ on the object. This method is optional, and does not need to be defined.
43
+
44
+ _t_initialize_belongs_to_association(association_id)
45
+
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.
49
+
50
+ _t_initialize_has_one_association(association_id)
51
+
52
+ Perform any ORM specific initialization necessary to support a has one
53
+ association. This could include defining properties, or callback methods,
54
+ on the object. This method is optional, and does not need to be defined.
55
+
56
+
57
+ == Instance Methods
58
+
59
+ _t_reload
60
+
61
+ Reload the object from the database, overwriting the objects properties with
62
+ the data fetched from the database. Return nothing.
63
+
64
+ _t_associate_many(association_id, associate_ids)
65
+
66
+ Create has_many associations between this object and the objects with ids
67
+ 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.
69
+
70
+ _t_get_associate_ids(association_id)
71
+
72
+ Get the ids of the objects associated with this object through the specified
73
+ association, and return them in an array.
74
+
75
+ _t_clear_associates(association_id)
76
+
77
+ Destroy the association between this object and its current associates through
78
+ the specified association. Return nothing.
79
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+ source :gemcutter
3
+
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,70 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ tenacity (0.1.0)
5
+ activesupport (>= 2.3)
6
+
7
+ GEM
8
+ remote: http://rubygems.org/
9
+ remote: http://rubygems.org/
10
+ specs:
11
+ activemodel (3.0.3)
12
+ activesupport (= 3.0.3)
13
+ builder (~> 2.1.2)
14
+ i18n (~> 0.4)
15
+ activerecord (3.0.3)
16
+ activemodel (= 3.0.3)
17
+ activesupport (= 3.0.3)
18
+ arel (~> 2.0.2)
19
+ tzinfo (~> 0.3.23)
20
+ activesupport (3.0.3)
21
+ arel (2.0.6)
22
+ bson (1.1.5)
23
+ bson_ext (1.1.5)
24
+ builder (2.1.2)
25
+ couchrest (1.0.1)
26
+ json (>= 1.4.6)
27
+ mime-types (>= 1.15)
28
+ rest-client (>= 1.5.1)
29
+ couchrest_model (1.0.0.beta7)
30
+ activemodel (>= 3.0.0.beta4)
31
+ activesupport (>= 2.3.5)
32
+ couchrest (>= 1.0.0.beta)
33
+ mime-types (>= 1.15)
34
+ i18n (0.5.0)
35
+ jnunemaker-validatable (1.8.4)
36
+ activesupport (>= 2.3.4)
37
+ json (1.4.6)
38
+ mime-types (1.16)
39
+ mongo (1.1.5)
40
+ bson (>= 1.1.5)
41
+ mongo_mapper (0.8.6)
42
+ activesupport (>= 2.3.4)
43
+ jnunemaker-validatable (~> 1.8.4)
44
+ plucky (~> 0.3.6)
45
+ mysql (2.8.1)
46
+ plucky (0.3.6)
47
+ mongo (~> 1.1)
48
+ rake (0.8.7)
49
+ rcov (0.9.9)
50
+ rest-client (1.6.1)
51
+ mime-types (>= 1.16)
52
+ shoulda (2.11.3)
53
+ tzinfo (0.3.23)
54
+
55
+ PLATFORMS
56
+ ruby
57
+
58
+ DEPENDENCIES
59
+ activerecord (~> 3.0.0)
60
+ activesupport (>= 2.3)
61
+ bson_ext (~> 1.1.3)
62
+ bundler (~> 1.0.0)
63
+ couchrest (~> 1.0.0)
64
+ couchrest_model
65
+ mongo_mapper (~> 0.8.6)
66
+ mysql (~> 2.8.1)
67
+ rake (~> 0.8.7)
68
+ rcov (~> 0.9.9)
69
+ shoulda (~> 2.11.3)
70
+ tenacity!
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 John Wood
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,103 @@
1
+ = Tenacity
2
+
3
+ An ORM independent way of specifying simple relationships between models
4
+ backed by different databases.
5
+
6
+ It is sometimes necessary, or advantageous, to use more than one database in a
7
+ given application. However, most ORMs do not support inter-database
8
+ relationships. While supporting such relationships isn't difficult, it can
9
+ add quite a bit of boilerplate code to your project.
10
+
11
+ Tenacity aims to address this by providing an ORM independent way of specifying
12
+ simple relationships between models backed by different databases.
13
+
14
+ Tenacity is heavily based on ActiveRecord's associations, and aims to behave in
15
+ much the same way, supporting many of the same options.
16
+
17
+
18
+ == Example
19
+
20
+ class User
21
+ include MongoMapper::Document
22
+ include Tenacity
23
+
24
+ t_has_many :entries
25
+ end
26
+
27
+ class Entry < ActiveRecord::Base
28
+ include Tenacity
29
+
30
+ t_belongs_to :user
31
+ end
32
+
33
+
34
+ # Fetch related object from the respective database
35
+ entry.user
36
+
37
+ # Set the related object
38
+ entry.user = some_user
39
+ entry.save
40
+
41
+ # Fetch related objects from the respective database
42
+ user.entries
43
+
44
+ # Add a related object to the collection
45
+ user.entries << some_entry
46
+ user.save
47
+
48
+
49
+ == Additional Usage Details
50
+
51
+ * The directories that contain your model classes must be in your load path in order for Tenacity to find them.
52
+
53
+
54
+ == Supported ORMs
55
+
56
+ * ActiveRecord
57
+ * MongoMapper
58
+ * CouchRest (CouchModel and ExtendedDocument)
59
+
60
+ See EXTEND.rdoc for information on extendeding Tenacity to work with other ORMs.
61
+
62
+
63
+ == Documentation
64
+ * http://rdoc.info/github/jwood/tenacity/master/frames
65
+
66
+
67
+ == Contributing to Tenacity
68
+
69
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
70
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
71
+ * Fork the project
72
+ * Start a feature/bugfix branch
73
+ * Commit and push until you are happy with your contribution
74
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
75
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
76
+
77
+
78
+ == Development
79
+
80
+ * MySQL, MongoDB, and CouchDB must be installed and configured.
81
+ * Install the dependencies
82
+
83
+ bundler install
84
+
85
+ * Create the test databases
86
+
87
+ rake db:create
88
+
89
+ * Setup the test databases
90
+
91
+ rake db:test:prepare
92
+
93
+ * Run the tests
94
+
95
+ rake test
96
+
97
+ * Code away!
98
+
99
+
100
+ == Copyright
101
+
102
+ Copyright (c) 2010 John Wood. See LICENSE.txt for further details.
103
+
data/Rakefile ADDED
@@ -0,0 +1,96 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ begin
7
+ Bundler.setup(:default, :development)
8
+ rescue Bundler::BundlerError => e
9
+ $stderr.puts e.message
10
+ $stderr.puts "Run `bundle install` to install missing gems"
11
+ exit e.status_code
12
+ end
13
+ require 'rake'
14
+
15
+ require 'rake/testtask'
16
+ Rake::TestTask.new(:test) do |test|
17
+ test.libs << 'lib' << 'test' << 'test/fixtures'
18
+ test.pattern = 'test/**/*_test.rb'
19
+ test.verbose = true
20
+ end
21
+
22
+ require 'rcov/rcovtask'
23
+ Rcov::RcovTask.new do |test|
24
+ test.libs << 'test' << 'test/fixtures'
25
+ test.pattern = 'test/**/*_test.rb'
26
+ test.verbose = true
27
+ test.rcov_opts << '--exclude "gems/*"'
28
+ end
29
+
30
+ task :default => :test
31
+
32
+ require 'rake/rdoctask'
33
+ Rake::RDocTask.new do |rdoc|
34
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
35
+
36
+ rdoc.rdoc_dir = 'rdoc'
37
+ rdoc.title = "tenacity #{version}"
38
+ rdoc.rdoc_files.include('README*')
39
+ rdoc.rdoc_files.include('EXTEND*')
40
+ rdoc.rdoc_files.include('lib/**/*.rb')
41
+ end
42
+
43
+ desc 'Delete rcov, rdoc, and other generated files'
44
+ task :clobber => [:clobber_rcov, :clobber_rdoc]
45
+
46
+ begin
47
+ require 'test/helpers/active_record_test_helper'
48
+ namespace :db do
49
+ desc "Create the test databases"
50
+ task :create do
51
+ system "mysqladmin -u root create tenacity_test"
52
+ end
53
+
54
+ desc "Drop the test databases"
55
+ task :drop do
56
+ system "mysqladmin -u root drop -f tenacity_test"
57
+ end
58
+
59
+ desc "Reset the test databases"
60
+ task :reset => [:drop, :create] do
61
+ Rake::Task['db:test:prepare'].invoke
62
+ end
63
+
64
+ namespace :test do
65
+ desc "Setup the test databases"
66
+ task :prepare do
67
+ ActiveRecord::Schema.define :version => 0 do
68
+
69
+ create_table :active_record_cars, :force => true do |t|
70
+ end
71
+
72
+ create_table :active_record_climate_control_units, :force => true do |t|
73
+ t.string :mongo_mapper_dashboard_id
74
+ end
75
+
76
+ create_table :active_record_cars_mongo_mapper_wheels, :force => true do |t|
77
+ t.integer :active_record_car_id
78
+ t.string :mongo_mapper_wheel_id
79
+ end
80
+
81
+ create_table :active_record_nuts, :force => true do |t|
82
+ t.string :mongo_mapper_wheel_id
83
+ end
84
+
85
+ create_table :active_record_nuts_mongo_mapper_wheels, :force => true do |t|
86
+ t.integer :active_record_nut_id
87
+ t.string :mongo_mapper_wheel_id
88
+ end
89
+
90
+ end
91
+ end
92
+ end
93
+ end
94
+ rescue LoadError
95
+ # No ActiveRecord
96
+ end
@@ -0,0 +1,28 @@
1
+ module Tenacity
2
+ module BelongsTo #:nodoc:
3
+
4
+ private
5
+
6
+ def belongs_to_associate(association_id)
7
+ associate_id = self.send("#{association_id}_id")
8
+ clazz = associate_class(association_id)
9
+ clazz._t_find(associate_id)
10
+ end
11
+
12
+ def set_belongs_to_associate(association_id, associate)
13
+ self.send "#{association_id}_id=", associate.id.to_s
14
+ end
15
+
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)
19
+ end
20
+
21
+ def _t_stringify_belongs_to_value(record, association_id)
22
+ record.send "#{association_id}_id=", record.send("#{association_id}_id").to_s
23
+ end
24
+ end
25
+
26
+ end
27
+ end
28
+
@@ -0,0 +1,86 @@
1
+ module Tenacity
2
+ module HasMany #:nodoc:
3
+
4
+ private
5
+
6
+ def has_many_associates(association_id)
7
+ ids = _t_get_associate_ids(association_id)
8
+ clazz = associate_class(association_id)
9
+ clazz._t_find_bulk(ids)
10
+ end
11
+
12
+ def has_many_associate_ids(association_id)
13
+ _t_get_associate_ids(association_id)
14
+ end
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)
19
+ end
20
+
21
+ def save_without_callback
22
+ @perform_save_associates_callback = false
23
+ save
24
+ ensure
25
+ @perform_save_associates_callback = true
26
+ end
27
+
28
+ def has_many_property_name(association_id)
29
+ self.class.has_many_property_name(association_id)
30
+ end
31
+
32
+ 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)
35
+
36
+ attr_accessor :perform_save_associates_callback
37
+ end
38
+
39
+ def _t_save_associates(record, association_id)
40
+ return if record.perform_save_associates_callback == false
41
+
42
+ _t_clear_old_associations(record, association_id)
43
+
44
+ associates = (record.instance_variable_get "@_t_#{association_id.to_s}") || []
45
+ associates.each do |associate|
46
+ associate.send("#{property_name_for_record(record)}=", record.id.to_s)
47
+ save_associate(associate)
48
+ end
49
+
50
+ unless associates.blank?
51
+ associate_ids = associates.map { |associate| associate.id.to_s }
52
+ record._t_associate_many(association_id, associate_ids)
53
+ save_associate(record)
54
+ end
55
+ end
56
+
57
+ def _t_clear_old_associations(record, association_id)
58
+ clazz = associate_class(association_id)
59
+ property_name = property_name_for_record(record)
60
+
61
+ old_associates = clazz._t_find_all_by_associate(property_name, record.id.to_s)
62
+ old_associates.each do |old_associate|
63
+ old_associate.send("#{property_name}=", nil)
64
+ save_associate(old_associate)
65
+ end
66
+
67
+ record._t_clear_associates(association_id)
68
+ save_associate(record)
69
+ end
70
+
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
+ def save_associate(associate)
80
+ associate.respond_to?(:_t_save_without_callback) ? associate._t_save_without_callback : associate.save
81
+ end
82
+ end
83
+
84
+ end
85
+ end
86
+
@@ -0,0 +1,40 @@
1
+ module Tenacity
2
+ module HasOne #:nodoc:
3
+
4
+ private
5
+
6
+ def has_one_associate(association_id)
7
+ clazz = associate_class(association_id)
8
+ clazz._t_find_first_by_associate(property_name, self.id.to_s)
9
+ end
10
+
11
+ def set_has_one_associate(association_id, associate)
12
+ associate.send "#{property_name}=", self.id.to_s
13
+ associate.save
14
+ end
15
+
16
+ def property_name
17
+ "#{ActiveSupport::Inflector.underscore(self.class.to_s)}_id"
18
+ end
19
+
20
+ module ClassMethods #:nodoc:
21
+ def initialize_has_one_association(association_id)
22
+ begin
23
+ require association_id.to_s
24
+ rescue Exception => e
25
+ puts "ERROR: #{association_id.to_s} does not appear to be in the load path. Please make sure all model files are in the load path."
26
+ raise
27
+ end
28
+
29
+ clazz = associate_class(association_id)
30
+ clazz._t_initialize_has_one_association(ActiveSupport::Inflector.underscore(self.to_s)) if clazz.respond_to?(:_t_initialize_has_one_association)
31
+ end
32
+
33
+ def _t_stringify_has_one_value(record, association_id)
34
+ record.send "#{association_id}_id=", record.send("#{association_id}_id").to_s
35
+ end
36
+ end
37
+
38
+ end
39
+ end
40
+