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