mince_migrator 0.0.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +20 -58
  3. data/bin/mince_migrator +125 -0
  4. data/lib/mince_migrator/cli_helper.rb +72 -0
  5. data/lib/mince_migrator/config.rb +30 -0
  6. data/lib/mince_migrator/creator.rb +48 -0
  7. data/lib/mince_migrator/deleter.rb +42 -0
  8. data/lib/mince_migrator/list.rb +46 -0
  9. data/lib/mince_migrator/list_report.rb +67 -0
  10. data/lib/mince_migrator/migration.rb +66 -0
  11. data/lib/mince_migrator/migrations/file.rb +74 -0
  12. data/lib/mince_migrator/migrations/loader.rb +52 -0
  13. data/lib/mince_migrator/migrations/name.rb +48 -0
  14. data/lib/mince_migrator/migrations/runner.rb +39 -0
  15. data/lib/mince_migrator/migrations/runner_validator.rb +47 -0
  16. data/lib/mince_migrator/migrations/template.mustache +24 -0
  17. data/lib/mince_migrator/migrations/template.rb +16 -0
  18. data/lib/mince_migrator/migrations/versioned_file.rb +38 -0
  19. data/lib/mince_migrator/ran_migration.rb +27 -0
  20. data/lib/mince_migrator/reverter.rb +57 -0
  21. data/lib/mince_migrator/status_report.rb +41 -0
  22. data/lib/mince_migrator/version.rb +18 -1
  23. data/lib/mince_migrator.rb +5 -4
  24. data/spec/integration/create_a_migration_spec.rb +57 -0
  25. data/spec/integration/deleting_a_migration_spec.rb +40 -0
  26. data/spec/integration/list_all_migrations_spec.rb +57 -0
  27. data/spec/integration/reverting_a_migration_spec.rb +59 -0
  28. data/spec/integration/running_a_migration_spec.rb +66 -0
  29. data/spec/integration_helper.rb +17 -0
  30. data/spec/support/db/a_second_migration.rb +24 -0
  31. data/spec/support/db/a_second_migration_2.rb +24 -0
  32. data/spec/support/db/create_seeded_admin_users.rb +24 -0
  33. data/spec/support/db/first_migration.rb +24 -0
  34. data/spec/support/db/first_migration_2.rb +24 -0
  35. data/spec/support/db/first_migration_3.rb +24 -0
  36. data/spec/support/db/first_migration_4.rb +24 -0
  37. data/spec/support/db/name_of_migration.rb +24 -0
  38. data/spec/support/invalid_interface_migration.rb +7 -0
  39. data/spec/support/not_a_migration.rb +2 -0
  40. data/spec/support/test_migration.rb +30 -0
  41. data/spec/units/mince_migrator/cli_helper_spec.rb +147 -0
  42. data/spec/units/mince_migrator/creator_spec.rb +80 -0
  43. data/spec/units/mince_migrator/deleter_spec.rb +52 -0
  44. data/spec/units/mince_migrator/list_spec.rb +53 -0
  45. data/spec/units/mince_migrator/migration_spec.rb +119 -0
  46. data/spec/units/mince_migrator/migrations/file_spec.rb +85 -0
  47. data/spec/units/mince_migrator/migrations/loader_spec.rb +47 -0
  48. data/spec/units/mince_migrator/migrations/name_spec.rb +47 -0
  49. data/spec/units/mince_migrator/migrations/runner_spec.rb +70 -0
  50. data/spec/units/mince_migrator/migrations/runner_validator_spec.rb +46 -0
  51. data/spec/units/mince_migrator/migrations/template_spec.rb +42 -0
  52. data/spec/units/mince_migrator/migrations/versioned_file_spec.rb +48 -0
  53. data/spec/units/mince_migrator/ran_migration_spec.rb +60 -0
  54. data/spec/units/mince_migrator/reverter_spec.rb +78 -0
  55. metadata +298 -26
  56. data/.gitignore +0 -17
  57. data/Gemfile +0 -4
  58. data/Rakefile +0 -12
  59. data/mince_migrator.gemspec +0 -19
@@ -0,0 +1,52 @@
1
+ module MinceMigrator
2
+ module Migrations
3
+ class Loader
4
+ def initialize(options)
5
+ @klass_name = options[:klass_name]
6
+ @full_path = options[:full_path]
7
+ end
8
+
9
+ def klass
10
+ eval "::MinceMigrator::Migrations::#{@klass_name}"
11
+ end
12
+
13
+ def call
14
+ if valid?
15
+ require @full_path
16
+ perform_post_load_validations
17
+ end
18
+ end
19
+
20
+ def valid?
21
+ file_exists?
22
+ end
23
+
24
+ private
25
+
26
+ def perform_post_load_validations
27
+ mince_migration? && has_valid_interface?
28
+ end
29
+
30
+ def mince_migration?
31
+ klass # Check that constant is valid and loaded
32
+ rescue NameError
33
+ raise 'invalid migration'
34
+ end
35
+
36
+ def has_valid_interface?
37
+ if !has_all_interfaces?
38
+ raise "migration does not have all required methods (:run, :revert, and :time_created)"
39
+ end
40
+ end
41
+
42
+ def has_all_interfaces?
43
+ %w(run revert time_created).all?{|a| klass.respond_to?(a) }
44
+ end
45
+
46
+ def file_exists?
47
+ raise 'migration does not exist' unless ::File.exists?(@full_path)
48
+ true
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,48 @@
1
+ module MinceMigrator
2
+ module Migrations
3
+ class Name
4
+ attr_reader :value
5
+
6
+ def initialize(value)
7
+ self.value = value if value
8
+ @errors = []
9
+ end
10
+
11
+ def filename
12
+ @filename ||= "#{value.downcase.gsub(" ", "_")}.rb"
13
+ end
14
+
15
+ def value=(val)
16
+ normalized_string = normalized_string(val)
17
+ @value = capitalized_phrase(normalized_string)
18
+ end
19
+
20
+ def capitalized_phrase(val)
21
+ val.split(' ').each_with_index.map do |word, i|
22
+ i == 0 ? word.capitalize : word.downcase
23
+ end.join(" ")
24
+ end
25
+
26
+ def normalized_string(val)
27
+ val = val.gsub(/[-_]/, ' ') # convert dashes and underscores to spaces
28
+ pattern = /(^[0-9]|[^a-zA-Z0-9\s])/ # only allow letters and numbers, but do not allow numbers at the beginning
29
+ val.gsub(pattern, '')
30
+ end
31
+
32
+ def valid?
33
+ @errors = []
34
+ validate_value
35
+ @errors.empty?
36
+ end
37
+
38
+ def reasons_for_failure
39
+ @errors.join(" ")
40
+ end
41
+
42
+ def validate_value
43
+ @errors << "Name is invalid, it must start with a character from A-Z or a-z" if value.nil? || value == ''
44
+ end
45
+ end
46
+ end
47
+ end
48
+
@@ -0,0 +1,39 @@
1
+ module MinceMigrator
2
+ module Migrations
3
+ require_relative '../migration'
4
+ require_relative 'runner_validator'
5
+ require 'mince/config'
6
+
7
+ class Runner
8
+ attr_reader :name, :validator
9
+
10
+ def initialize(options)
11
+ if options[:migration]
12
+ @migration = options[:migration]
13
+ @name = migration.name
14
+ elsif options[:name]
15
+ @name = options[:name]
16
+ end
17
+ @validator = RunnerValidator.new(migration)
18
+ end
19
+
20
+ def can_run_migration?
21
+ validator.call
22
+ end
23
+
24
+ def run_migration
25
+ migration.run
26
+ RanMigration.create(name: migration.name)
27
+ true
28
+ end
29
+
30
+ def reasons_for_failure
31
+ validator.errors.join(" ")
32
+ end
33
+
34
+ def migration
35
+ @migration ||= Migration.find(name)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,47 @@
1
+ module MinceMigrator
2
+ module Migrations
3
+ require_relative '../migration'
4
+ require_relative 'runner_validator'
5
+ require 'mince/config'
6
+
7
+ class RunnerValidator
8
+ attr_reader :migration, :errors
9
+
10
+ def initialize(migration)
11
+ @migration = migration
12
+ end
13
+
14
+ def call
15
+ @errors = []
16
+ run_validations
17
+ errors.empty?
18
+ end
19
+
20
+ def run_validations
21
+ validate_mince_interface
22
+ validate_migration_exists
23
+ validate_migration_not_ran
24
+ end
25
+
26
+ def validate_migration_not_ran
27
+ @errors << "Migration has already ran" if interface_is_set? && migration_exists? && migration.ran?
28
+ end
29
+
30
+ def validate_migration_exists
31
+ @errors << "Migration does not exist" if interface_is_set? && !migration_exists?
32
+ end
33
+
34
+ def migration_exists?
35
+ !!migration
36
+ end
37
+
38
+ def validate_mince_interface
39
+ @errors << "Mince interface is not set" unless interface_is_set?
40
+ end
41
+
42
+ def interface_is_set?
43
+ !!Mince::Config.interface
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,24 @@
1
+ module MinceMigrator
2
+ module Migrations
3
+ require 'time'
4
+
5
+ module {{klass_name}}
6
+ def self.run
7
+ # Actual migration goes here
8
+ end
9
+
10
+ def self.revert
11
+ # In case you need to revert this one migration
12
+ end
13
+
14
+ # So you can change the order to run more easily
15
+ def self.time_created
16
+ Time.parse "{{time_created}}"
17
+ end
18
+
19
+ module Temporary
20
+ # Migration dependent classes go here
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,16 @@
1
+ module MinceMigrator
2
+ module Migrations
3
+ require 'mustache'
4
+
5
+ class Template < Mustache
6
+ self.template_file = File.expand_path('../template.mustache', __FILE__)
7
+
8
+ attr_reader :klass_name, :time_created
9
+
10
+ def initialize(klass_name)
11
+ @klass_name = klass_name
12
+ @time_created = Time.now.utc.to_s
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,38 @@
1
+ module MinceMigrator
2
+ module Migrations
3
+ require_relative 'file'
4
+
5
+ class VersionedFile
6
+ attr_reader :version
7
+
8
+ def initialize(name, version = 1)
9
+ @version = version
10
+ @name = name
11
+ end
12
+
13
+ def name
14
+ version > 1 ? "#{@name}_#{version}" : @name
15
+ end
16
+
17
+ def file
18
+ @file ||= File.new(name)
19
+ end
20
+
21
+ def next_unused_version
22
+ if file.persisted?
23
+ bump
24
+ next_unused_version
25
+ else
26
+ file
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def bump
33
+ @version += 1
34
+ @file = File.new(name)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,27 @@
1
+ module MinceMigrator
2
+ require 'mince/model'
3
+ require 'mince/data_model'
4
+
5
+ class RanMigrationDataModel
6
+ include Mince::DataModel
7
+
8
+ data_collection :migrations
9
+ data_fields :name
10
+ end
11
+
12
+ class RanMigration
13
+ include Mince::Model
14
+
15
+ data_model RanMigrationDataModel
16
+ field :name
17
+
18
+ def self.find_by_name(name)
19
+ data = data_model.find_by_field(:name, name)
20
+ new data if data
21
+ end
22
+
23
+ def delete
24
+ data_model.delete_by_params(name: name)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,57 @@
1
+ module MinceMigrator
2
+ require_relative 'migrations/name'
3
+ require_relative 'migration'
4
+ require_relative 'config'
5
+
6
+ class Reverter
7
+ attr_reader :name, :migration_name
8
+
9
+ def initialize(options)
10
+ if options[:migration]
11
+ @migration = options[:migration]
12
+ @migration_name = Migrations::Name.new migration.name
13
+ elsif options[:name]
14
+ @migration_name = Migrations::Name.new options[:name]
15
+ end
16
+ @name = migration_name.value
17
+ @errors = []
18
+ end
19
+
20
+ def can_revert_migration?
21
+ @errors = []
22
+ validate_migration
23
+ validate_ran_migration
24
+ @errors.empty?
25
+ end
26
+
27
+ def validate_ran_migration
28
+ @errors << "Migration has not ran" if migration_not_ran?
29
+ end
30
+
31
+ def validate_migration
32
+ @errors << "Migration does not exist with name '#{name}'" if migration.nil?
33
+ end
34
+
35
+ def revert_migration
36
+ migration.revert
37
+ ran_migration.delete
38
+ true
39
+ end
40
+
41
+ def reasons_for_failure
42
+ @errors.join ", "
43
+ end
44
+
45
+ def migration
46
+ @migration ||= Migration.find(name)
47
+ end
48
+
49
+ def migration_not_ran?
50
+ migration && !migration.ran?
51
+ end
52
+
53
+ def ran_migration
54
+ migration.ran_migration if migration
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,41 @@
1
+ module MinceMigrator
2
+ require 'command_line_reporter'
3
+
4
+ class StatusReport
5
+ include CommandLineReporter
6
+
7
+ attr_reader :migration
8
+
9
+ def initialize(migration)
10
+ @migration = migration
11
+ end
12
+
13
+ def run
14
+ vertical_spacing 2
15
+ header title: "Migration Details for #{migration.name}", bold: true, rule: true, align: 'center', width: 70, timestamp: true
16
+
17
+ table border: false do
18
+ row do
19
+ column 'Name', width: 15
20
+ column migration.name, width: 30
21
+ end
22
+ row do
23
+ column 'Status'
24
+ if migration.status == 'not ran'
25
+ column migration.status, color: 'red'
26
+ else
27
+ column migration.status, color: 'green'
28
+ end
29
+ end
30
+ row do
31
+ column 'Age'
32
+ column migration.age
33
+ end
34
+ row do
35
+ column 'Date Created'
36
+ column migration.time_created.strftime("%m/%d/%Y")
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,3 +1,20 @@
1
1
  module MinceMigrator
2
- VERSION = "0.0.1"
2
+ module Version
3
+ def self.major
4
+ 1
5
+ end
6
+
7
+ def self.minor
8
+ 0
9
+ end
10
+
11
+ def self.patch
12
+ 0
13
+ end
14
+ end
15
+
16
+ def self.version
17
+ [Version.major, Version.minor, Version.patch].join('.')
18
+ end
3
19
  end
20
+
@@ -1,5 +1,6 @@
1
1
  require "mince_migrator/version"
2
-
3
- module MinceMigrator
4
- # Your code goes here...
5
- end
2
+ require "mince_migrator/reverter"
3
+ require "mince_migrator/creator"
4
+ require "mince_migrator/deleter"
5
+ require "mince_migrator/list"
6
+ require "mince_migrator/migrations/runner"
@@ -0,0 +1,57 @@
1
+ require_relative '../integration_helper'
2
+
3
+ describe "The creation of a migration:" do
4
+ subject { MinceMigrator::Creator.new name }
5
+
6
+ context "when a name is not provided" do
7
+ let(:name) { nil }
8
+
9
+ it "returns reasons why a migration could not be created" do
10
+ subject.can_create_migration?.should be_false
11
+ end
12
+ end
13
+
14
+ describe "Creating a mince migration with a name" do
15
+ let(:name) { "Create seeded admin users" }
16
+
17
+ it "can create the migration" do
18
+ subject.can_create_migration?.should be_true
19
+ end
20
+
21
+ it "creates the migration file" do
22
+ subject.create_migration
23
+
24
+ relative_path = Dir.pwd
25
+ expected_migration_file_destination = ::File.join(MinceMigrator::Config.migration_dir, "create_seeded_admin_users.rb")
26
+ ::File.open(expected_migration_file_destination, 'r') do |f|
27
+ expected_content = <<-eos
28
+ module MinceMigrator
29
+ module Migrations
30
+ require 'time'
31
+
32
+ module CreateSeededAdminUsers
33
+ def self.run
34
+ # Actual migration goes here
35
+ end
36
+
37
+ def self.revert
38
+ # In case you need to revert this one migration
39
+ end
40
+
41
+ # So you can change the order to run more easily
42
+ def self.time_created
43
+ Time.parse "#{Time.now.utc.to_s}"
44
+ end
45
+
46
+ module Temporary
47
+ # Migration dependent classes go here
48
+ end
49
+ end
50
+ end
51
+ end
52
+ eos
53
+ f.read.should == expected_content
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,40 @@
1
+ require_relative '../integration_helper'
2
+
3
+ describe 'Deleting a migration' do
4
+ subject { MinceMigrator::Deleter.new(name: name) }
5
+
6
+ let(:name) { 'name of migration' }
7
+ let(:expected_name) { 'Name of migration' }
8
+
9
+ context 'when the migration exists' do
10
+ before do
11
+ MinceMigrator::Creator.create(name)
12
+ end
13
+
14
+ its(:can_delete_migration?) { should be_true }
15
+
16
+ it 'deletes it from the file system' do
17
+ expected_migration_destination = ::File.join(MinceMigrator::Config.migration_dir, 'name_of_migration.rb')
18
+ subject.delete_migration
19
+
20
+ ::File.exists?(expected_migration_destination).should be_false
21
+ end
22
+
23
+ context 'when the migration has ran' do
24
+ before do
25
+ MinceMigrator::Migrations::Runner.new(name: name).run_migration
26
+ end
27
+
28
+ it 'deletes it from the database' do
29
+ subject.delete_migration
30
+
31
+ MinceMigrator::RanMigration.all.should be_empty
32
+ end
33
+ end
34
+ end
35
+
36
+ context 'when the migration does not exist' do
37
+ its(:can_delete_migration?) { should be_false }
38
+ its(:reasons_for_failure) { should == "Migration does not exist with name '#{expected_name}'" }
39
+ end
40
+ end
@@ -0,0 +1,57 @@
1
+ require_relative '../integration_helper'
2
+
3
+ describe 'List of migrations' do
4
+ subject { MinceMigrator::List.new }
5
+
6
+ context 'when there are not any migrations' do
7
+ its(:number_of_migrations) { should == 0 }
8
+
9
+ it 'is empty' do
10
+ subject.all.should be_empty
11
+ end
12
+ end
13
+
14
+ context 'when there is one migration' do
15
+ let(:migration_name) { "first migration" }
16
+
17
+ before do
18
+ MinceMigrator::Creator.create(migration_name)
19
+ end
20
+
21
+ its(:number_of_migrations) { should == 1 }
22
+
23
+ it 'contains a record of the migration' do
24
+ subject.all.size.should == 1
25
+ subject.all.first.name.should == "First migration"
26
+ subject.all.first.status.should == 'not ran'
27
+ subject.all.first.relative_path.should == File.join(MinceMigrator::Config.migration_relative_dir, 'first_migration.rb')
28
+ subject.all.first.path.should == File.join(MinceMigrator::Config.migration_dir, 'first_migration.rb')
29
+ end
30
+ end
31
+
32
+ context 'when there are some migrations' do
33
+ let(:migration_1_name) { "first migration" }
34
+ let(:migration_2_name) { "a second migration" }
35
+ let(:migration_1) { MinceMigrator::Migration.find(migration_1_name) }
36
+ let(:migration_2) { MinceMigrator::Migration.find(migration_2_name) }
37
+
38
+ before do
39
+ MinceMigrator::Creator.create(migration_1_name)
40
+ MinceMigrator::Creator.create(migration_2_name)
41
+ end
42
+
43
+ its(:number_of_migrations) { should == 2 }
44
+
45
+ it 'contains a record of those migrations' do
46
+ expected_migrations = [migration_2, migration_1]
47
+ subject.all.size.should == expected_migrations.size
48
+ subject.all.each_with_index do |migration, index|
49
+ expected_migration = expected_migrations.find{|a| a.name == migration.name }
50
+ migration.name.should == expected_migration.name
51
+ migration.status.should == 'not ran'
52
+ migration.relative_path.should == migration.relative_path
53
+ migration.path.should == migration.path
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,59 @@
1
+ require_relative '../integration_helper'
2
+
3
+ describe 'Reverting a migration' do
4
+ subject { MinceMigrator::Reverter.new(name: name) }
5
+
6
+ let(:name) { 'name of migration' }
7
+ let(:expected_name) { 'Name of migration' }
8
+
9
+ context 'when the migration exists' do
10
+ let(:name) { "test migration" }
11
+ let(:spec_migration) { File.expand_path('../../support/test_migration.rb', __FILE__) }
12
+ let(:db_dir) { MinceMigrator::Config.migration_dir }
13
+ let(:data_model) { MinceMigrator::Migrations::TestMigration::Temporary::UserDataModel }
14
+
15
+ before do
16
+ FileUtils.mkdir_p(db_dir)
17
+ FileUtils.cp(spec_migration, db_dir)
18
+ end
19
+
20
+ context 'and the migration has not yet been ran' do
21
+ before do
22
+ subject.can_revert_migration?
23
+ end
24
+
25
+ its(:reasons_for_failure) { should == "Migration has not ran" }
26
+ its(:can_revert_migration?) { should be_false }
27
+ end
28
+
29
+ context 'when the migration has ran' do
30
+ before do
31
+ MinceMigrator::Migrations::Runner.new(name: name).run_migration
32
+ end
33
+
34
+ its(:can_revert_migration?) { should be_true }
35
+ its(:reasons_for_failure) { should be_empty }
36
+
37
+ it 'reverts the migration' do
38
+ subject.revert_migration
39
+
40
+ data_model.all.size.should == 0
41
+ end
42
+
43
+ it 'reverts it from the database' do
44
+ subject.revert_migration
45
+
46
+ MinceMigrator::RanMigration.all.should be_empty
47
+ end
48
+ end
49
+ end
50
+
51
+ context 'when the migration does not exist' do
52
+ before do
53
+ subject.can_revert_migration?
54
+ end
55
+
56
+ its(:can_revert_migration?) { should be_false }
57
+ its(:reasons_for_failure) { should == "Migration does not exist with name '#{expected_name}'" }
58
+ end
59
+ end