tush 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ tush
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-1.9.3-p327
data/Gemfile ADDED
@@ -0,0 +1,24 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ gem "activerecord"
9
+ gem "activesupport"
10
+ gem "shoulda", ">= 0"
11
+ gem "rdoc", "~> 3.12"
12
+ gem "bundler"
13
+ gem "ruby_deep_clone"
14
+ gem "sneaky-save", :git => "git@github.com:partyearth/sneaky-save.git"
15
+
16
+ group :development, :test do
17
+ gem 'simplecov'
18
+ gem "awesome_print"
19
+ gem "pry"
20
+ gem "pry-nav"
21
+ gem "rspec"
22
+ gem "sqlite3"
23
+ gem "jeweler", "~> 1.8.4"
24
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,86 @@
1
+ GIT
2
+ remote: git@github.com:partyearth/sneaky-save.git
3
+ revision: b000d4f941fc220ea9f4686b99753712b103c853
4
+ specs:
5
+ sneaky-save (0.0.4)
6
+ activerecord (>= 3.2.0)
7
+
8
+ GEM
9
+ remote: http://rubygems.org/
10
+ specs:
11
+ activemodel (3.2.13)
12
+ activesupport (= 3.2.13)
13
+ builder (~> 3.0.0)
14
+ activerecord (3.2.13)
15
+ activemodel (= 3.2.13)
16
+ activesupport (= 3.2.13)
17
+ arel (~> 3.0.2)
18
+ tzinfo (~> 0.3.29)
19
+ activesupport (3.2.13)
20
+ i18n (= 0.6.1)
21
+ multi_json (~> 1.0)
22
+ arel (3.0.2)
23
+ awesome_print (1.1.0)
24
+ builder (3.0.4)
25
+ coderay (1.0.9)
26
+ diff-lcs (1.2.4)
27
+ git (1.2.5)
28
+ i18n (0.6.1)
29
+ jeweler (1.8.4)
30
+ bundler (~> 1.0)
31
+ git (>= 1.2.5)
32
+ rake
33
+ rdoc
34
+ json (1.7.7)
35
+ method_source (0.8.1)
36
+ multi_json (1.7.3)
37
+ pry (0.9.12.1)
38
+ coderay (~> 1.0.5)
39
+ method_source (~> 0.8)
40
+ slop (~> 3.4)
41
+ pry-nav (0.2.3)
42
+ pry (~> 0.9.10)
43
+ rake (10.0.4)
44
+ rdoc (3.12.2)
45
+ json (~> 1.4)
46
+ rspec (2.13.0)
47
+ rspec-core (~> 2.13.0)
48
+ rspec-expectations (~> 2.13.0)
49
+ rspec-mocks (~> 2.13.0)
50
+ rspec-core (2.13.1)
51
+ rspec-expectations (2.13.0)
52
+ diff-lcs (>= 1.1.3, < 2.0)
53
+ rspec-mocks (2.13.1)
54
+ ruby_deep_clone (0.3.0)
55
+ shoulda (3.5.0)
56
+ shoulda-context (~> 1.0, >= 1.0.1)
57
+ shoulda-matchers (>= 1.4.1, < 3.0)
58
+ shoulda-context (1.1.1)
59
+ shoulda-matchers (2.1.0)
60
+ activesupport (>= 3.0.0)
61
+ simplecov (0.7.1)
62
+ multi_json (~> 1.0)
63
+ simplecov-html (~> 0.7.1)
64
+ simplecov-html (0.7.1)
65
+ slop (3.4.4)
66
+ sqlite3 (1.3.7)
67
+ tzinfo (0.3.37)
68
+
69
+ PLATFORMS
70
+ ruby
71
+
72
+ DEPENDENCIES
73
+ activerecord
74
+ activesupport
75
+ awesome_print
76
+ bundler
77
+ jeweler (~> 1.8.4)
78
+ pry
79
+ pry-nav
80
+ rdoc (~> 3.12)
81
+ rspec
82
+ ruby_deep_clone
83
+ shoulda
84
+ simplecov
85
+ sneaky-save!
86
+ sqlite3
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 David Huie
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/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # Tush
2
+
3
+ Tush is a gem for migrating database rows between applications with ActiveRecord,
4
+ while preserving *all associations.*
5
+
6
+ ## Installing Tush
7
+
8
+ If you're using Rails, just add the following to your Gemfile:
9
+ ```
10
+ gem 'tush'
11
+ ```
12
+
13
+ ## Tush Exports
14
+
15
+ Data is fed to the importer by first creating a JSON export of your
16
+ rows. This is done by feeding your ActiveRecord model instances to the
17
+ exporter:
18
+ ```ruby
19
+ model_instance1 = ActiveRecordModel.create
20
+ json_export = Tush::Exporter.new([model_instance1]).export_json
21
+ ```
22
+ This will immediately scan each input model instance and recursively
23
+ add any associated models to the export. If `model_instance1` has an
24
+ association with `model_instance2` and `model_instance2` has an
25
+ association with `model_instance3`, *all 3* model instances will be
26
+ included in the export.
27
+
28
+ ## Tush Imports
29
+
30
+ Using a JSON export created with `Tush::Exporter`, we can initialize
31
+ an import, usually in a different application that shares the same
32
+ ActiveRecord models,like this:
33
+ ```ruby
34
+ importer = Tush::Importer.new_from_json(json_export)
35
+ ```
36
+ When we want to create the new models, and therefore importing all
37
+ exported rows from our other application, we run
38
+ ```ruby
39
+ importer.create_models!
40
+ ```
41
+ And to update all foreign keys to be accurate in the new application,
42
+ run
43
+ ```ruby
44
+ importer.update_foreign_keys!
45
+ ```
46
+
47
+ ## Contributing to Tush
48
+
49
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
50
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
51
+ * Fork the project.
52
+ * Start a feature/bugfix branch.
53
+ * Commit and push until you are happy with your contribution.
54
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
55
+ * Tush has *100% test coverage*; keep it that way.
56
+
57
+ ## Copyright
58
+
59
+ Copyright (c) 2013 NationBuilder. See LICENSE.txt for
60
+ further details.
data/Rakefile ADDED
@@ -0,0 +1,38 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ gem.name = "tush"
17
+ gem.homepage = "http://github.com/3dna/tush"
18
+ gem.license = "MIT"
19
+ gem.summary = "Simplified ActiveRecord data migrations between app instances"
20
+ gem.description = "Simplified ActiveRecord data migrations between app instances"
21
+ gem.email = "david@nationbuilder.com"
22
+ gem.authors = ["David Huie", "Lauren Mermel"]
23
+ end
24
+ Jeweler::RubygemsDotOrgTasks.new
25
+
26
+ require 'rspec/core/rake_task'
27
+ RSpec::Core::RakeTask.new(:spec)
28
+ task :default => :spec
29
+
30
+ require 'rdoc/task'
31
+ Rake::RDocTask.new do |rdoc|
32
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
33
+
34
+ rdoc.rdoc_dir = 'rdoc'
35
+ rdoc.title = "tush #{version}"
36
+ rdoc.rdoc_files.include('README*')
37
+ rdoc.rdoc_files.include('lib/**/*.rb')
38
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.1
data/lib/tush.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'tush/model_store'
2
+ require 'tush/model_wrapper'
3
+ require 'tush/exporter'
4
+ require 'tush/importer'
5
+
6
+ module Tush
7
+
8
+ SUPPORTED_ASSOCIATIONS = [:belongs_to,
9
+ :has_one,
10
+ :has_many]
11
+
12
+ end
@@ -0,0 +1,27 @@
1
+ require 'json'
2
+
3
+ module Tush
4
+
5
+ # This class exports a collection of ModelStores as JSON.
6
+ class Exporter
7
+
8
+ attr_accessor :data
9
+
10
+ def initialize(model_instances, opts={})
11
+ blacklisted_models = opts[:blacklisted_models] || []
12
+ copy_only_models = opts[:copy_only_models] || []
13
+
14
+ model_store = ModelStore.new(:blacklisted_models => blacklisted_models,
15
+ :copy_only_models => copy_only_models)
16
+ model_store.push_array(model_instances)
17
+
18
+ self.data = model_store.export
19
+ end
20
+
21
+ def export_json
22
+ self.data.to_json
23
+ end
24
+
25
+ end
26
+
27
+ end
@@ -0,0 +1,92 @@
1
+ module Tush
2
+
3
+ class AssociationHelpers
4
+
5
+ def self.relation_infos(relation_type, klass)
6
+ if klass.is_a?(String)
7
+ klass = klass.constantize
8
+ end
9
+
10
+ klass.reflect_on_all_associations(relation_type)
11
+ end
12
+
13
+ def self.model_relation_info(model)
14
+ relation_infos = {}
15
+
16
+ SUPPORTED_ASSOCIATIONS.each do |association_type|
17
+ relation_infos[association_type] = self.relation_infos(association_type, model)
18
+ end
19
+
20
+ relation_infos
21
+ end
22
+
23
+ def self.model_to_relation_infos(models)
24
+ models = models.uniq
25
+ model_to_relation_infos_hash = {}
26
+
27
+ models.each do |model|
28
+ model_to_relation_infos_hash[model] = self.model_relation_info(model)
29
+ end
30
+
31
+ model_to_relation_infos_hash
32
+ end
33
+
34
+ # Determine the class the foreign key points to
35
+ def self.class_for_foreign_key(association_info)
36
+ if association_info.macro == :belongs_to
37
+ association_info.class_name.constantize
38
+ else
39
+ association_info.active_record
40
+ end
41
+ end
42
+
43
+ # Determine the class that actually has the foreign key.
44
+ def self.class_with_foreign_key(association_info)
45
+ if association_info.macro == :belongs_to
46
+ association_info.active_record
47
+ else
48
+ # has_one and has_many keys are stored in a model that's
49
+ # different than the one they're declared in.
50
+ association_info.class_name.constantize
51
+ end
52
+ end
53
+
54
+ # This method locates all foreign key columns for a a list of model classes
55
+ # for foreign keys declared within the list of models classes.
56
+ def self.create_foreign_key_mapping(model_classes)
57
+ model_to_foreign_keys = {}
58
+ model_classes.each do |model_class|
59
+ model_to_foreign_keys[model_class] = []
60
+ end
61
+
62
+ model_to_relation_infos(model_classes).each do |model, relation_infos|
63
+ SUPPORTED_ASSOCIATIONS.each do |association_type|
64
+
65
+ associations = relation_infos[association_type]
66
+
67
+ associations.each do |association|
68
+ klass = self.class_with_foreign_key(association)
69
+
70
+ # An association with a class that wasn't
71
+ # included in our list of model_classes might be found.
72
+ unless model_to_foreign_keys.keys.include?(klass)
73
+ model_to_foreign_keys[klass] = []
74
+ end
75
+
76
+ association_hash = { :foreign_key => association.foreign_key,
77
+ :class => self.class_for_foreign_key(association) }
78
+
79
+ unless model_to_foreign_keys[klass].include?(association_hash)
80
+ model_to_foreign_keys[klass] << association_hash
81
+ end
82
+ end
83
+ end
84
+
85
+ end
86
+
87
+ model_to_foreign_keys
88
+ end
89
+
90
+ end
91
+
92
+ end
@@ -0,0 +1,91 @@
1
+ require 'tush/model_store'
2
+ require 'json'
3
+
4
+ module Tush
5
+
6
+ # This class takes in a tush export and imports it into ActiveRecord.
7
+ class Importer
8
+
9
+ class NonUniqueWrapperError < RuntimeError; end
10
+ class InvalidWrapperError < RuntimeError; end
11
+
12
+ attr_accessor(:data,
13
+ :imported_model_wrappers,
14
+ :model_to_attribute_blacklist)
15
+
16
+ def initialize(exported_data)
17
+ self.data = exported_data
18
+ self.imported_model_wrappers = []
19
+ self.model_to_attribute_blacklist = {}
20
+ end
21
+
22
+ def self.new_from_json_file(json_path)
23
+ unparsed_json = File.read(json_path)
24
+ self.new(JSON.parse(unparsed_json))
25
+ end
26
+
27
+ def self.new_from_json(unparsed_json)
28
+ self.new(JSON.parse(unparsed_json))
29
+ end
30
+
31
+ def create_models!
32
+ model_wrappers = self.data["model_wrappers"]
33
+
34
+ model_wrappers.each do |model_wrapper|
35
+ model_class = model_wrapper["model_class"].constantize
36
+ imported_model_wrapper =
37
+ ModelWrapper.new(:model_class => model_class.to_s,
38
+ :model_attributes => model_wrapper["model_attributes"],
39
+ :blacklisted_attributes => self.model_to_attribute_blacklist[model_class])
40
+
41
+ imported_model_wrapper.create_copy
42
+
43
+ self.imported_model_wrappers << imported_model_wrapper
44
+ end
45
+ end
46
+
47
+ def find_wrapper_by_class_and_old_id(klass, old_id)
48
+ wrappers = self.imported_model_wrappers.select do |wrapper|
49
+ (wrapper.model_class == klass) and (wrapper.original_db_id == old_id)
50
+ end
51
+
52
+ wrappers[0]
53
+ end
54
+
55
+ # This method updates stale foreign keys after the new models have been created.
56
+ def update_foreign_keys!
57
+ models = self.imported_model_wrappers.map { |wrapper| wrapper.model_class }
58
+ model_to_foreign_keys = AssociationHelpers.create_foreign_key_mapping(models)
59
+
60
+ imported_model_wrappers.each do |wrapper|
61
+ foreign_keys = model_to_foreign_keys[wrapper.model_class]
62
+
63
+ foreign_keys.each do |foreign_key_info|
64
+ match = self.find_wrapper_by_class_and_old_id(foreign_key_info[:class],
65
+ wrapper.model_attributes[foreign_key_info[:foreign_key]])
66
+
67
+ if match
68
+ new_id = match.new_model.send(match.original_db_key)
69
+ else
70
+ # If we don't have a model wrapper (like in the case of a copy only model),
71
+ # remove the id
72
+ new_id = nil
73
+ end
74
+
75
+ ActiveRecord::Base.transaction(:requires_new => true) do
76
+ begin
77
+ wrapper.new_model.update_column(foreign_key_info[:foreign_key],
78
+ new_id)
79
+ # If the column has a not null restraint, keep the original value
80
+ rescue ActiveRecord::StatementInvalid
81
+ raise ActiveRecord::Rollback
82
+ end
83
+
84
+ wrapper.new_model_attributes[foreign_key_info[:foreign_key]] = new_id
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ end