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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 41b113ade7cbebb55bd8640f1f1a49739a1ba0c0
4
+ data.tar.gz: cb22a342cd4f20d89ecf0d0bd99a1b577566bde2
5
+ SHA512:
6
+ metadata.gz: a02d661a150eb49a28aea7e37ab96f9d69e979e9531bb0072aac2de525e1bb56eda2c31675f16e6285e95f7e46bfad678b13be914cfcb0579b1302feb7458a2f
7
+ data.tar.gz: 2d58ae4f149108c101ac01e939ddba2f5afc37e224d2fb6133054bdb4588cc559d3b1625563da847447e21fe3c46ca62b20882c87f9be2a835cad077d09bed2a
data/README.md CHANGED
@@ -1,89 +1,51 @@
1
1
  # Mince Migrator
2
2
 
3
- **This is not yet released, this read me is a roadmap for what to do, it is not necessarily what has been done yet.**
3
+ [![Travis CI](https://travis-ci.org/coffeencoke/mince_migrator.png)](https://travis-ci.org/#!/coffeencoke/mince_migrator)
4
4
 
5
- Mince Migrator is a library that provides a way to run database migrations for your application using the [Mince libraries](https://github.com/coffeencoke/mince).
5
+ Mince Migrator is a library that provides a way to run database
6
+ migrations for your ruby applications.
6
7
 
7
8
  ## Installation
8
9
 
9
- Add this line to your application's Gemfile:
10
-
11
- gem 'mince_migrator'
12
-
13
- And then execute:
14
-
15
- $ bundle install
16
-
17
- Or install it yourself as:
18
-
19
10
  $ gem install mince_migrator
20
11
 
21
12
  ## Usage
22
13
 
23
14
  ### Generate a migration
24
15
 
25
- $ bundle exec mince_migrator create "Load admin users"
16
+ $ mince_migrator create "Load admin users"
26
17
 
27
18
  ### Implement your migration
28
19
 
29
- The template provided to you for a migration looks like this:
30
-
31
- ```ruby
32
- module MinceMigrator
33
- module Migration
34
- module LoadAdminUsers
35
- def self.run
36
- # Actual migration goes here
37
- end
38
-
39
- def self.revert
40
- # In case you need to revert this one migration
41
- end
42
-
43
- # So you can change the order to run more easily
44
- def self.time_created
45
- "2013-02-23 19:03:27 UTC"
46
- end
47
-
48
- module Temporary
49
- # Migration dependent classes go here
50
- end
51
- end
52
- end
53
- end
54
- ```
55
-
56
- For an evolving explanation for the Temporary module, view [Migrating With Temporary Classes](https://github.com/coffeencoke/mince_migrator/wiki/migrating-with-temporary-classes).
20
+ The output of creating the migration will tell you where the migration was created.
21
+
22
+ Implement the `run` method with the behavior of your migration.
57
23
 
58
24
  ### Run your single migration
59
25
 
60
- $ bundle exec mince_migrator run "Load admin users"
26
+ $ mince_migrator run "Load admin users"
61
27
 
62
- *You can use any of the following as the name for the above command:*
63
-
64
- * `Load admin users` (in quotes)*
65
- * `LoadAdminUsers`
66
- * `load_admin_users`
67
-
68
28
  ### Revert your single migration
69
29
 
70
- $ bundle exec mince_migrator revert "Load admin users"
30
+ $ mince_migrator revert "Load admin users"
71
31
 
72
32
  ### Run all migrations that have not yet been ran
73
33
 
74
- $ bundle exec mince_migrator run_all
75
-
34
+ $ mince_migrator run_all
35
+
36
+ ### View all migrations
37
+
38
+ *My favorite!*
39
+
40
+ $ mince_migrator list
41
+
76
42
  ### Check the status of a migration
77
43
 
78
- $ bundle exec mince_migrator status "Load admin users"
44
+ $ mince_migrator show "Load admin users"
79
45
 
80
46
  ### View all available commands
81
47
 
82
- *Run any of the following*
83
-
84
- $ bundle exec mince_migrator
85
- $ bundle exec mince_migrator --help
86
- $ bundle exec mince_migrator help
48
+ $ mince_migrator --help
87
49
 
88
50
  ## Contributing
89
51
 
@@ -108,4 +70,4 @@ Copyright (c) 2013 Matt Simpson
108
70
 
109
71
  MIT License
110
72
 
111
- View the LICENSE.txt file included in the source
73
+ View the LICENSE.txt file included in the source
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env ruby
2
+ require 'gli'
3
+ require 'mince_migrator/version'
4
+ require 'mince_migrator/list'
5
+ require 'mince_migrator/cli_helper'
6
+
7
+ include GLI::App
8
+ include MinceMigrator::CliHelper
9
+
10
+ program_desc 'Manages database migrations for ruby applications'
11
+ version MinceMigrator.version
12
+
13
+ desc 'Creates a migration'
14
+ arg_name 'Name of migration'
15
+ command :create do |c|
16
+ c.action do |global_options,options,args|
17
+ name = args.join(" ")
18
+ create_migration(name)
19
+ end
20
+ end
21
+
22
+ desc 'Lists all migrations and their statuses'
23
+ command :list do |c|
24
+ c.action do |global_options,options,args|
25
+ list_migrations(MinceMigrator::List.new)
26
+ end
27
+ end
28
+
29
+ desc 'Reverts a migration'
30
+ arg_name 'Name of migration'
31
+ command :revert do |c|
32
+ c.action do |global_options,options,args|
33
+ name = args.join(" ")
34
+ revert_migration(name: name)
35
+ end
36
+ end
37
+
38
+ desc 'Deletes all migrations that have already ran'
39
+ command :delete_ran do |c|
40
+ c.action do |global_options,options,args|
41
+ list = MinceMigrator::List.new('ran')
42
+ if list.all.any?
43
+ list.all.each do |migration|
44
+ delete_migration(migration: migration)
45
+ end
46
+ else
47
+ puts "No migrations were found."
48
+ end
49
+ end
50
+ end
51
+
52
+ desc 'Deletes a migration'
53
+ arg_name 'Name of migration'
54
+ command :delete do |c|
55
+ c.action do |global_options,options,args|
56
+ name = args.join(" ")
57
+
58
+ delete_migration(name: name)
59
+ end
60
+ end
61
+
62
+ desc 'Runs a migration'
63
+ arg_name 'Name of migration'
64
+ command :run do |c|
65
+ c.action do |global_options,options,args|
66
+ name = args.join(" ")
67
+
68
+ run_migration(name: name)
69
+ end
70
+ end
71
+
72
+ desc 'Runs all migrations that have not yet been ran'
73
+ command :run_all do |c|
74
+ c.action do
75
+ list = MinceMigrator::List.new('not ran')
76
+ if list.all.any?
77
+ list.all.each do |migration|
78
+ run_migration(migration: migration)
79
+ end
80
+ else
81
+ puts "There were no migrations to run."
82
+ end
83
+ end
84
+ end
85
+
86
+ desc 'Shows the details of a migration'
87
+ arg_name 'Name of migration'
88
+ command :show do |c|
89
+ c.action do |global_options,options,args|
90
+ name = args.join(" ")
91
+
92
+ if args.any?
93
+ show_migration(name)
94
+ else
95
+ puts "You must provide a name"
96
+ end
97
+ end
98
+ end
99
+
100
+ pre do |global,command,options,args|
101
+ # Pre logic here
102
+ # Return true to proceed; false to abort and not call the
103
+ # chosen command
104
+ # Use skips_pre before a command to skip this block
105
+ # on that command only
106
+ if File.exists?(MinceMigrator::Config.config_file)
107
+ require MinceMigrator::Config.config_file
108
+ end
109
+
110
+ true
111
+ end
112
+
113
+ post do |global,command,options,args|
114
+ # Post logic here
115
+ # Use skips_post before a command to skip this
116
+ # block on that command only
117
+ end
118
+
119
+ on_error do |exception|
120
+ # Error logic here
121
+ # return false to skip default error handling
122
+ true
123
+ end
124
+
125
+ exit run(ARGV)
@@ -0,0 +1,72 @@
1
+ module MinceMigrator
2
+ require_relative 'deleter'
3
+ require_relative 'creator'
4
+ require_relative 'reverter'
5
+ require_relative 'list'
6
+ require_relative 'migrations/runner'
7
+ require_relative 'list_report'
8
+
9
+ module CliHelper
10
+ def delete_migration(options)
11
+ deleter = MinceMigrator::Deleter.new(options)
12
+ if deleter.can_delete_migration?
13
+ puts "Deleting #{deleter.name}..."
14
+ deleter.delete_migration
15
+ puts "Migration deleted successfully"
16
+ else
17
+ help_now!(deleter.reasons_for_failure)
18
+ end
19
+ end
20
+
21
+ def run_migration(options)
22
+ runner = MinceMigrator::Migrations::Runner.new(options)
23
+ if runner.can_run_migration?
24
+ puts "Running #{runner.name}..."
25
+ runner.run_migration
26
+ puts "Migration finished."
27
+ else
28
+ help_now!(runner.reasons_for_failure)
29
+ end
30
+ end
31
+
32
+ def create_migration(name)
33
+ creator = MinceMigrator::Creator.new(name)
34
+ if creator.can_create_migration?
35
+ puts "Creating #{creator.name}..."
36
+ creator.create_migration
37
+ puts "Migration created at #{creator.migration_file_relative_path}"
38
+ else
39
+ help_now!(creator.reasons_for_failure)
40
+ end
41
+ end
42
+
43
+ def list_migrations(list)
44
+ if list.all.any?
45
+ MinceMigrator::ListReport.new(list).run
46
+ else
47
+ puts "\nThere are no migrations in the '#{MinceMigrator::Config.migration_relative_dir}' directory.\n".red
48
+ puts "run the following for more info to create a migration:\n\n"
49
+ puts " mince_migrator create --help\n\n".green
50
+ end
51
+ end
52
+
53
+ def revert_migration(options)
54
+ reverter = MinceMigrator::Reverter.new(options)
55
+ if reverter.can_revert_migration?
56
+ puts "Reverting #{reverter.name}..."
57
+ reverter.revert_migration
58
+ puts "Migration reverted successfully"
59
+ else
60
+ help_now!(reverter.reasons_for_failure)
61
+ end
62
+ end
63
+
64
+ def show_migration(name)
65
+ if migration = MinceMigrator::Migration.find(name)
66
+ MinceMigrator::StatusReport.new(migration).run
67
+ else
68
+ puts "No migration was found with name: '#{name}'"
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,30 @@
1
+ module MinceMigrator
2
+ require 'singleton'
3
+
4
+ class Config
5
+ include Singleton
6
+
7
+ attr_reader :migration_relative_dir, :migration_dir
8
+
9
+ def initialize
10
+ self.migration_relative_dir = ::File.join("db", "migrations")
11
+ end
12
+
13
+ def migration_relative_dir=(val)
14
+ @migration_relative_dir = val
15
+ @migration_dir = ::File.join(Dir.pwd, migration_relative_dir)
16
+ end
17
+
18
+ def self.migration_relative_dir
19
+ instance.migration_relative_dir
20
+ end
21
+
22
+ def self.migration_dir
23
+ instance.migration_dir
24
+ end
25
+
26
+ def self.config_file
27
+ ::File.join(Dir.pwd, 'config', 'mince_migrator.rb')
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,48 @@
1
+ module MinceMigrator
2
+ require 'fileutils'
3
+ require_relative 'migrations/versioned_file'
4
+ require_relative 'migrations/name'
5
+ require_relative 'config'
6
+
7
+ class Creator
8
+ attr_reader :name, :migration_name
9
+
10
+ def initialize(name=nil)
11
+ @migration_name = Migrations::Name.new(name)
12
+ @name = migration_name.value
13
+ end
14
+
15
+ def can_create_migration?
16
+ migration_name.valid?
17
+ end
18
+
19
+ def reasons_for_failure
20
+ migration_name.reasons_for_failure
21
+ end
22
+
23
+ def create_migration
24
+ FileUtils.mkdir_p(Config.migration_dir)
25
+ file = ::File.open(migration_file.full_path, 'w+')
26
+ file.write migration_file.body
27
+ file.close
28
+ end
29
+
30
+ def migration_file
31
+ @migration_file ||= versioned_file.next_unused_version
32
+ end
33
+
34
+ def versioned_file
35
+ @versioned_file ||= Migrations::VersionedFile.new(name)
36
+ end
37
+
38
+ def migration_file_relative_path
39
+ migration_file.full_relative_path
40
+ end
41
+
42
+ def self.create(name)
43
+ new(name).tap do |creator|
44
+ creator.create_migration if creator.can_create_migration?
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,42 @@
1
+ module MinceMigrator
2
+ require_relative 'config'
3
+ require 'fileutils'
4
+ require_relative 'migrations/name'
5
+ require_relative 'ran_migration'
6
+
7
+ class Deleter
8
+ attr_reader :name, :filename, :migration_name, :migration
9
+
10
+ def initialize(options)
11
+ if options[:migration]
12
+ @migration = options[:migration]
13
+ @migration_name = Migrations::Name.new(migration.name)
14
+ elsif options[:name]
15
+ @migration_name = Migrations::Name.new(options[:name])
16
+ end
17
+ @name = migration_name.value
18
+ @filename = migration_name.filename
19
+ end
20
+
21
+ def delete_migration
22
+ ::FileUtils.rm(migration_path)
23
+ ran_migration.delete if ran_migration
24
+ end
25
+
26
+ def can_delete_migration?
27
+ ::File.exists?(migration_path)
28
+ end
29
+
30
+ def reasons_for_failure
31
+ "Migration does not exist with name '#{name}'" unless can_delete_migration?
32
+ end
33
+
34
+ def migration_path
35
+ ::File.join Config.migration_dir, filename
36
+ end
37
+
38
+ def ran_migration
39
+ @ran_migration ||= RanMigration.find_by_name(name)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,46 @@
1
+ module MinceMigrator
2
+ require_relative 'config'
3
+ require_relative 'migration'
4
+
5
+ class List
6
+ attr_reader :status
7
+
8
+ def initialize(status=:any)
9
+ @status = status
10
+ end
11
+
12
+ def all
13
+ @all ||= all_for_status
14
+ end
15
+
16
+ def number_of_migrations
17
+ all.size
18
+ end
19
+
20
+ def filelist
21
+ @filelist ||= Dir.glob(filelist_pattern)
22
+ end
23
+
24
+ def filelist_pattern
25
+ File.join(Config.migration_dir, '*')
26
+ end
27
+
28
+ private
29
+
30
+ def all_for_status
31
+ filtered_by_status(all_for_any_status).sort_by(&:time_created)
32
+ end
33
+
34
+ def all_for_any_status
35
+ filelist.map{|a| Migration.load_from_file(a) }
36
+ end
37
+
38
+ def filtered_by_status(migrations)
39
+ migrations.select { |m| matches_status?(m) }
40
+ end
41
+
42
+ def matches_status?(migration)
43
+ status == :any || migration.status == status
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,67 @@
1
+ module MinceMigrator
2
+ require 'command_line_reporter'
3
+
4
+ class ListReport
5
+ include CommandLineReporter
6
+
7
+ attr_reader :sum_ran, :sum_not_ran
8
+
9
+ def initialize(list)
10
+ @list = list
11
+ @sum_ran = 0
12
+ @sum_not_ran = 0
13
+ end
14
+
15
+ def run
16
+ vertical_spacing 2
17
+ header title: 'List Of All Migrations', bold: true, rule: true, align: 'center', width: 70, timestamp: true
18
+
19
+ table border: false do
20
+ row header: true do
21
+ column 'Name', width: 40
22
+ column 'Status', width: 10
23
+ column 'Age', width: 5
24
+ column 'Date Created', width: 15
25
+ end
26
+ @list.all.each do |migration|
27
+ row do
28
+ column migration.name
29
+ status_column(migration)
30
+ column migration.age
31
+ column migration.time_created.strftime("%m/%d/%Y")
32
+ end
33
+ end
34
+ end
35
+
36
+ vertical_spacing 2
37
+
38
+ table border: false do
39
+ row do
40
+ column "Ran", width: 6
41
+ column sum_ran, color: 'green', width: 5
42
+ column "Not ran", width: 8
43
+ column sum_not_ran, color: 'red', width: 5
44
+ end
45
+ row bold: true do
46
+ column "Total", width: 6
47
+ column total_number_of_migrations, width: 5
48
+ end
49
+ end
50
+ end
51
+
52
+ def total_number_of_migrations
53
+ sum_ran + sum_not_ran
54
+ end
55
+
56
+ def status_column(migration)
57
+ if migration.ran?
58
+ @sum_ran += 1
59
+ color = 'green'
60
+ else
61
+ @sum_not_ran += 1
62
+ color = 'red'
63
+ end
64
+ column migration.status, color: color
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,66 @@
1
+ require 'debugger'
2
+
3
+ module MinceMigrator
4
+ require_relative 'migrations/file'
5
+ require_relative 'ran_migration'
6
+ require_relative 'migrations/name'
7
+
8
+ class Migration
9
+ attr_reader :time_created, :name, :status, :relative_path, :path
10
+
11
+ def initialize(options)
12
+ @klass = options[:klass]
13
+ @time_created = @klass.time_created
14
+ self.name = options[:name]
15
+ @relative_path = options[:relative_path]
16
+ @path = options[:path]
17
+ end
18
+
19
+ def age
20
+ @age ||= "#{(Time.now.utc.to_date - time_created.to_date).to_i}d"
21
+ end
22
+
23
+ def name=(val)
24
+ @name ||= Migrations::Name.new(val).value
25
+ end
26
+
27
+ def run
28
+ @klass.run
29
+ end
30
+
31
+ def revert
32
+ @klass.revert
33
+ end
34
+
35
+ def ran?
36
+ !!ran_migration
37
+ end
38
+
39
+ def status
40
+ ran? ? 'ran' : 'not ran'
41
+ end
42
+
43
+ def ran_migration
44
+ @ran_migration ||= RanMigration.find_by_name(name)
45
+ end
46
+
47
+ def self.find(name)
48
+ if file = Migrations::File.find(name)
49
+ new_from_file file
50
+ end
51
+ end
52
+
53
+ def self.load_from_file(path_to_file)
54
+ new_from_file Migrations::File.load_from_file(path_to_file)
55
+ end
56
+
57
+ def self.new_from_file(file)
58
+ new(
59
+ klass: file.klass,
60
+ name: file.name,
61
+ relative_path: file.full_relative_path,
62
+ path: file.full_path
63
+ )
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,74 @@
1
+ module MinceMigrator
2
+ module Migrations
3
+ require_relative 'template'
4
+ require_relative '../config'
5
+ require_relative 'versioned_file'
6
+ require_relative 'loader'
7
+
8
+ class File
9
+ attr_reader :name
10
+
11
+ def initialize(name)
12
+ @original_name = name
13
+ @version = 1
14
+ self.name = name
15
+ end
16
+
17
+ def full_relative_path
18
+ ::File.join(Config.migration_relative_dir, filename)
19
+ end
20
+
21
+ def name=(val)
22
+ @name = val.gsub(" ", "_").downcase
23
+ end
24
+
25
+ def filename
26
+ "#{name}.rb"
27
+ end
28
+
29
+ def full_path
30
+ ::File.join Config.migration_dir, filename
31
+ end
32
+
33
+ def klass_name
34
+ name.split("_").map(&:capitalize).join
35
+ end
36
+
37
+ def klass
38
+ loader.klass
39
+ end
40
+
41
+ def body
42
+ @body ||= Template.new(klass_name).render
43
+ end
44
+
45
+ def persisted?
46
+ ::File.exists?(full_path)
47
+ end
48
+
49
+ def load
50
+ loader.call
51
+ end
52
+
53
+ def self.load_from_file(path_to_file)
54
+ name = convert_path_to_name(path_to_file)
55
+ find(name)
56
+ end
57
+
58
+ def self.find(name)
59
+ file = new(name)
60
+ file.tap(&:load) if file.persisted?
61
+ end
62
+
63
+ def self.convert_path_to_name(path)
64
+ path.split("/")[-1].gsub('.rb', '')
65
+ end
66
+
67
+ private
68
+
69
+ def loader
70
+ @loader ||= Loader.new(full_path: full_path, klass_name: klass_name)
71
+ end
72
+ end
73
+ end
74
+ end