tush 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +24 -0
- data/Gemfile.lock +86 -0
- data/LICENSE.txt +20 -0
- data/README.md +60 -0
- data/Rakefile +38 -0
- data/VERSION +1 -0
- data/lib/tush.rb +12 -0
- data/lib/tush/exporter.rb +27 -0
- data/lib/tush/helpers/association_helpers.rb +92 -0
- data/lib/tush/importer.rb +91 -0
- data/lib/tush/model_store.rb +58 -0
- data/lib/tush/model_wrapper.rb +107 -0
- data/spec/association_helpers_spec.rb +81 -0
- data/spec/exporter_spec.rb +39 -0
- data/spec/helper.rb +40 -0
- data/spec/importer_spec.rb +177 -0
- data/spec/model_store_spec.rb +32 -0
- data/spec/model_wrapper_spec.rb +119 -0
- data/spec/support/exported_data.json +1 -0
- data/spec/support/schema.rb +88 -0
- data/tush.gemspec +102 -0
- metadata +299 -0
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'tush/helpers/association_helpers'
|
2
|
+
|
3
|
+
module Tush
|
4
|
+
|
5
|
+
# This holds the collection of models that will be exported.
|
6
|
+
class ModelStore
|
7
|
+
|
8
|
+
attr_accessor :model_wrappers, :blacklisted_models, :copy_only_models
|
9
|
+
|
10
|
+
def initialize(opts={})
|
11
|
+
self.blacklisted_models = opts[:blacklisted_models] || []
|
12
|
+
self.copy_only_models = opts[:copy_only_models] || []
|
13
|
+
self.model_wrappers = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def push_array(model_array)
|
17
|
+
model_array.each { |model_instance| self.push(model_instance) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def push(model_instance, parent_wrapper=nil)
|
21
|
+
return if self.object_in_stack?(model_instance)
|
22
|
+
return if self.blacklisted_models.include?(model_instance.class)
|
23
|
+
|
24
|
+
model_wrapper = ModelWrapper.new(:model => model_instance)
|
25
|
+
|
26
|
+
if parent_wrapper and parent_wrapper.model_trace.any?
|
27
|
+
model_wrapper.add_model_trace_list(parent_wrapper.model_trace)
|
28
|
+
model_wrapper.add_model_trace(parent_wrapper)
|
29
|
+
elsif parent_wrapper
|
30
|
+
model_wrapper.add_model_trace(parent_wrapper)
|
31
|
+
end
|
32
|
+
|
33
|
+
model_wrappers.push(model_wrapper)
|
34
|
+
|
35
|
+
return if self.copy_only_models.include?(model_instance.class)
|
36
|
+
|
37
|
+
model_wrapper.association_objects.each do |object|
|
38
|
+
self.push(object, model_wrapper)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def object_in_stack?(model_instance)
|
43
|
+
self.model_wrappers.each do |model_wrapper|
|
44
|
+
next if model_instance.class != model_wrapper.model_class
|
45
|
+
next if model_instance.attributes != model_wrapper.model_attributes
|
46
|
+
return true
|
47
|
+
end
|
48
|
+
|
49
|
+
return false
|
50
|
+
end
|
51
|
+
|
52
|
+
def export
|
53
|
+
{ :model_wrappers => self.model_wrappers.map { |model_wrapper| model_wrapper.export } }
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_support'
|
3
|
+
require 'deep_clone'
|
4
|
+
require 'sneaky-save'
|
5
|
+
|
6
|
+
module Tush
|
7
|
+
|
8
|
+
# This is a class the wraps each model instance that we
|
9
|
+
# plan on exporting.
|
10
|
+
class ModelWrapper
|
11
|
+
|
12
|
+
attr_accessor(:model_attributes,
|
13
|
+
:new_model,
|
14
|
+
:new_model_attributes,
|
15
|
+
:model,
|
16
|
+
:model_class,
|
17
|
+
:model_trace,
|
18
|
+
:original_db_id)
|
19
|
+
|
20
|
+
def initialize(opts={})
|
21
|
+
model = opts[:model]
|
22
|
+
|
23
|
+
if model.is_a?(ActiveRecord::Base)
|
24
|
+
self.model = model
|
25
|
+
self.model_class = model.class
|
26
|
+
self.model_attributes = model.attributes || {}
|
27
|
+
else
|
28
|
+
self.model_class = opts[:model_class].constantize
|
29
|
+
self.model_attributes = opts[:model_attributes] || {}
|
30
|
+
end
|
31
|
+
|
32
|
+
self.model_trace = []
|
33
|
+
end
|
34
|
+
|
35
|
+
def original_db_key
|
36
|
+
"id"
|
37
|
+
end
|
38
|
+
|
39
|
+
def create_copy
|
40
|
+
# Define the custom_create method on a model to save
|
41
|
+
# new models in a custom manner.
|
42
|
+
if model_class.respond_to?(:custom_create)
|
43
|
+
self.new_model = self.model_class.custom_create(model_attributes)
|
44
|
+
self.new_model_attributes = self.new_model.attributes
|
45
|
+
else
|
46
|
+
create_without_validation_and_callbacks
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def create_without_validation_and_callbacks
|
51
|
+
attributes = model_attributes.clone
|
52
|
+
attributes.delete(original_db_key)
|
53
|
+
|
54
|
+
copy = model_class.new(attributes)
|
55
|
+
copy.sneaky_save
|
56
|
+
copy.reload
|
57
|
+
|
58
|
+
self.new_model = copy
|
59
|
+
self.new_model_attributes = copy.attributes
|
60
|
+
end
|
61
|
+
|
62
|
+
def original_db_id
|
63
|
+
model_attributes[self.original_db_key]
|
64
|
+
end
|
65
|
+
|
66
|
+
def add_model_trace_list(list)
|
67
|
+
model_trace.concat(list)
|
68
|
+
end
|
69
|
+
|
70
|
+
def add_model_trace(model_wrapper)
|
71
|
+
model_trace << [model_wrapper.model_class.to_s,
|
72
|
+
model_wrapper.original_db_id]
|
73
|
+
end
|
74
|
+
|
75
|
+
def association_objects
|
76
|
+
objects = []
|
77
|
+
SUPPORTED_ASSOCIATIONS.each do |association_type|
|
78
|
+
relation_infos =
|
79
|
+
AssociationHelpers.relation_infos(association_type,
|
80
|
+
model_class)
|
81
|
+
next if relation_infos.empty?
|
82
|
+
|
83
|
+
relation_infos.each do |info|
|
84
|
+
next unless model.respond_to?(info.name)
|
85
|
+
|
86
|
+
object = model.send(info.name)
|
87
|
+
|
88
|
+
if object.is_a?(Array)
|
89
|
+
objects.concat(object)
|
90
|
+
elsif object
|
91
|
+
objects << object
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
objects
|
97
|
+
end
|
98
|
+
|
99
|
+
def export
|
100
|
+
{ :model_class => model_class.to_s,
|
101
|
+
:model_attributes => model_attributes,
|
102
|
+
:model_trace => model_trace }
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'helper'
|
3
|
+
require 'tush'
|
4
|
+
|
5
|
+
describe Tush::AssociationHelpers do
|
6
|
+
|
7
|
+
before :all do
|
8
|
+
|
9
|
+
class Jacob < ActiveRecord::Base
|
10
|
+
belongs_to :jesse
|
11
|
+
end
|
12
|
+
|
13
|
+
class Jesse < ActiveRecord::Base
|
14
|
+
has_one :jim
|
15
|
+
end
|
16
|
+
|
17
|
+
class Jim < ActiveRecord::Base
|
18
|
+
belongs_to :jesse
|
19
|
+
has_many :leah
|
20
|
+
end
|
21
|
+
|
22
|
+
class Leah < ActiveRecord::Base
|
23
|
+
belongs_to :jim
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
describe ".relation_infos" do
|
29
|
+
|
30
|
+
it "returns an info for an association" do
|
31
|
+
infos = Tush::AssociationHelpers.relation_infos(:has_one, Jesse)
|
32
|
+
|
33
|
+
infos.count.should == 1
|
34
|
+
infos.first.name.should == :jim
|
35
|
+
end
|
36
|
+
|
37
|
+
it "works with string classes" do
|
38
|
+
infos = Tush::AssociationHelpers.relation_infos(:belongs_to, "Jacob")
|
39
|
+
|
40
|
+
infos.count.should == 1
|
41
|
+
infos.first.name.should == :jesse
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
describe ".model_relation_info" do
|
47
|
+
|
48
|
+
it "returns a mapping of association to relation info" do
|
49
|
+
info = Tush::AssociationHelpers.model_relation_info(Jim)
|
50
|
+
|
51
|
+
info.keys.should == Tush::SUPPORTED_ASSOCIATIONS
|
52
|
+
info[:has_many].count.should == 1
|
53
|
+
info[:has_many].first.name.should == :leah
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
describe ".create_foreign_key_mapping" do
|
59
|
+
|
60
|
+
it "it finds the appropriate foreign keys for the passed in classes" do
|
61
|
+
mapping = Tush::AssociationHelpers.create_foreign_key_mapping([Jacob, Jesse, Jim, Leah])
|
62
|
+
|
63
|
+
mapping.should == { Jacob => [{ :foreign_key=>"jesse_id", :class=> Jesse }],
|
64
|
+
Jesse => [],
|
65
|
+
Jim => [{ :foreign_key=>"jesse_id", :class=> Jesse }],
|
66
|
+
Leah => [{ :foreign_key=>"jim_id", :class=> Jim }] }
|
67
|
+
end
|
68
|
+
|
69
|
+
it "it returns newly discovered classes in the mapping if an input class has \
|
70
|
+
a has_many or a has_one" do
|
71
|
+
mapping = Tush::AssociationHelpers.create_foreign_key_mapping([Jacob, Jesse])
|
72
|
+
|
73
|
+
mapping.should == { Jacob => [{ :foreign_key=>"jesse_id", :class=> Jesse }],
|
74
|
+
Jesse => [],
|
75
|
+
Jim => [{ :foreign_key=>"jesse_id", :class=> Jesse }] }
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
describe Tush::Exporter do
|
5
|
+
|
6
|
+
before :all do
|
7
|
+
class Jason < ActiveRecord::Base
|
8
|
+
has_one :kumie
|
9
|
+
end
|
10
|
+
|
11
|
+
class Kumie < ActiveRecord::Base; end
|
12
|
+
end
|
13
|
+
|
14
|
+
let!(:jason1) { Jason.create }
|
15
|
+
let!(:jason2) { Jason.create }
|
16
|
+
let!(:kumie1) { Kumie.create :jason_id => jason1.id }
|
17
|
+
let!(:kumie2) { Kumie.create :jason_id => jason2.id }
|
18
|
+
|
19
|
+
let!(:exporter) { Tush::Exporter.new([jason1, jason2]) }
|
20
|
+
|
21
|
+
describe "#data" do
|
22
|
+
|
23
|
+
it "should store data correctly" do
|
24
|
+
exporter.data.should ==
|
25
|
+
{:model_wrappers=>[{:model_class=>"Jason", :model_attributes=>{"id"=>1}, :model_trace=>[]}, {:model_class=>"Kumie", :model_attributes=>{"id"=>1, "jason_id"=>1}, :model_trace=>[["Jason", 1]]}, {:model_class=>"Jason", :model_attributes=>{"id"=>2}, :model_trace=>[]}, {:model_class=>"Kumie", :model_attributes=>{"id"=>2, "jason_id"=>2}, :model_trace=>[["Jason", 2]]}]}
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "#export_json" do
|
31
|
+
|
32
|
+
it "should export its data in json" do
|
33
|
+
exporter.export_json.should ==
|
34
|
+
"{\"model_wrappers\":[{\"model_class\":\"Jason\",\"model_attributes\":{\"id\":1},\"model_trace\":[]},{\"model_class\":\"Kumie\",\"model_attributes\":{\"id\":1,\"jason_id\":1},\"model_trace\":[[\"Jason\",1]]},{\"model_class\":\"Jason\",\"model_attributes\":{\"id\":2},\"model_trace\":[]},{\"model_class\":\"Kumie\",\"model_attributes\":{\"id\":2,\"jason_id\":2},\"model_trace\":[[\"Jason\",2]]}]}"
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
data/spec/helper.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
SimpleCov.start
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'bundler'
|
6
|
+
|
7
|
+
begin
|
8
|
+
Bundler.setup(:default, :development)
|
9
|
+
rescue Bundler::BundlerError => e
|
10
|
+
$stderr.puts e.message
|
11
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
12
|
+
exit e.status_code
|
13
|
+
end
|
14
|
+
|
15
|
+
require 'rspec'
|
16
|
+
require 'shoulda'
|
17
|
+
require 'pry'
|
18
|
+
|
19
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
20
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
21
|
+
require 'tush'
|
22
|
+
|
23
|
+
require 'active_record'
|
24
|
+
|
25
|
+
ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
|
26
|
+
|
27
|
+
require 'support/schema'
|
28
|
+
|
29
|
+
RSpec.configure do |config|
|
30
|
+
config.around do |example|
|
31
|
+
ActiveRecord::Base.transaction do
|
32
|
+
example.run
|
33
|
+
raise ActiveRecord::Rollback
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_root
|
39
|
+
File.expand_path '../..', __FILE__
|
40
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'json'
|
4
|
+
require 'sneaky-save'
|
5
|
+
|
6
|
+
describe Tush::Importer do
|
7
|
+
|
8
|
+
before :all do
|
9
|
+
class Kai < ActiveRecord::Base
|
10
|
+
has_one :brett
|
11
|
+
end
|
12
|
+
|
13
|
+
class Brett < ActiveRecord::Base; end
|
14
|
+
|
15
|
+
class Byron < ActiveRecord::Base
|
16
|
+
belongs_to :kai
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
let!(:exported_data_path) { "#{test_root}/spec/support/exported_data.json" }
|
22
|
+
let(:file) { File.read(exported_data_path) }
|
23
|
+
let(:importer) { Tush::Importer.new_from_json_file(exported_data_path) }
|
24
|
+
|
25
|
+
describe "#create_models!" do
|
26
|
+
|
27
|
+
it "imports data" do
|
28
|
+
importer.create_models!
|
29
|
+
importer.data.should ==
|
30
|
+
{"model_wrappers"=>
|
31
|
+
[{"model_class"=>"Kai",
|
32
|
+
"model_attributes"=>{"id"=>10, "sample_data"=>"data string"},
|
33
|
+
"original_db_key"=>"id",
|
34
|
+
"new_db_key"=>nil,
|
35
|
+
"original_db_id"=>1},
|
36
|
+
{"model_class"=>"Brett",
|
37
|
+
"model_attributes"=>{"id"=>1, "kai_id"=>10, "sample_data"=>"data string"},
|
38
|
+
"original_db_key"=>"id",
|
39
|
+
"new_db_key"=>nil,
|
40
|
+
"original_db_id"=>1},
|
41
|
+
{"model_class"=>"Kai",
|
42
|
+
"model_attributes"=>{"id"=>2, "sample_data"=>"data string"},
|
43
|
+
"original_db_key"=>"id",
|
44
|
+
"new_db_key"=>nil,
|
45
|
+
"original_db_id"=>2},
|
46
|
+
{"model_class"=>"Brett",
|
47
|
+
"model_attributes"=>{"id"=>2, "kai_id"=>2, "sample_data"=>"data string"},
|
48
|
+
"original_db_key"=>"id",
|
49
|
+
"new_db_key"=>nil,
|
50
|
+
"original_db_id"=>2}]}
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "#find_wrapper_by_class_and_old_id" do
|
56
|
+
|
57
|
+
it "returns a matching wrapper" do
|
58
|
+
importer.create_models!
|
59
|
+
match = importer.find_wrapper_by_class_and_old_id(Kai, 10)
|
60
|
+
|
61
|
+
match.model_class.should == Kai
|
62
|
+
match.original_db_id.should == 10
|
63
|
+
match.original_db_key.should == "id"
|
64
|
+
match.new_model.should == Kai.first
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "#update_foreign_keys!" do
|
70
|
+
|
71
|
+
PREFILLED_ROWS = 11
|
72
|
+
|
73
|
+
before :all do
|
74
|
+
class Lauren < ActiveRecord::Base
|
75
|
+
has_one :david
|
76
|
+
def self.custom_create(attributes)
|
77
|
+
Lauren.find_or_create_by_sample_data(attributes["sample_data"])
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class David < ActiveRecord::Base
|
82
|
+
belongs_to :charlie
|
83
|
+
end
|
84
|
+
|
85
|
+
class Charlie < ActiveRecord::Base
|
86
|
+
belongs_to :lauren
|
87
|
+
end
|
88
|
+
|
89
|
+
class Miguel < ActiveRecord::Base
|
90
|
+
belongs_to :lauren
|
91
|
+
end
|
92
|
+
|
93
|
+
class Dan < ActiveRecord::Base
|
94
|
+
has_many :lauren
|
95
|
+
end
|
96
|
+
|
97
|
+
PREFILLED_ROWS.times do
|
98
|
+
Lauren.create
|
99
|
+
Charlie.create
|
100
|
+
David.create
|
101
|
+
Dan.create
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
let!(:dan) { Dan.create }
|
107
|
+
let!(:lauren1) { Lauren.create :dan_id => dan.id, :sample_data => "sample data" }
|
108
|
+
let!(:lauren2) { Lauren.create :dan_id => dan.id, :sample_data => "a;sdlfad" }
|
109
|
+
let!(:charlie) { Charlie.create :lauren_id => lauren2.id }
|
110
|
+
let!(:david) { David.create :lauren_id => lauren1.id, :charlie_id => charlie.id }
|
111
|
+
|
112
|
+
let!(:exported) { Tush::Exporter.new([lauren1, lauren2, david, charlie, dan]).export_json }
|
113
|
+
let!(:importer) { Tush::Importer.new(JSON.parse(exported)) }
|
114
|
+
|
115
|
+
it "imports a few database rows into the same database correctly" do
|
116
|
+
importer.create_models!
|
117
|
+
importer.update_foreign_keys!
|
118
|
+
|
119
|
+
existing_rows = PREFILLED_ROWS + 1
|
120
|
+
|
121
|
+
Dan.count.should == existing_rows + 1
|
122
|
+
Lauren.count.should == existing_rows + 1
|
123
|
+
Charlie.count.should == existing_rows + 1
|
124
|
+
David.count.should == existing_rows + 1
|
125
|
+
|
126
|
+
Dan.last.lauren.map { |lauren| lauren.id }.should == [12, 13]
|
127
|
+
David.last.charlie.id.should == 13
|
128
|
+
David.last.lauren_id.should == 12
|
129
|
+
Charlie.last.lauren.id.should == 13
|
130
|
+
end
|
131
|
+
|
132
|
+
describe "when a model wrapper doesn't exist" do
|
133
|
+
|
134
|
+
it "removes foreign keys if a model wrapper doesn't exist for an association" do
|
135
|
+
lauren = Lauren.create
|
136
|
+
charlie = Charlie.create :lauren => lauren
|
137
|
+
|
138
|
+
exported = Tush::Exporter.new([charlie], :blacklisted_models => [Lauren]).export_json
|
139
|
+
importer = Tush::Importer.new(JSON.parse(exported))
|
140
|
+
|
141
|
+
importer.create_models!
|
142
|
+
importer.update_foreign_keys!
|
143
|
+
|
144
|
+
importer.imported_model_wrappers.count.should == 1
|
145
|
+
importer.imported_model_wrappers[0].new_model.lauren_id.should == nil
|
146
|
+
end
|
147
|
+
|
148
|
+
it "Doesn't remove foreign keys if the column has a not null restraint" do
|
149
|
+
lauren = Lauren.create
|
150
|
+
miguel = Miguel.create :lauren => lauren
|
151
|
+
|
152
|
+
exported = Tush::Exporter.new([miguel], :blacklisted_models => [Lauren]).export_json
|
153
|
+
importer = Tush::Importer.new(JSON.parse(exported))
|
154
|
+
|
155
|
+
importer.create_models!
|
156
|
+
importer.update_foreign_keys!
|
157
|
+
|
158
|
+
importer.imported_model_wrappers.count.should == 1
|
159
|
+
importer.imported_model_wrappers[0].new_model.lauren_id.should == lauren.id
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
|
166
|
+
describe ".new_from_json" do
|
167
|
+
|
168
|
+
it "parses json and stores it in the data attribute" do
|
169
|
+
test_hash = { 'data' => 'data' }
|
170
|
+
importer = Tush::Importer.new_from_json(test_hash.to_json)
|
171
|
+
|
172
|
+
importer.data.should == test_hash
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
|
177
|
+
end
|