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.
@@ -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