ai_refactor 0.3.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -2
  3. data/Gemfile +9 -0
  4. data/Gemfile.lock +169 -1
  5. data/README.md +169 -43
  6. data/Rakefile +1 -1
  7. data/ai_refactor.gemspec +1 -0
  8. data/examples/.gitignore +1 -0
  9. data/examples/ex1_convert_a_rspec_test_to_minitest.yml +7 -0
  10. data/examples/ex1_input_spec.rb +32 -0
  11. data/examples/rails_helper.rb +21 -0
  12. data/examples/test_helper.rb +14 -0
  13. data/exe/ai_refactor +139 -52
  14. data/lib/ai_refactor/cli.rb +138 -0
  15. data/lib/ai_refactor/command_file_parser.rb +27 -0
  16. data/lib/ai_refactor/context.rb +33 -0
  17. data/lib/ai_refactor/file_processor.rb +34 -17
  18. data/lib/ai_refactor/prompt.rb +84 -0
  19. data/lib/ai_refactor/prompts/diff.md +17 -0
  20. data/lib/ai_refactor/prompts/input.md +1 -0
  21. data/lib/ai_refactor/refactors/base_refactor.rb +183 -0
  22. data/lib/ai_refactor/refactors/custom.rb +43 -0
  23. data/lib/ai_refactor/refactors/minitest/write_test_for_class.md +15 -0
  24. data/lib/ai_refactor/refactors/minitest/write_test_for_class.rb +51 -0
  25. data/lib/ai_refactor/refactors/project/write_changelog_from_history.md +35 -0
  26. data/lib/ai_refactor/refactors/project/write_changelog_from_history.rb +50 -0
  27. data/lib/ai_refactor/refactors/{prompts/rspec_to_minitest_rails.md → rails/minitest/rspec_to_minitest.md} +40 -1
  28. data/lib/ai_refactor/refactors/rails/minitest/rspec_to_minitest.rb +77 -0
  29. data/lib/ai_refactor/refactors/rspec/minitest_to_rspec.rb +13 -0
  30. data/lib/ai_refactor/refactors/ruby/refactor_ruby.md +10 -0
  31. data/lib/ai_refactor/refactors/ruby/refactor_ruby.rb +29 -0
  32. data/lib/ai_refactor/refactors/ruby/write_ruby.md +7 -0
  33. data/lib/ai_refactor/refactors/ruby/write_ruby.rb +33 -0
  34. data/lib/ai_refactor/refactors.rb +13 -5
  35. data/lib/ai_refactor/run_configuration.rb +115 -0
  36. data/lib/ai_refactor/{refactors/tests → test_runners}/minitest_runner.rb +2 -2
  37. data/lib/ai_refactor/{refactors/tests → test_runners}/rspec_runner.rb +1 -1
  38. data/lib/ai_refactor/{refactors/tests → test_runners}/test_run_diff_report.rb +1 -1
  39. data/lib/ai_refactor/{refactors/tests → test_runners}/test_run_result.rb +1 -1
  40. data/lib/ai_refactor/version.rb +1 -1
  41. data/lib/ai_refactor.rb +13 -8
  42. metadata +47 -13
  43. data/lib/ai_refactor/base_refactor.rb +0 -66
  44. data/lib/ai_refactor/refactors/generic.rb +0 -113
  45. data/lib/ai_refactor/refactors/minitest_to_rspec.rb +0 -11
  46. data/lib/ai_refactor/refactors/rspec_to_minitest_rails.rb +0 -103
  47. /data/lib/ai_refactor/refactors/{prompts → rspec}/minitest_to_rspec.md +0 -0
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AIRefactor
4
+ module Refactors
5
+ class BaseRefactor
6
+ # All subclasses must register themselves with the Registry
7
+ def self.inherited(subclass)
8
+ super
9
+ Refactors.register(subclass)
10
+ end
11
+
12
+ def self.description
13
+ "(No description provided)"
14
+ end
15
+
16
+ def self.takes_input_files?
17
+ true
18
+ end
19
+
20
+ attr_reader :input_file, :options, :logger
21
+ attr_accessor :input_content
22
+ attr_writer :failed_message
23
+
24
+ def initialize(input_file, options, logger)
25
+ @input_file = input_file
26
+ @options = options
27
+ @logger = logger
28
+ end
29
+
30
+ def run
31
+ raise NotImplementedError
32
+ end
33
+
34
+ def failed_message
35
+ @failed_message || "Reason not specified"
36
+ end
37
+
38
+ private
39
+
40
+ def file_processor
41
+ context = ::AIRefactor::Context.new(files: options[:context_file_paths], text: options[:context_text], logger: logger)
42
+ prompt = ::AIRefactor::Prompt.new(input_content: input_content, input_path: input_file, output_file_path: output_file_path, prompt: prompt_input, context: context, logger: logger, options: options)
43
+ AIRefactor::FileProcessor.new(prompt: prompt, ai_client: ai_client, output_path: output_file_path, logger: logger, options: options)
44
+ end
45
+
46
+ def process!(strip_ticks: true)
47
+ processor = file_processor
48
+
49
+ if processor.output_exists?
50
+ return false unless overwrite_existing_output?(output_file_path)
51
+ end
52
+
53
+ logger.verbose "Processing #{input_file}..."
54
+
55
+ begin
56
+ output_content, finished_reason, usage = processor.process! do |content|
57
+ if block_given?
58
+ yield content
59
+ elsif strip_ticks
60
+ content.gsub("```ruby", "").gsub("```", "")
61
+ else
62
+ content
63
+ end
64
+ end
65
+
66
+ logger.verbose "AI finished, with reason '#{finished_reason}'..."
67
+ logger.verbose "Used tokens: #{usage["total_tokens"]}".colorize(:light_black) if usage
68
+ if finished_reason == "length"
69
+ logger.warn "Translation may contain an incomplete output as the max token length was reached. You can try using the '--continue' option next time to increase the length of generated output."
70
+ end
71
+
72
+ if !output_content || output_content.length == 0
73
+ logger.warn "Skipping #{input_file}, no translated output..."
74
+ logger.error "Failed to translate #{input_file}, finished reason #{finished_reason}"
75
+ self.failed_message = "AI conversion failed, no output was generated"
76
+ raise NoOutputError, "No output"
77
+ end
78
+
79
+ output_content
80
+ rescue => e
81
+ logger.error "Request to AI failed: #{e.message}"
82
+ puts e.backtrace
83
+ logger.warn "Skipping #{input_file}..."
84
+ self.failed_message = "Request to OpenAI failed"
85
+ raise e
86
+ end
87
+ end
88
+
89
+ def overwrite_existing_output?(output_path)
90
+ overwrite = options && options[:overwrite]&.downcase
91
+ answer = if ["y", "n"].include? overwrite
92
+ overwrite
93
+ else
94
+ logger.info "Do you wish to overwrite #{output_path}? (y/n)"
95
+ $stdin.gets.chomp.downcase
96
+ end
97
+ if answer == "y"
98
+ logger.verbose "Overwriting #{output_path}..."
99
+ return true
100
+ end
101
+ logger.warn "Skipping #{input_file}..."
102
+ self.failed_message = "Skipped as output file already exists"
103
+ false
104
+ end
105
+
106
+ def prompt_input
107
+ if options && options[:prompt]&.length&.positive?
108
+ return options[:prompt]
109
+ end
110
+
111
+ file = if options && options[:prompt_file_path]&.length&.positive?
112
+ options[:prompt_file_path]
113
+ else
114
+ location = Module.const_source_location(::AIRefactor::Refactors::BaseRefactor.name)
115
+ File.join(File.dirname(location.first), "#{refactor_name}.md")
116
+ end
117
+ file.tap do |prompt|
118
+ raise "No prompt file '#{prompt}' found for #{refactor_name}" unless File.exist?(prompt)
119
+ end
120
+
121
+ File.read(file)
122
+ end
123
+
124
+ def output_file_path
125
+ @output_file_path ||= determine_output_file_path
126
+ end
127
+
128
+ def determine_output_file_path
129
+ return output_file_path_from_template if output_template_path
130
+
131
+ path = options[:output_file_path]
132
+ return default_output_path unless path
133
+
134
+ if path == true
135
+ input_file
136
+ else
137
+ path
138
+ end
139
+ end
140
+
141
+ def default_output_path
142
+ nil
143
+ end
144
+
145
+ def output_template_path
146
+ options[:output_template_path]
147
+ end
148
+
149
+ def output_file_path_from_template
150
+ path = output_template_path.gsub("[FILE]", File.basename(input_file))
151
+ .gsub("[NAME]", File.basename(input_file, ".*"))
152
+ .gsub("[DIR]", File.dirname(input_file))
153
+ .gsub("[REFACTOR]", self.class.refactor_name)
154
+ .gsub("[EXT]", File.extname(input_file))
155
+ raise "Output template could not be used" unless path.length.positive?
156
+ path
157
+ end
158
+
159
+ def ai_client
160
+ @ai_client ||= OpenAI::Client.new
161
+ end
162
+
163
+ def refactor_name
164
+ self.class.refactor_name
165
+ end
166
+
167
+ class << self
168
+ def command_line_options
169
+ []
170
+ end
171
+
172
+ def refactor_name
173
+ name.gsub("AIRefactor::Refactors::", "")
174
+ .gsub(/::/, "/")
175
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
176
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
177
+ .tr("-", "_")
178
+ .downcase
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AIRefactor
4
+ module Refactors
5
+ class Custom < BaseRefactor
6
+ def run
7
+ logger.verbose "Custom refactor to #{input_file}... (using user supplied prompt #{prompt_file_path})"
8
+ logger.verbose "Write output to #{output_file_path}..." if output_file_path
9
+
10
+ begin
11
+ output_content = process!(strip_ticks: false)
12
+ rescue => e
13
+ logger.error "Failed to process #{input_file}: #{e.message}"
14
+ return false
15
+ end
16
+
17
+ return false unless output_content
18
+
19
+ output_file_path ? true : output_content
20
+ end
21
+
22
+ def self.description
23
+ "Generic refactor using user supplied prompt"
24
+ end
25
+
26
+ private
27
+
28
+ def prompt_file_path
29
+ specified_prompt_path = options[:prompt_file_path]
30
+ if specified_prompt_path&.length&.positive?
31
+ if File.exist?(specified_prompt_path)
32
+ return specified_prompt_path
33
+ else
34
+ logger.error "No prompt file '#{specified_prompt_path}' found"
35
+ end
36
+ else
37
+ logger.error "No prompt file was specified!"
38
+ end
39
+ exit 1
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,15 @@
1
+ You are an expert Ruby senior software developer. Write a unit test for the class show below, using minitest and minitest/mock if necessary.
2
+ Test 100% of the code.
3
+
4
+ The path to the file to test is: __{{input_file_path}}__
5
+ The output file path is: __{{output_file_path}}__
6
+
7
+ __{{prompt_header}}__
8
+
9
+ Only show me the test file code. Do NOT provide any other description of your work. Always enclose the output code in triple backticks (```).
10
+
11
+ __{{context}}__
12
+
13
+ __{{prompt_footer}}__
14
+
15
+ The class to test is:
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AIRefactor
4
+ module Refactors
5
+ module Minitest
6
+ class WriteTestForClass < BaseRefactor
7
+ def run
8
+ logger.verbose "'Write minitest test' refactor for #{input_file}..."
9
+ logger.verbose "Write output to #{output_file_path}..." if output_file_path
10
+
11
+ begin
12
+ output_content = process!
13
+ rescue => e
14
+ logger.error "Failed to process #{input_file}: #{e.message}"
15
+ return false
16
+ end
17
+
18
+ return false unless output_file_path
19
+
20
+ logger.verbose "Generated #{output_file_path} from #{input_file} ..." if output_content
21
+
22
+ minitest_runner = AIRefactor::TestRunners::MinitestRunner.new(output_file_path, command_template: "bundle exec ruby __FILE__")
23
+
24
+ logger.verbose "Run generated test file #{output_file_path} (#{minitest_runner.command})..."
25
+ test_run = minitest_runner.run
26
+
27
+ if test_run.failed?
28
+ logger.warn "#{input_file} was translated to #{output_file_path} but the resulting test is failing..."
29
+ logger.error "Failed to run test, exited with status #{test_run.exitstatus}. Stdout: #{test_run.stdout}\n\nStderr: #{test_run.stderr}\n\n"
30
+ logger.error "New test failed!", bold: true
31
+ self.failed_message = "Generated test file failed to run correctly"
32
+ return false
33
+ end
34
+
35
+ logger.verbose "\nNew test file ran and returned the following results:"
36
+ logger.verbose ">> Runs: #{test_run.example_count}, Failures: #{test_run.failure_count}, Skips: #{test_run.pending_count}\n"
37
+
38
+ output_file_path ? true : output_content
39
+ end
40
+
41
+ def self.description
42
+ "Write a minitest test for a class"
43
+ end
44
+
45
+ def default_output_path
46
+ File.join("test", input_file.gsub(/\.rb$/, "_test.rb"))
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,35 @@
1
+ You are a senior Ruby software developer. You are diligent about writing clear changelogs for your users.
2
+
3
+ You have a git history of changes to your project. You want to write a changelog from this history.
4
+
5
+ The format of the changelog you will create entries for is from keepachangelog.com version 1.0.0.
6
+
7
+ Here is an example of a set of changelog entries:
8
+
9
+ ```
10
+ ## [0.2.0] - 2015-10-06
11
+
12
+ ### Changed
13
+
14
+ - Remove exclusionary mentions of "open source" since this project can
15
+ benefit both "open" and "closed" source projects equally.
16
+
17
+ ## [0.1.0] - 2015-10-06
18
+
19
+ ### Added
20
+
21
+ - Answer "Should you ever rewrite a change log?".
22
+
23
+ ### Changed
24
+
25
+ - Improve argument against commit logs.
26
+ - Start following [SemVer](https://semver.org) properly.
27
+
28
+ ### Fixed
29
+
30
+ - Fix typos in recent CHANGELOG entries.
31
+ ```
32
+
33
+ If the history contains any mention of changes of version (such as "Bump version to 1.0.0"), then you should use that version number in a new changelog entry.
34
+
35
+ Now write changelog entries from the following git history.
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AIRefactor
4
+ module Refactors
5
+ module Project
6
+ class WriteChangelogFromHistory < BaseRefactor
7
+ def self.description
8
+ "Write changelog entries from the git history"
9
+ end
10
+
11
+ def self.takes_input_files?
12
+ false
13
+ end
14
+
15
+ def run
16
+ logger.verbose "Creating changelog entries for project from #{options[:git_commit_count] || 3} commits..."
17
+ logger.verbose "Write output to #{output_file_path}..." if output_file_path
18
+
19
+ self.input_content = `git log -#{options[:git_commit_count] || 3} --pretty=format:"%ci %d %s"`.split("\n").map { |line| "- #{line}" }.join("\n")
20
+ logger.debug "\nInput messages: \n#{input_content}\n\n"
21
+ begin
22
+ output_content = process!(strip_ticks: false)
23
+ rescue => e
24
+ logger.error "Failed to process: #{e.message}"
25
+ return false
26
+ end
27
+
28
+ return false unless output_content
29
+
30
+ output_file_path ? true : output_content
31
+ end
32
+
33
+ def default_output_path
34
+ nil
35
+ end
36
+
37
+ def self.command_line_options
38
+ [
39
+ {
40
+ key: :git_commit_count,
41
+ long: "--commits N",
42
+ type: Integer,
43
+ help: "The number of commits to analyse when creating the changelog entries (defaults to 3)"
44
+ }
45
+ ]
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -1,8 +1,8 @@
1
+ You are an expert software developer.
1
2
  You convert RSpec tests to ActiveSupport::TestCase tests for Ruby on Rails.
2
3
  ActiveSupport::TestCase uses MiniTest under the hood.
3
4
  Remember that MiniTest does not support `context` blocks, instead these should be removed and the context
4
5
  specified in them should be moved directly into the relevant tests.
5
- Always enclose the output code in triple backticks (```).
6
6
 
7
7
  Here are some examples to use as a guide:
8
8
 
@@ -42,6 +42,10 @@ subject(:model) { create(:order_state) }
42
42
  context "when rejected" do
43
43
  before { model.rejected_at = 1.day.ago }
44
44
 
45
+ it "should be not valid" do
46
+ expect(model).not_to be_valid
47
+ end
48
+
45
49
  context "with reason and message" do
46
50
  before do
47
51
  model.rejected_message = reason
@@ -68,6 +72,11 @@ setup do
68
72
  @reason = "my reason"
69
73
  end
70
74
 
75
+ test "when rejected, model should be not valid" do
76
+ @model.rejected_at = 1.day.ago
77
+ refute @model.valid?
78
+ end
79
+
71
80
  test "when rejected, with reason and message, model should be valid" do
72
81
  @model.rejected_at = 1.day.ago
73
82
  @model.rejected_message = @reason
@@ -231,3 +240,33 @@ test "stubs any instance" do
231
240
  end
232
241
  end
233
242
  ```
243
+
244
+ Example 9) RSpec:
245
+ ```
246
+ assert_association @model, :message_thread, :belongs_to
247
+ ```
248
+
249
+ Result 9) minitest:
250
+
251
+ ```
252
+ assert_instance_of MessageThread, @model.message_thread
253
+ ```
254
+
255
+ Example 10) RSpec:
256
+ ```
257
+ assert_association @model, :message_thread, :belongs_to, optional: true
258
+ ```
259
+
260
+ Result 10) minitest:
261
+ ```
262
+ assoc = @model.reflect_on_association(:message_thread)
263
+ refute assoc.nil?, "no association :message_thread"
264
+ assert_equal :belongs_to, assoc.macro
265
+ assert assoc.options[:optional]
266
+ ```
267
+
268
+ __{{context}}__
269
+
270
+ Only output the refactored class. Do NOT provide any other description of your work. Always enclose the output code in triple backticks (```).
271
+
272
+ Convert this Rspec file to minitest:
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AIRefactor
4
+ module Refactors
5
+ module Rails
6
+ module Minitest
7
+ class RspecToMinitest < BaseRefactor
8
+ def run
9
+ spec_runner = AIRefactor::TestRunners::RSpecRunner.new(input_file, command_template: options.rspec_run_command)
10
+ logger.verbose "Run spec #{input_file}... (#{spec_runner.command})"
11
+
12
+ spec_run = spec_runner.run
13
+
14
+ if spec_run.failed?
15
+ logger.warn "Skipping #{input_file}..."
16
+ logger.error "Failed to run #{input_file}, exited with status #{spec_run.exitstatus}. Stdout: #{spec_run.stdout}\n\nStderr: #{spec_run.stderr}\n\n"
17
+ self.failed_message = "Failed to run RSpec file, has errors"
18
+ return false
19
+ end
20
+
21
+ logger.debug "\nOriginal test run results:"
22
+ logger.debug ">> Examples: #{spec_run.example_count}, Failures: #{spec_run.failure_count}, Pendings: #{spec_run.pending_count}\n"
23
+
24
+ begin
25
+ result = process!
26
+ rescue AIRefactor::NoOutputError => _e
27
+ return false
28
+ rescue => e
29
+ logger.error "Failed to convert #{input_file} to Minitest, error: #{e.message}"
30
+ return false
31
+ end
32
+
33
+ logger.verbose "Converted #{input_file} to #{output_file_path}..." if result
34
+
35
+ minitest_runner = AIRefactor::TestRunners::MinitestRunner.new(output_file_path, command_template: options.minitest_run_command)
36
+
37
+ logger.verbose "Run generated test file #{output_file_path} (#{minitest_runner.command})..."
38
+ test_run = minitest_runner.run
39
+
40
+ if test_run.failed?
41
+ logger.warn "Skipping #{input_file}..."
42
+ logger.error "Failed to run translated #{output_file_path}, exited with status #{test_run.exitstatus}. Stdout: #{test_run.stdout}\n\nStderr: #{test_run.stderr}\n\n"
43
+ logger.error "Conversion failed!", bold: true
44
+ self.failed_message = "Generated test file failed to run correctly"
45
+ return false
46
+ end
47
+
48
+ logger.debug "\nTranslated test file results:"
49
+ logger.debug ">> Runs: #{test_run.example_count}, Failures: #{test_run.failure_count}, Skips: #{test_run.pending_count}\n"
50
+
51
+ report = AIRefactor::TestRunners::TestRunDiffReport.new(spec_run, test_run)
52
+
53
+ if report.no_differences?
54
+ logger.verbose "Done converting #{input_file} to #{output_file_path}..."
55
+ logger.success "\nNo differences found! Conversion worked!"
56
+ true
57
+ else
58
+ logger.warn report.diff.colorize(:yellow)
59
+ logger.verbose "Done converting #{input_file} to #{output_file_path}..."
60
+ logger.error "\nDifferences found! Conversion failed!", bold: true
61
+ self.failed_message = "Generated test file run output did not match original RSpec spec run output"
62
+ false
63
+ end
64
+ end
65
+
66
+ def self.description
67
+ "Convert RSpec file to Minitest (for Rails apps)"
68
+ end
69
+
70
+ def default_output_path
71
+ input_file.gsub("_spec.rb", "_test.rb").gsub("spec/", "test/")
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AIRefactor
4
+ module Refactors
5
+ module Rspec
6
+ class MinitestToRspec < BaseRefactor
7
+ def run
8
+ raise "Not implemented"
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,10 @@
1
+ __{{context}}__
2
+
3
+ __{{prompt_header}}__
4
+
5
+ The input file is: __{{input_file_path}}__
6
+ The output file path is: __{{output_file_path}}__
7
+
8
+ __{{content}}__
9
+
10
+ __{{prompt_footer}}__
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AIRefactor
4
+ module Refactors
5
+ module Ruby
6
+ class RefactorRuby < Custom
7
+ def run
8
+ logger.verbose "Custom refactor to #{input_file}... (using user supplied prompt #{prompt_file_path})"
9
+ logger.verbose "Write output to #{output_file_path}..." if output_file_path
10
+
11
+ begin
12
+ output_content = process!(strip_ticks: true)
13
+ rescue => e
14
+ logger.error "Failed to process #{input_file}: #{e.message}"
15
+ return false
16
+ end
17
+
18
+ return false unless output_content
19
+
20
+ output_file_path ? true : output_content
21
+ end
22
+
23
+ def self.description
24
+ "Generic refactor using user supplied prompt (assumes Ruby code generation)"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,7 @@
1
+ __{{context}}__
2
+
3
+ __{{prompt_header}}__
4
+
5
+ __{{content}}__
6
+
7
+ __{{prompt_footer}}__
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AIRefactor
4
+ module Refactors
5
+ module Ruby
6
+ class WriteRuby < Custom
7
+ def run
8
+ logger.verbose "Write some ruby code... (using user supplied prompt #{prompt_file_path})"
9
+ logger.verbose "Write output to #{output_file_path}..." if output_file_path
10
+
11
+ begin
12
+ output_content = process!(strip_ticks: true)
13
+ rescue => e
14
+ logger.error "Failed to process #{input_file}: #{e.message}"
15
+ return false
16
+ end
17
+
18
+ return false unless output_content
19
+
20
+ output_file_path ? true : output_content
21
+ end
22
+
23
+ def self.takes_input_files?
24
+ false
25
+ end
26
+
27
+ def self.description
28
+ "User supplied prompt to write Ruby code"
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -2,6 +2,11 @@
2
2
 
3
3
  module AIRefactor
4
4
  module Refactors
5
+ def register(klass)
6
+ all[klass.refactor_name] = klass
7
+ end
8
+ module_function :register
9
+
5
10
  def get(name)
6
11
  all[name]
7
12
  end
@@ -12,16 +17,19 @@ module AIRefactor
12
17
  end
13
18
  module_function :names
14
19
 
15
- def all
16
- @all ||= constants.map { |n| const_get(n) }.select { |c| c.is_a? Class }.each_with_object({}) do |klass, hash|
17
- hash[klass.refactor_name] = klass
18
- end
20
+ def descriptions
21
+ names.map { |n| "\"#{n}\"" }.zip(all.values.map(&:description)).to_h
19
22
  end
20
- module_function :all
23
+ module_function :descriptions
21
24
 
22
25
  def supported?(name)
23
26
  names.include?(name)
24
27
  end
25
28
  module_function :supported?
29
+
30
+ def all
31
+ @all ||= {}
32
+ end
33
+ module_function :all
26
34
  end
27
35
  end