mince_migrator 0.0.1 → 1.0.0

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