tush 0.2.1
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/.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
|