forceps 0.5.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 (79) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/Rakefile +32 -0
  3. data/lib/forceps/acts_as_copyable_model.rb +245 -0
  4. data/lib/forceps/client.rb +126 -0
  5. data/lib/forceps/version.rb +3 -0
  6. data/lib/forceps.rb +30 -0
  7. data/lib/tasks/forceps_tasks.rake +4 -0
  8. data/test/callbacks_test.rb +21 -0
  9. data/test/clone_structures_test.rb +73 -0
  10. data/test/dummy/README.rdoc +28 -0
  11. data/test/dummy/Rakefile +6 -0
  12. data/test/dummy/app/assets/javascripts/application.js +13 -0
  13. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  14. data/test/dummy/app/controllers/application_controller.rb +5 -0
  15. data/test/dummy/app/helpers/application_helper.rb +2 -0
  16. data/test/dummy/app/models/address.rb +3 -0
  17. data/test/dummy/app/models/car.rb +5 -0
  18. data/test/dummy/app/models/invoice.rb +7 -0
  19. data/test/dummy/app/models/line_item.rb +4 -0
  20. data/test/dummy/app/models/product.rb +4 -0
  21. data/test/dummy/app/models/tag.rb +3 -0
  22. data/test/dummy/app/models/user.rb +4 -0
  23. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  24. data/test/dummy/bin/bundle +3 -0
  25. data/test/dummy/bin/rails +4 -0
  26. data/test/dummy/bin/rake +4 -0
  27. data/test/dummy/config/application.rb +23 -0
  28. data/test/dummy/config/boot.rb +5 -0
  29. data/test/dummy/config/database.yml +31 -0
  30. data/test/dummy/config/environment.rb +5 -0
  31. data/test/dummy/config/environments/development.rb +29 -0
  32. data/test/dummy/config/environments/production.rb +80 -0
  33. data/test/dummy/config/environments/test.rb +36 -0
  34. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  35. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  36. data/test/dummy/config/initializers/inflections.rb +16 -0
  37. data/test/dummy/config/initializers/mime_types.rb +5 -0
  38. data/test/dummy/config/initializers/secret_token.rb +12 -0
  39. data/test/dummy/config/initializers/session_store.rb +3 -0
  40. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  41. data/test/dummy/config/locales/en.yml +23 -0
  42. data/test/dummy/config/routes.rb +56 -0
  43. data/test/dummy/config.ru +4 -0
  44. data/test/dummy/db/development.sqlite3 +0 -0
  45. data/test/dummy/db/migrate/20140101112114_create_invoices.rb +9 -0
  46. data/test/dummy/db/migrate/20140102125046_add_number_to_invoices.rb +5 -0
  47. data/test/dummy/db/migrate/20140103172515_create_users.rb +8 -0
  48. data/test/dummy/db/migrate/20140103172643_add_user_id_to_invoices.rb +5 -0
  49. data/test/dummy/db/migrate/20140104145204_create_line_items.rb +11 -0
  50. data/test/dummy/db/migrate/20140104145227_create_products.rb +10 -0
  51. data/test/dummy/db/migrate/20140104150906_create_addresses.rb +12 -0
  52. data/test/dummy/db/migrate/20140114111137_create_products_tags_join_table.rb +8 -0
  53. data/test/dummy/db/migrate/20140114111219_create_tags.rb +7 -0
  54. data/test/dummy/db/migrate/20140117112556_add_type_to_products.rb +5 -0
  55. data/test/dummy/db/remote.sqlite3 +0 -0
  56. data/test/dummy/db/schema.rb +64 -0
  57. data/test/dummy/db/test.sqlite3 +0 -0
  58. data/test/dummy/log/RAILS_ENV=test.log +0 -0
  59. data/test/dummy/log/development.log +608 -0
  60. data/test/dummy/log/remote.log +182 -0
  61. data/test/dummy/log/target.log +30 -0
  62. data/test/dummy/log/test.log +132555 -0
  63. data/test/dummy/public/404.html +58 -0
  64. data/test/dummy/public/422.html +58 -0
  65. data/test/dummy/public/500.html +57 -0
  66. data/test/dummy/public/favicon.ico +0 -0
  67. data/test/exclude_records_test.rb +43 -0
  68. data/test/fixtures/invoices.yml +11 -0
  69. data/test/fixtures/users.yml +8 -0
  70. data/test/reuse_existing_records_test.rb +41 -0
  71. data/test/support/minitest_ext.rb +13 -0
  72. data/test/support/remote_address.rb +7 -0
  73. data/test/support/remote_invoice.rb +8 -0
  74. data/test/support/remote_line_item.rb +8 -0
  75. data/test/support/remote_product.rb +8 -0
  76. data/test/support/remote_tag.rb +7 -0
  77. data/test/support/remote_user.rb +8 -0
  78. data/test/test_helper.rb +37 -0
  79. metadata +296 -0
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2013 YOURNAME
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/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Forceps'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+ Bundler::GemHelper.install_tasks
21
+
22
+ require 'rake/testtask'
23
+
24
+ Rake::TestTask.new(:test) do |t|
25
+ t.libs << 'lib'
26
+ t.libs << 'test'
27
+ t.pattern = 'test/**/*_test.rb'
28
+ t.verbose = false
29
+ end
30
+
31
+
32
+ task default: :test
@@ -0,0 +1,245 @@
1
+ module Forceps
2
+ module ActsAsCopyableModel
3
+ extend ActiveSupport::Concern
4
+
5
+ def copy_to_local
6
+ without_record_timestamps do
7
+ DeepCopier.new(forceps_options).copy(self)
8
+ end
9
+ end
10
+
11
+ private
12
+
13
+ def without_record_timestamps
14
+ self.class.base_class.record_timestamps = false
15
+ yield
16
+ ensure
17
+ self.class.base_class.record_timestamps = true
18
+ end
19
+
20
+ def forceps_options
21
+ Forceps.client.options
22
+ end
23
+
24
+ class DeepCopier
25
+ attr_accessor :copied_remote_objects, :options, :level, :reused_local_objects
26
+
27
+ def initialize(options)
28
+ @copied_remote_objects = {}
29
+ @reused_local_objects = Set.new
30
+ @options = options
31
+ @level = 0
32
+ end
33
+
34
+ def copy(remote_object)
35
+ cached_local_copy(remote_object) || perform_copy(remote_object)
36
+ end
37
+
38
+ private
39
+
40
+ def cached_local_copy(remote_object)
41
+ cached_object = copied_remote_objects[remote_object]
42
+ debug "#{as_trace(remote_object)} from cache..." if cached_object
43
+ cached_object
44
+ end
45
+
46
+ def perform_copy(remote_object)
47
+ copied_object = local_copy_with_simple_attributes(remote_object)
48
+ copied_remote_objects[remote_object] = copied_object
49
+ copy_associated_objects(copied_object, remote_object) unless was_reused?(copied_object)
50
+ copied_object
51
+ end
52
+
53
+ def local_copy_with_simple_attributes(remote_object)
54
+ if should_reuse_local_copy?(remote_object)
55
+ find_or_clone_local_copy_with_simple_attributes(remote_object)
56
+ else
57
+ create_local_copy_with_simple_attributes(remote_object)
58
+ end
59
+ end
60
+
61
+ def should_reuse_local_copy?(remote_object)
62
+ finders_for_reusing_classes.include?(remote_object.class.base_class)
63
+ end
64
+
65
+ def finders_for_reusing_classes
66
+ options[:reuse] || {}
67
+ end
68
+
69
+ def find_or_clone_local_copy_with_simple_attributes(remote_object)
70
+ found_local_object = finder_for_remote_object(remote_object).call(remote_object)
71
+ if found_local_object
72
+ copy_simple_attributes(found_local_object, remote_object)
73
+ reused_local_objects << found_local_object
74
+ found_local_object
75
+ else
76
+ create_local_copy_with_simple_attributes(remote_object)
77
+ end
78
+ end
79
+
80
+ def was_reused?(local_object)
81
+ reused_local_objects.include? local_object
82
+ end
83
+
84
+ def find_local_copy_with_simple_attributes(remote_object)
85
+ finder_for_remote_object(remote_object).call(remote_object)
86
+ end
87
+
88
+ def finder_for_remote_object(remote_object)
89
+ finder = finders_for_reusing_classes[remote_object.class.base_class]
90
+ finder = build_attribute_finder(remote_object, finder) if finder.is_a? Symbol
91
+ finder
92
+ end
93
+
94
+ def build_attribute_finder(remote_object, attribute_name)
95
+ value = remote_object.send(attribute_name)
96
+ lambda do |object|
97
+ object.class.base_class.where(attribute_name => value).first
98
+ end
99
+ end
100
+
101
+ def create_local_copy_with_simple_attributes(remote_object)
102
+ # 'self.dup.becomes(Invoice)' won't work because of different AR connections.
103
+ # todo: prepare for rails 3 and attribute protection
104
+ debug "#{as_trace(remote_object)} copying..."
105
+
106
+ # todo: pending test for STI scenario
107
+ base_class = base_local_class_for(remote_object)
108
+
109
+ disable_all_callbacks_for(base_class)
110
+
111
+ cloned_object = base_class.new
112
+ copy_attributes(cloned_object, simple_attributes_to_copy(remote_object))
113
+ cloned_object.save!(validate: false)
114
+ invoke_callbacks(:after_each, cloned_object, remote_object)
115
+ cloned_object
116
+ end
117
+
118
+ def base_local_class_for(remote_object)
119
+ base_class = remote_object.class.base_class
120
+ if remote_object.respond_to?(:type)
121
+ base_class = remote_object.type.constantize rescue base_class
122
+ end
123
+ base_class
124
+ end
125
+
126
+ def invoke_callbacks(callback_name, copied_object, remote_object)
127
+ callback = callbacks_for(callback_name)[copied_object.class]
128
+ return unless callback
129
+ callback.call(copied_object, remote_object)
130
+ end
131
+
132
+ def callbacks_for(callback_name)
133
+ options[callback_name] || {}
134
+ end
135
+
136
+ # Using setters explicitly to avoid having to mess with disabling mass protection in Rails 3
137
+ def copy_attributes(target_object, attributes_map)
138
+ attributes_map.each do |attribute_name, attribute_value|
139
+ target_object.send("#{attribute_name}=", attribute_value) rescue debug("The method '#{attribute_name}=' does not exist. Different schemas in the remote and local databases?")
140
+ end
141
+ end
142
+
143
+ def disable_all_callbacks_for(base_class)
144
+ [:create, :save, :update, :validate, :touch].each { |callback| base_class.reset_callbacks callback }
145
+ end
146
+
147
+ def simple_attributes_to_copy(remote_object)
148
+ remote_object.attributes.except('id').reject do |attribute_name|
149
+ attributes_to_exclude(remote_object).include? attribute_name.to_sym
150
+ end
151
+ end
152
+
153
+ def attributes_to_exclude(remote_object)
154
+ @attributes_to_exclude_map ||= {}
155
+ @attributes_to_exclude_map[remote_object.class.base_class] ||= calculate_attributes_to_exclude(remote_object)
156
+ end
157
+
158
+ def calculate_attributes_to_exclude(remote_object)
159
+ ((options[:exclude] && options[:exclude][remote_object.class.base_class]) || []).collect(&:to_sym)
160
+ end
161
+
162
+ def copy_simple_attributes(target_local_object, source_remote_object)
163
+ debug "#{as_trace(source_remote_object)} reusing..."
164
+ # update_columns skips callbacks too but not available in Rails 3
165
+ copy_attributes(target_local_object, simple_attributes_to_copy(source_remote_object))
166
+ target_local_object.save!(validate: false)
167
+ end
168
+
169
+ def logger
170
+ Forceps.logger
171
+ end
172
+
173
+ def increase_level
174
+ @level += 1
175
+ end
176
+
177
+ def decrease_level
178
+ @level -= 1
179
+ end
180
+
181
+ def as_trace(remote_object)
182
+ "<#{remote_object.class.base_class.name} - #{remote_object.id}>"
183
+ end
184
+
185
+ def debug(message)
186
+ left_margin = " "*level
187
+ logger.debug "#{left_margin}#{message}"
188
+ end
189
+
190
+ def copy_associated_objects(local_object, remote_object)
191
+ with_nested_logging do
192
+ [:has_many, :has_one, :belongs_to, :has_and_belongs_to_many].each do |association_kind|
193
+ copy_objects_associated_by_association_kind(local_object, remote_object, association_kind)
194
+ end
195
+ end
196
+ end
197
+
198
+ def with_nested_logging
199
+ increase_level
200
+ yield
201
+ decrease_level
202
+ end
203
+
204
+ def copy_objects_associated_by_association_kind(local_object, remote_object, association_kind)
205
+ associations_to_copy(remote_object, association_kind).collect(&:name).each do |association_name|
206
+ send "copy_associated_objects_in_#{association_kind}", local_object, remote_object, association_name
207
+ end
208
+ end
209
+
210
+ def associations_to_copy(remote_object, association_kind)
211
+ excluded_attributes = attributes_to_exclude(remote_object)
212
+ remote_object.class.reflect_on_all_associations(association_kind).reject do |association|
213
+ association.options[:through] || excluded_attributes.include?(:all_associations) || excluded_attributes.include?(association.name)
214
+ end
215
+ end
216
+
217
+ def copy_associated_objects_in_has_many(local_object, remote_object, association_name)
218
+ remote_object.send(association_name).find_each do |remote_associated_object|
219
+ local_object.send(association_name) << copy(remote_associated_object)
220
+ end
221
+ end
222
+
223
+ def copy_associated_objects_in_has_one(local_object, remote_object, association_name)
224
+ remote_associated_object = remote_object.send(association_name)
225
+ local_object.send "#{association_name}=", remote_associated_object && copy(remote_associated_object)
226
+ local_object.save!(validate: false)
227
+ end
228
+
229
+ def copy_associated_objects_in_belongs_to(local_object, remote_object, association_name)
230
+ copy_associated_objects_in_has_one local_object, remote_object, association_name
231
+ end
232
+
233
+ def copy_associated_objects_in_has_and_belongs_to_many(local_object, remote_object, association_name)
234
+ remote_object.send(association_name).find_each do |remote_associated_object|
235
+ # TODO: Review dirty way to avoid copying objects related by has_and_belong_to_many in both extremes twice
236
+ cloned_local_associated_object = copy(remote_associated_object)
237
+ unless local_object.send(association_name).where(id: cloned_local_associated_object.id).exists?
238
+ local_object.send(association_name) << cloned_local_associated_object
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end
245
+
@@ -0,0 +1,126 @@
1
+ module Forceps
2
+ class Client
3
+ attr_reader :options
4
+
5
+ def configure(options={})
6
+ @options = options.merge(default_options)
7
+
8
+ declare_remote_model_classes
9
+ make_associations_reference_remote_classes
10
+
11
+ logger.debug "Classes handled by Forceps: #{model_classes.collect(&:name).inspect}"
12
+ end
13
+
14
+ private
15
+
16
+ def logger
17
+ Forceps.logger
18
+ end
19
+
20
+ def default_options
21
+ {}
22
+ end
23
+
24
+ def model_classes
25
+ @model_classes ||= ActiveRecord::Base.descendants - model_classes_to_exclude
26
+ end
27
+
28
+ def model_classes_to_exclude
29
+ if Rails::VERSION::MAJOR >= 4
30
+ [ActiveRecord::SchemaMigration]
31
+ else
32
+ []
33
+ end
34
+ end
35
+
36
+ def declare_remote_model_classes
37
+ return if @remote_classes_defined
38
+ model_classes.each { |remote_class| declare_remote_model_class(remote_class) }
39
+ @remote_classes_defined = true
40
+ end
41
+
42
+ def declare_remote_model_class(klass)
43
+ class_name = remote_class_name_for(klass.name)
44
+ new_class = build_new_remote_class klass, class_name
45
+ Forceps::Remote.const_set(class_name, new_class)
46
+ remote_class_for(class_name).establish_connection 'remote'
47
+ end
48
+
49
+ def build_new_remote_class(local_class, class_name)
50
+ needs_type_condition = (local_class.base_class != ActiveRecord::Base) && local_class.finder_needs_type_condition?
51
+ Class.new(local_class) do
52
+ self.table_name = local_class.table_name
53
+
54
+ include Forceps::ActsAsCopyableModel
55
+
56
+ # We don't want to include STI condition automatically (the base class extends the original one)
57
+ unless needs_type_condition
58
+ def self.finder_needs_type_condition?
59
+ false
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def remote_class_name_for(local_class_name)
66
+ local_class_name.gsub('::', '_')
67
+ end
68
+
69
+ def remote_class_for(class_name)
70
+ Forceps::Remote::const_get(remote_class_name_for(class_name))
71
+ end
72
+
73
+ def make_associations_reference_remote_classes
74
+ model_classes.each do |model_class|
75
+ make_associations_reference_remote_classes_for(model_class)
76
+ end
77
+ end
78
+
79
+ def make_associations_reference_remote_classes_for(model_class)
80
+ model_class.reflect_on_all_associations.each do |association|
81
+ next if association.class_name =~ /Forceps::Remote/ rescue next
82
+ reference_remote_class(model_class, association)
83
+ end
84
+ end
85
+
86
+ def reference_remote_class(model_class, association)
87
+ remote_model_class = remote_class_for(model_class.name)
88
+
89
+ if association.options[:polymorphic]
90
+ reference_remote_class_in_polymorfic_association(association, remote_model_class)
91
+ else
92
+ reference_remote_class_in_normal_association(association, remote_model_class)
93
+ end
94
+ end
95
+
96
+ def reference_remote_class_in_polymorfic_association(association, remote_model_class)
97
+ # todo: test
98
+ foreign_type_attribute_name = association.foreign_type
99
+
100
+ remote_model_class.send(:define_method, association.foreign_type) do
101
+ "Forceps::Remote::#{super()}"
102
+ end
103
+
104
+ remote_model_class.send(:define_method, "[]") do |attribute_name|
105
+ if (attribute_name.to_s==foreign_type_attribute_name)
106
+ "Forceps::Remote::#{super(attribute_name)}"
107
+ else
108
+ super(attribute_name)
109
+ end
110
+ end
111
+ end
112
+
113
+ def reference_remote_class_in_normal_association(association, remote_model_class)
114
+ related_remote_class = remote_class_for(association.klass.name)
115
+
116
+ cloned_association = association.dup
117
+ cloned_association.instance_variable_set("@klass", related_remote_class)
118
+
119
+ cloned_reflections = remote_model_class.reflections.dup
120
+ cloned_reflections[cloned_association.name.to_sym] = cloned_association
121
+ remote_model_class.reflections = cloned_reflections
122
+ end
123
+ end
124
+ end
125
+
126
+
@@ -0,0 +1,3 @@
1
+ module Forceps
2
+ VERSION = "0.5.0"
3
+ end
data/lib/forceps.rb ADDED
@@ -0,0 +1,30 @@
1
+ require "forceps/client"
2
+ require "forceps/acts_as_copyable_model"
3
+ require 'logging'
4
+
5
+ module Forceps
6
+ def self.configure(options={})
7
+ client.configure(options)
8
+ end
9
+
10
+ def self.client
11
+ @@client ||= Forceps::Client.new
12
+ end
13
+
14
+ def self.logger
15
+ @@logger ||= begin
16
+ logger = Logging.logger(STDOUT)
17
+ logger.level = :debug
18
+ logger
19
+ end
20
+ end
21
+
22
+ def self.logger=(logger)
23
+ @@logger = logger
24
+ end
25
+
26
+ module Remote
27
+
28
+ end
29
+ end
30
+
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :forceps do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,21 @@
1
+ require 'test_helper'
2
+
3
+ class CallbacksTest < ActiveSupport::TestCase
4
+ def setup
5
+ @remote_product = RemoteProduct.create!(name: 'MBP', price: '2000', id: 123456).tap{|product| product.update_column :type, nil}
6
+ end
7
+
8
+ test "should invoke the configured 'after_each' callback" do
9
+ after_each_callback_mock = MiniTest::Mock.new
10
+ after_each_callback_mock.expect(:call, nil) do |args|
11
+ assert_equal Product.find_by_name('MBP'), args[0]
12
+ assert_identical @remote_product, args[1]
13
+ end
14
+
15
+ Forceps.configure :after_each => {Product => after_each_callback_mock}
16
+
17
+ Forceps::Remote::Product.find(@remote_product).copy_to_local
18
+
19
+ assert after_each_callback_mock.verify
20
+ end
21
+ end
@@ -0,0 +1,73 @@
1
+ require 'test_helper'
2
+
3
+ class CloneStructuresTest < ActiveSupport::TestCase
4
+
5
+ def setup
6
+ @remote_invoice = RemoteInvoice.create! number: 123, date: Time.now
7
+ @remote_user = RemoteUser.create! name: 'Jorge'
8
+ Forceps.configure
9
+ end
10
+
11
+ test "should download a single record" do
12
+ Forceps::Remote::Invoice.find_by_number(@remote_invoice.number).copy_to_local
13
+ assert_identical @remote_invoice.becomes(Invoice), Invoice.find_by_number(123)
14
+ end
15
+
16
+ test "should download object with 'has_many'" do
17
+ 2.times { |index| @remote_user.invoices.create! number: index+1, date: "2014-1-#{index+1}" }
18
+
19
+ Forceps::Remote::User.find(@remote_user).copy_to_local
20
+
21
+ copied_user = User.find_by_name('Jorge')
22
+ assert_identical @remote_user, copied_user
23
+ 2.times { |index| assert_identical @remote_user.invoices[index], copied_user.invoices[index] }
24
+ end
25
+
26
+ test "should download object with 'belongs_to'" do
27
+ @remote_invoice = @remote_user.invoices.create! number: 1234, date: "2014-1-3"
28
+
29
+ Forceps::Remote::Invoice.find(@remote_invoice).copy_to_local
30
+
31
+ copied_invoice = Invoice.find_by_number(1234)
32
+ assert_identical @remote_invoice, copied_invoice
33
+ end
34
+
35
+ test "should download objects with 'has_and_belongs_to_many'" do
36
+ remote_tags = 2.times.collect { |index| RemoteTag.create name: "tag #{index}" }
37
+ remote_products = 2.times.collect do |index|
38
+ RemoteProduct.create(name: "product #{index}").tap do |product|
39
+ product.update_column :type, nil # we don't STI here
40
+ end
41
+ end
42
+ remote_products.each { |remote_product| remote_tags.each {|remote_tag| remote_product.tags << remote_tag} }
43
+
44
+ Forceps::Remote::Tag.find(remote_tags[0]).copy_to_local
45
+
46
+ assert_equal 2, Product.count
47
+ assert_equal 2, Tag.count
48
+
49
+ 2.times do |index|
50
+ assert_not_nil tag = Tag.find_by_name("tag #{index}")
51
+ assert_not_nil Product.find_by_name("product #{index}")
52
+ assert_equal 2, tag.products.count
53
+ end
54
+ end
55
+
56
+ test "should download object with 'has_one'" do
57
+ remote_address = RemoteAddress.create!(street: 'Uria', city: 'Oviedo', country: 'Spain', user: @remote_user)
58
+
59
+ Forceps::Remote::User.find(@remote_user).copy_to_local
60
+
61
+ copied_user = User.find_by_name('Jorge')
62
+ assert_identical remote_address, copied_user.address
63
+ end
64
+
65
+ test "should download child objects when using single table inheritance" do
66
+ remote_car = RemoteProduct.create!(name: 'audi')
67
+ remote_car.update_column :type, 'Car'
68
+
69
+ Forceps::Remote::Product.find_by_name('audi').becomes(Forceps::Remote::Product).copy_to_local
70
+ assert_not_nil Product.find_by_name('CAR: audi')
71
+ end
72
+
73
+ end
@@ -0,0 +1,28 @@
1
+ == README
2
+
3
+ This README would normally document whatever steps are necessary to get the
4
+ application up and running.
5
+
6
+ Things you may want to cover:
7
+
8
+ * Ruby version
9
+
10
+ * System dependencies
11
+
12
+ * Configuration
13
+
14
+ * Database creation
15
+
16
+ * Database initialization
17
+
18
+ * How to run the test suite
19
+
20
+ * Services (job queues, cache servers, search engines, etc.)
21
+
22
+ * Deployment instructions
23
+
24
+ * ...
25
+
26
+
27
+ Please feel free to use a different markup language if you do not plan to run
28
+ <tt>rake doc:app</tt>.
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require File.expand_path('../config/application', __FILE__)
5
+
6
+ Dummy::Application.load_tasks
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file.
9
+ //
10
+ // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .
@@ -0,0 +1,13 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the top of the
9
+ * compiled file, but it's generally better to create a new file per style scope.
10
+ *
11
+ *= require_self
12
+ *= require_tree .
13
+ */
@@ -0,0 +1,5 @@
1
+ class ApplicationController < ActionController::Base
2
+ # Prevent CSRF attacks by raising an exception.
3
+ # For APIs, you may want to use :null_session instead.
4
+ protect_from_forgery with: :exception
5
+ end
@@ -0,0 +1,2 @@
1
+ module ApplicationHelper
2
+ end
@@ -0,0 +1,3 @@
1
+ class Address < ActiveRecord::Base
2
+ belongs_to :user
3
+ end
@@ -0,0 +1,5 @@
1
+ class Car < Product
2
+ def name=(new_name)
3
+ super("CAR: #{new_name}")
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ class Invoice < ActiveRecord::Base
2
+ belongs_to :user
3
+
4
+ has_many :line_items
5
+
6
+ has_one :address, through: :user
7
+ end
@@ -0,0 +1,4 @@
1
+ class LineItem < ActiveRecord::Base
2
+ belongs_to :product
3
+ belongs_to :invoice
4
+ end
@@ -0,0 +1,4 @@
1
+ class Product < ActiveRecord::Base
2
+ has_many :line_items
3
+ has_and_belongs_to_many :tags
4
+ end
@@ -0,0 +1,3 @@
1
+ class Tag < ActiveRecord::Base
2
+ has_and_belongs_to_many :products
3
+ end
@@ -0,0 +1,4 @@
1
+ class User < ActiveRecord::Base
2
+ has_many :invoices
3
+ has_one :address
4
+ end