tush 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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