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.
- data/MIT-LICENSE +20 -0
- data/Rakefile +32 -0
- data/lib/forceps/acts_as_copyable_model.rb +245 -0
- data/lib/forceps/client.rb +126 -0
- data/lib/forceps/version.rb +3 -0
- data/lib/forceps.rb +30 -0
- data/lib/tasks/forceps_tasks.rake +4 -0
- data/test/callbacks_test.rb +21 -0
- data/test/clone_structures_test.rb +73 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +13 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/models/address.rb +3 -0
- data/test/dummy/app/models/car.rb +5 -0
- data/test/dummy/app/models/invoice.rb +7 -0
- data/test/dummy/app/models/line_item.rb +4 -0
- data/test/dummy/app/models/product.rb +4 -0
- data/test/dummy/app/models/tag.rb +3 -0
- data/test/dummy/app/models/user.rb +4 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/config/application.rb +23 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +31 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +29 -0
- data/test/dummy/config/environments/production.rb +80 -0
- data/test/dummy/config/environments/test.rb +36 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +12 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +56 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/db/development.sqlite3 +0 -0
- data/test/dummy/db/migrate/20140101112114_create_invoices.rb +9 -0
- data/test/dummy/db/migrate/20140102125046_add_number_to_invoices.rb +5 -0
- data/test/dummy/db/migrate/20140103172515_create_users.rb +8 -0
- data/test/dummy/db/migrate/20140103172643_add_user_id_to_invoices.rb +5 -0
- data/test/dummy/db/migrate/20140104145204_create_line_items.rb +11 -0
- data/test/dummy/db/migrate/20140104145227_create_products.rb +10 -0
- data/test/dummy/db/migrate/20140104150906_create_addresses.rb +12 -0
- data/test/dummy/db/migrate/20140114111137_create_products_tags_join_table.rb +8 -0
- data/test/dummy/db/migrate/20140114111219_create_tags.rb +7 -0
- data/test/dummy/db/migrate/20140117112556_add_type_to_products.rb +5 -0
- data/test/dummy/db/remote.sqlite3 +0 -0
- data/test/dummy/db/schema.rb +64 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/log/RAILS_ENV=test.log +0 -0
- data/test/dummy/log/development.log +608 -0
- data/test/dummy/log/remote.log +182 -0
- data/test/dummy/log/target.log +30 -0
- data/test/dummy/log/test.log +132555 -0
- data/test/dummy/public/404.html +58 -0
- data/test/dummy/public/422.html +58 -0
- data/test/dummy/public/500.html +57 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/exclude_records_test.rb +43 -0
- data/test/fixtures/invoices.yml +11 -0
- data/test/fixtures/users.yml +8 -0
- data/test/reuse_existing_records_test.rb +41 -0
- data/test/support/minitest_ext.rb +13 -0
- data/test/support/remote_address.rb +7 -0
- data/test/support/remote_invoice.rb +8 -0
- data/test/support/remote_line_item.rb +8 -0
- data/test/support/remote_product.rb +8 -0
- data/test/support/remote_tag.rb +7 -0
- data/test/support/remote_user.rb +8 -0
- data/test/test_helper.rb +37 -0
- 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
|
+
|
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,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>.
|
data/test/dummy/Rakefile
ADDED
@@ -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
|
+
*/
|