gorails 0.1.0 → 0.1.3

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -1
  3. data/Gemfile +3 -1
  4. data/Gemfile.lock +65 -0
  5. data/README.md +41 -12
  6. data/bin/update-deps +95 -0
  7. data/exe/gorails +18 -0
  8. data/gorails.gemspec +4 -3
  9. data/lib/gorails/commands/episodes.rb +25 -0
  10. data/lib/gorails/commands/example.rb +19 -0
  11. data/lib/gorails/commands/help.rb +21 -0
  12. data/lib/gorails/commands/jobs.rb +25 -0
  13. data/lib/gorails/commands/jumpstart.rb +29 -0
  14. data/lib/gorails/commands/railsbytes.rb +67 -0
  15. data/lib/gorails/commands.rb +19 -0
  16. data/lib/gorails/entry_point.rb +10 -0
  17. data/lib/gorails/version.rb +1 -1
  18. data/lib/gorails.rb +22 -1
  19. data/vendor/deps/cli-kit/REVISION +1 -0
  20. data/vendor/deps/cli-kit/lib/cli/kit/args/definition.rb +301 -0
  21. data/vendor/deps/cli-kit/lib/cli/kit/args/evaluation.rb +237 -0
  22. data/vendor/deps/cli-kit/lib/cli/kit/args/parser/node.rb +131 -0
  23. data/vendor/deps/cli-kit/lib/cli/kit/args/parser.rb +128 -0
  24. data/vendor/deps/cli-kit/lib/cli/kit/args/tokenizer.rb +132 -0
  25. data/vendor/deps/cli-kit/lib/cli/kit/args.rb +15 -0
  26. data/vendor/deps/cli-kit/lib/cli/kit/base_command.rb +29 -0
  27. data/vendor/deps/cli-kit/lib/cli/kit/command_help.rb +256 -0
  28. data/vendor/deps/cli-kit/lib/cli/kit/command_registry.rb +141 -0
  29. data/vendor/deps/cli-kit/lib/cli/kit/config.rb +137 -0
  30. data/vendor/deps/cli-kit/lib/cli/kit/core_ext.rb +30 -0
  31. data/vendor/deps/cli-kit/lib/cli/kit/error_handler.rb +165 -0
  32. data/vendor/deps/cli-kit/lib/cli/kit/executor.rb +99 -0
  33. data/vendor/deps/cli-kit/lib/cli/kit/ini.rb +94 -0
  34. data/vendor/deps/cli-kit/lib/cli/kit/levenshtein.rb +89 -0
  35. data/vendor/deps/cli-kit/lib/cli/kit/logger.rb +95 -0
  36. data/vendor/deps/cli-kit/lib/cli/kit/opts.rb +284 -0
  37. data/vendor/deps/cli-kit/lib/cli/kit/resolver.rb +67 -0
  38. data/vendor/deps/cli-kit/lib/cli/kit/sorbet_runtime_stub.rb +142 -0
  39. data/vendor/deps/cli-kit/lib/cli/kit/support/test_helper.rb +253 -0
  40. data/vendor/deps/cli-kit/lib/cli/kit/support.rb +10 -0
  41. data/vendor/deps/cli-kit/lib/cli/kit/system.rb +350 -0
  42. data/vendor/deps/cli-kit/lib/cli/kit/util.rb +133 -0
  43. data/vendor/deps/cli-kit/lib/cli/kit/version.rb +7 -0
  44. data/vendor/deps/cli-kit/lib/cli/kit.rb +151 -0
  45. data/vendor/deps/cli-ui/REVISION +1 -0
  46. data/vendor/deps/cli-ui/lib/cli/ui/ansi.rb +180 -0
  47. data/vendor/deps/cli-ui/lib/cli/ui/color.rb +98 -0
  48. data/vendor/deps/cli-ui/lib/cli/ui/formatter.rb +216 -0
  49. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_stack.rb +116 -0
  50. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/box.rb +176 -0
  51. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/bracket.rb +149 -0
  52. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style.rb +112 -0
  53. data/vendor/deps/cli-ui/lib/cli/ui/frame.rb +300 -0
  54. data/vendor/deps/cli-ui/lib/cli/ui/glyph.rb +92 -0
  55. data/vendor/deps/cli-ui/lib/cli/ui/os.rb +58 -0
  56. data/vendor/deps/cli-ui/lib/cli/ui/printer.rb +72 -0
  57. data/vendor/deps/cli-ui/lib/cli/ui/progress.rb +102 -0
  58. data/vendor/deps/cli-ui/lib/cli/ui/prompt/interactive_options.rb +534 -0
  59. data/vendor/deps/cli-ui/lib/cli/ui/prompt/options_handler.rb +36 -0
  60. data/vendor/deps/cli-ui/lib/cli/ui/prompt.rb +354 -0
  61. data/vendor/deps/cli-ui/lib/cli/ui/sorbet_runtime_stub.rb +143 -0
  62. data/vendor/deps/cli-ui/lib/cli/ui/spinner/async.rb +46 -0
  63. data/vendor/deps/cli-ui/lib/cli/ui/spinner/spin_group.rb +292 -0
  64. data/vendor/deps/cli-ui/lib/cli/ui/spinner.rb +82 -0
  65. data/vendor/deps/cli-ui/lib/cli/ui/stdout_router.rb +264 -0
  66. data/vendor/deps/cli-ui/lib/cli/ui/terminal.rb +53 -0
  67. data/vendor/deps/cli-ui/lib/cli/ui/truncater.rb +107 -0
  68. data/vendor/deps/cli-ui/lib/cli/ui/version.rb +6 -0
  69. data/vendor/deps/cli-ui/lib/cli/ui/widgets/base.rb +37 -0
  70. data/vendor/deps/cli-ui/lib/cli/ui/widgets/status.rb +75 -0
  71. data/vendor/deps/cli-ui/lib/cli/ui/widgets.rb +91 -0
  72. data/vendor/deps/cli-ui/lib/cli/ui/wrap.rb +63 -0
  73. data/vendor/deps/cli-ui/lib/cli/ui.rb +356 -0
  74. metadata +114 -5
@@ -0,0 +1,137 @@
1
+ # typed: true
2
+ require 'cli/kit'
3
+ require 'fileutils'
4
+
5
+ module CLI
6
+ module Kit
7
+ class Config
8
+ extend T::Sig
9
+
10
+ XDG_CONFIG_HOME = 'XDG_CONFIG_HOME'
11
+
12
+ sig { params(tool_name: String).void }
13
+ def initialize(tool_name:)
14
+ @tool_name = tool_name
15
+ end
16
+
17
+ # Returns the config corresponding to `name` from the config file
18
+ # `false` is returned if it doesn't exist
19
+ #
20
+ # #### Parameters
21
+ # `section` : the section of the config value you are looking for
22
+ # `name` : the name of the config value you are looking for
23
+ #
24
+ # #### Returns
25
+ # `value` : the value of the config variable (nil if none)
26
+ #
27
+ # #### Example Usage
28
+ # `config.get('name.of.config')`
29
+ #
30
+ sig { params(section: String, name: String, default: T.nilable(String)).returns(T.nilable(String)) }
31
+ def get(section, name, default: nil)
32
+ all_configs.dig("[#{section}]", name) || default
33
+ end
34
+
35
+ # Coalesce and enforce the value of a config to a boolean
36
+ sig { params(section: String, name: String, default: T.nilable(T::Boolean)).returns(T.nilable(T::Boolean)) }
37
+ def get_bool(section, name, default: false)
38
+ case get(section, name)
39
+ when 'true'
40
+ true
41
+ when 'false'
42
+ false
43
+ when nil
44
+ default
45
+ else
46
+ raise CLI::Kit::Abort, "Invalid config: #{section}.#{name} is expected to be true or false"
47
+ end
48
+ end
49
+
50
+ # Sets the config value in the config file
51
+ #
52
+ # #### Parameters
53
+ # `section` : the section of the config you are setting
54
+ # `name` : the name of the config you are setting
55
+ # `value` : the value of the config you are setting
56
+ #
57
+ # #### Example Usage
58
+ # `config.set('section', 'name.of.config', 'value')`
59
+ #
60
+ sig { params(section: String, name: String, value: T.nilable(T.any(String, T::Boolean))).void }
61
+ def set(section, name, value)
62
+ all_configs["[#{section}]"] ||= {}
63
+ case value
64
+ when nil
65
+ T.must(all_configs["[#{section}]"]).delete(name)
66
+ else
67
+ T.must(all_configs["[#{section}]"])[name] = value.to_s
68
+ end
69
+ write_config
70
+ end
71
+
72
+ # Unsets a config value in the config file
73
+ #
74
+ # #### Parameters
75
+ # `section` : the section of the config you are deleting
76
+ # `name` : the name of the config you are deleting
77
+ #
78
+ # #### Example Usage
79
+ # `config.unset('section', 'name.of.config')`
80
+ #
81
+ sig { params(section: String, name: String).void }
82
+ def unset(section, name)
83
+ set(section, name, nil)
84
+ end
85
+
86
+ # Gets the hash for the entire section
87
+ #
88
+ # #### Parameters
89
+ # `section` : the section of the config you are getting
90
+ #
91
+ # #### Example Usage
92
+ # `config.get_section('section')`
93
+ #
94
+ sig { params(section: String).returns(T::Hash[String, String]) }
95
+ def get_section(section)
96
+ (all_configs["[#{section}]"] || {}).dup
97
+ end
98
+
99
+ sig { returns(String) }
100
+ def to_s
101
+ ini.to_s
102
+ end
103
+
104
+ # The path on disk at which the configuration is stored:
105
+ # `$XDG_CONFIG_HOME/<toolname>/config`
106
+ # if ENV['XDG_CONFIG_HOME'] is not set, we default to ~/.config, e.g.:
107
+ # ~/.config/tool/config
108
+ #
109
+ sig { returns(String) }
110
+ def file
111
+ config_home = ENV.fetch(XDG_CONFIG_HOME, '~/.config')
112
+ File.expand_path(File.join(@tool_name, 'config'), config_home)
113
+ end
114
+
115
+ private
116
+
117
+ sig { returns(T::Hash[String, T::Hash[String, String]]) }
118
+ def all_configs
119
+ ini.ini
120
+ end
121
+
122
+ sig { returns(CLI::Kit::Ini) }
123
+ def ini
124
+ @ini ||= CLI::Kit::Ini.new(file).tap(&:parse)
125
+ end
126
+
127
+ sig { void }
128
+ def write_config
129
+ all_configs.each do |section, sub_config|
130
+ all_configs.delete(section) if sub_config.empty?
131
+ end
132
+ FileUtils.mkdir_p(File.dirname(file))
133
+ File.write(file, to_s)
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,30 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ class Exception
5
+ extend(T::Sig)
6
+
7
+ # You'd think instance variables @bug and @silent would work here. They
8
+ # don't. I'm not sure why. If you, the reader, want to take some time to
9
+ # figure it out, go ahead and refactor to that.
10
+
11
+ sig { returns(T::Boolean) }
12
+ def bug?
13
+ true
14
+ end
15
+
16
+ sig { returns(T::Boolean) }
17
+ def silent?
18
+ false
19
+ end
20
+
21
+ sig { params(bug: T::Boolean).void }
22
+ def bug!(bug = true)
23
+ singleton_class.define_method(:bug?) { bug }
24
+ end
25
+
26
+ sig { params(silent: T::Boolean).void }
27
+ def silent!(silent = true)
28
+ singleton_class.define_method(:silent?) { silent }
29
+ end
30
+ end
@@ -0,0 +1,165 @@
1
+ # typed: true
2
+ require 'cli/kit'
3
+ require 'English'
4
+
5
+ module CLI
6
+ module Kit
7
+ class ErrorHandler
8
+ extend T::Sig
9
+
10
+ ExceptionReporterOrProc = T.type_alias do
11
+ T.any(T.class_of(ExceptionReporter), T.proc.returns(T.class_of(ExceptionReporter)))
12
+ end
13
+
14
+ sig { params(override_exception_handler: T.proc.params(arg0: Exception).returns(Integer)).void }
15
+ attr_writer :override_exception_handler
16
+
17
+ sig do
18
+ params(
19
+ log_file: T.nilable(String),
20
+ exception_reporter: ExceptionReporterOrProc,
21
+ tool_name: T.nilable(String),
22
+ dev_mode: T::Boolean,
23
+ ).void
24
+ end
25
+ def initialize(log_file: nil, exception_reporter: NullExceptionReporter, tool_name: nil, dev_mode: false)
26
+ @log_file = log_file
27
+ @exception_reporter_or_proc = exception_reporter
28
+ @tool_name = tool_name
29
+ @dev_mode = dev_mode
30
+ end
31
+
32
+ class ExceptionReporter
33
+ extend T::Sig
34
+ extend T::Helpers
35
+ abstract!
36
+
37
+ sig { abstract.params(exception: T.nilable(Exception), logs: T.nilable(String)).void }
38
+ def self.report(exception, logs = nil); end
39
+ end
40
+
41
+ class NullExceptionReporter < ExceptionReporter
42
+ extend T::Sig
43
+
44
+ sig { override.params(_exception: T.nilable(Exception), _logs: T.nilable(String)).void }
45
+ def self.report(_exception, _logs = nil)
46
+ nil
47
+ end
48
+ end
49
+
50
+ sig { params(block: T.proc.void).returns(Integer) }
51
+ def call(&block)
52
+ # @at_exit_exception is set if handle_abort decides to submit an error.
53
+ # $ERROR_INFO is set if we terminate because of a signal.
54
+ at_exit { report_exception(@at_exit_exception || $ERROR_INFO) }
55
+ triage_all_exceptions(&block)
56
+ end
57
+
58
+ sig { params(error: T.nilable(Exception)).void }
59
+ def report_exception(error)
60
+ if (notify_with = exception_for_submission(error))
61
+ logs = nil
62
+ if @log_file
63
+ logs = begin
64
+ File.read(@log_file)
65
+ rescue => e
66
+ "(#{e.class}: #{e.message})"
67
+ end
68
+ end
69
+ exception_reporter.report(notify_with, logs)
70
+ end
71
+ end
72
+
73
+ SIGNALS_THAT_ARENT_BUGS = [
74
+ 'SIGTERM', 'SIGHUP', 'SIGINT',
75
+ ].freeze
76
+
77
+ private
78
+
79
+ # Run the program, handling any errors that occur.
80
+ #
81
+ # Errors are printed to stderr unless they're #silent?, and are reported
82
+ # to bugsnag (by setting @at_exit_exeption for our at_exit handler) if
83
+ # they're #bug?
84
+ #
85
+ # Returns an exit status for the program.
86
+ sig { params(block: T.proc.void).returns(Integer) }
87
+ def triage_all_exceptions(&block)
88
+ begin
89
+ block.call
90
+ CLI::Kit::EXIT_SUCCESS
91
+ rescue Interrupt => e # Ctrl-C
92
+ # transform message, prevent bugsnag
93
+ exc = e.exception('Interrupt')
94
+ CLI::Kit.raise(exc, bug: false)
95
+ rescue Errno::ENOSPC => e
96
+ # transform message, prevent bugsnag
97
+ message = if @tool_name
98
+ "Your disk is full - {{command:#{@tool_name}}} requires free space to operate"
99
+ else
100
+ 'Your disk is full - free space is required to operate'
101
+ end
102
+ exc = e.exception(message)
103
+ CLI::Kit.raise(exc, bug: false)
104
+ end
105
+ rescue Exception => e # rubocop:disable Lint/RescueException
106
+ @at_exit_exception = e if e.bug?
107
+
108
+ if (eh = @override_exception_handler)
109
+ return eh.call(e)
110
+ end
111
+
112
+ raise(e) if @dev_mode && e.bug?
113
+
114
+ stderr_puts(e.message) unless e.silent?
115
+ e.bug? ? CLI::Kit::EXIT_BUG : CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG
116
+ end
117
+
118
+ sig { params(error: T.nilable(Exception)).returns(T.nilable(Exception)) }
119
+ def exception_for_submission(error)
120
+ # happens on normal non-error termination
121
+ return(nil) if error.nil?
122
+
123
+ return(nil) unless error.bug?
124
+
125
+ case error
126
+ when SignalException
127
+ SIGNALS_THAT_ARENT_BUGS.include?(error.message) ? nil : error
128
+ when SystemExit # "exit N" called
129
+ case error.status
130
+ when CLI::Kit::EXIT_SUCCESS # submit nothing if it was `exit 0`
131
+ nil
132
+ when CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG
133
+ # if it was `exit 30`, translate the exit code to 1, and submit
134
+ # nothing. 30 is used to signal normal failures that are not
135
+ # indicative of bugs. However, users should see it presented as 1.
136
+ exit(1)
137
+ else
138
+ # A weird termination status happened. `error.exception "message"`
139
+ # will maintain backtrace but allow us to set a message
140
+ error.exception("abnormal termination status: #{error.status}")
141
+ end
142
+ else
143
+ error
144
+ end
145
+ end
146
+
147
+ sig { params(message: String).void }
148
+ def stderr_puts(message)
149
+ $stderr.puts(CLI::UI.fmt("{{red:#{message}}}"))
150
+ rescue Errno::EPIPE
151
+ nil
152
+ end
153
+
154
+ sig { returns(T.class_of(ExceptionReporter)) }
155
+ def exception_reporter
156
+ case @exception_reporter_or_proc
157
+ when Proc
158
+ @exception_reporter_or_proc.call
159
+ else
160
+ @exception_reporter_or_proc
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,99 @@
1
+ # typed: true
2
+ require 'cli/kit'
3
+ require 'English'
4
+ require 'fileutils'
5
+
6
+ module CLI
7
+ module Kit
8
+ class Executor
9
+ extend T::Sig
10
+
11
+ sig { params(log_file: String).void }
12
+ def initialize(log_file:)
13
+ FileUtils.mkpath(File.dirname(log_file))
14
+ @log_file = log_file
15
+ end
16
+
17
+ sig { params(command: T.class_of(CLI::Kit::BaseCommand), command_name: String, args: T::Array[String]).void }
18
+ def call(command, command_name, args)
19
+ with_traps do
20
+ with_logging do |id|
21
+ command.call(args, command_name)
22
+ rescue => e
23
+ begin
24
+ $stderr.puts "This command ran with ID: #{id}"
25
+ $stderr.puts 'Please include this information in any issues/report along with relevant logs'
26
+ rescue SystemCallError
27
+ # Outputting to stderr is best-effort. Avoid raising another error when outputting debug info so that
28
+ # we can detect and log the original error, which may even be the source of this error.
29
+ nil
30
+ end
31
+ raise e
32
+ end
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ sig do
39
+ type_parameters(:T).params(block: T.proc.params(id: String).returns(T.type_parameter(:T)))
40
+ .returns(T.type_parameter(:T))
41
+ end
42
+ def with_logging(&block)
43
+ CLI::UI.log_output_to(@log_file) do
44
+ CLI::UI::StdoutRouter.with_id(on_streams: [CLI::UI::StdoutRouter.duplicate_output_to].compact) do |id|
45
+ block.call(id)
46
+ end
47
+ end
48
+ end
49
+
50
+ sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
51
+ def with_traps(&block)
52
+ twrap('QUIT', method(:quit_handler)) do
53
+ twrap('INFO', method(:info_handler), &block)
54
+ end
55
+ end
56
+
57
+ sig do
58
+ type_parameters(:T).params(signal: String, handler: Method,
59
+ block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T))
60
+ end
61
+ def twrap(signal, handler, &block)
62
+ return yield unless Signal.list.key?(signal)
63
+
64
+ begin
65
+ begin
66
+ prev_handler = trap(signal, handler)
67
+ installed = true
68
+ rescue ArgumentError
69
+ # If we couldn't install a signal handler because the signal is
70
+ # reserved, remember not to uninstall it later.
71
+ installed = false
72
+ end
73
+ yield
74
+ ensure
75
+ trap(signal, prev_handler) if installed
76
+ end
77
+ end
78
+
79
+ sig { params(_sig: T.untyped).void }
80
+ def quit_handler(_sig)
81
+ z = caller
82
+ CLI::UI.raw do
83
+ $stderr.puts('SIGQUIT: quit')
84
+ $stderr.puts(z)
85
+ end
86
+ exit(CLI::Kit::EXIT_FAILURE_BUT_NOT_BUG)
87
+ end
88
+
89
+ sig { params(_sig: T.untyped).void }
90
+ def info_handler(_sig)
91
+ z = caller
92
+ CLI::UI.raw do
93
+ $stderr.puts('SIGINFO:')
94
+ $stderr.puts(z)
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,94 @@
1
+ # typed: true
2
+ require 'cli/kit'
3
+
4
+ module CLI
5
+ module Kit
6
+ # INI is a language similar to JSON or YAML, but simplied
7
+ # The spec is here: https://en.wikipedia.org/wiki/INI_file
8
+ # This parser includes supports for 2 very basic uses
9
+ # - Sections
10
+ # - Key Value Pairs (within and outside of the sections)
11
+ #
12
+ # [global]
13
+ # key = val
14
+ #
15
+ # Nothing else is supported right now
16
+ # See the ini_test.rb file for more examples
17
+ #
18
+ class Ini
19
+ extend T::Sig
20
+
21
+ sig { returns(T::Hash[String, T::Hash[String, String]]) }
22
+ attr_accessor :ini
23
+
24
+ sig do
25
+ params(path: T.nilable(String), config: T.nilable(String), default_section: String).void
26
+ end
27
+ def initialize(path = nil, config: nil, default_section: '[global]')
28
+ @config = if path && File.exist?(path)
29
+ File.readlines(path)
30
+ elsif config
31
+ config.lines
32
+ end
33
+ @ini = {}
34
+ @current_key = default_section
35
+ end
36
+
37
+ sig { returns(T::Hash[String, T::Hash[String, String]]) }
38
+ def parse
39
+ return @ini if @config.nil?
40
+
41
+ @config.each do |l|
42
+ l.strip!
43
+
44
+ if section_designator?(l)
45
+ @current_key = l
46
+ else
47
+ k, v = l.split('=', 2).map(&:strip)
48
+ set_val(k, v) if k && v
49
+ end
50
+ end
51
+
52
+ @ini
53
+ end
54
+
55
+ sig { returns(String) }
56
+ def git_format
57
+ to_ini(git_format: true)
58
+ end
59
+
60
+ sig { returns(String) }
61
+ def to_s
62
+ to_ini
63
+ end
64
+
65
+ private
66
+
67
+ sig { params(git_format: T::Boolean).returns(String) }
68
+ def to_ini(git_format: false)
69
+ optional_tab = git_format ? "\t" : ''
70
+ str = []
71
+ @ini.each do |section_designator, section|
72
+ str << '' unless str.empty? || git_format
73
+ str << section_designator
74
+ section.each do |k, v|
75
+ str << "#{optional_tab}#{k} = #{v}"
76
+ end
77
+ end
78
+ str.join("\n")
79
+ end
80
+
81
+ sig { params(key: String, val: String).void }
82
+ def set_val(key, val)
83
+ current_key = @current_key
84
+ @ini[current_key] ||= {}
85
+ @ini[current_key][key] = val
86
+ end
87
+
88
+ sig { params(k: String).returns(T::Boolean) }
89
+ def section_designator?(k)
90
+ k.start_with?('[') && k.end_with?(']')
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,89 @@
1
+ # typed: true
2
+ # Copyright (c) 2014-2016 Yuki Nishijima
3
+
4
+ # MIT License
5
+
6
+ # Permission is hereby granted, free of charge, to any person obtaining
7
+ # a copy of this software and associated documentation files (the
8
+ # "Software"), to deal in the Software without restriction, including
9
+ # without limitation the rights to use, copy, modify, merge, publish,
10
+ # distribute, sublicense, and/or sell copies of the Software, and to
11
+ # permit persons to whom the Software is furnished to do so, subject to
12
+ # the following conditions:
13
+
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+
25
+ require 'cli/kit'
26
+
27
+ module CLI
28
+ module Kit
29
+ module Levenshtein
30
+ extend T::Sig
31
+
32
+ # This code is based directly on the Text gem implementation
33
+ # Copyright (c) 2006-2013 Paul Battley, Michael Neumann, Tim Fletcher.
34
+ #
35
+ # Returns a value representing the "cost" of transforming str1 into str2
36
+ sig { params(str1: String, str2: String).returns(Integer) }
37
+ def distance(str1, str2)
38
+ n = str1.length
39
+ m = str2.length
40
+ return m if n.zero?
41
+ return n if m.zero?
42
+
43
+ d = (0..m).to_a
44
+ x = 0
45
+
46
+ # to avoid duplicating an enumerable object, create it outside of the loop
47
+ str2_codepoints = str2.codepoints
48
+
49
+ str1.each_codepoint.with_index(1) do |char1, i|
50
+ j = 0
51
+ while j < m
52
+ cost = char1 == str2_codepoints[j] ? 0 : 1
53
+ x = min3(
54
+ T.must(d[j + 1]) + 1, # insertion
55
+ i + 1, # deletion
56
+ T.must(d[j]) + cost # substitution
57
+ )
58
+ d[j] = i
59
+ i = x
60
+
61
+ j += 1
62
+ end
63
+ d[m] = x
64
+ end
65
+
66
+ x
67
+ end
68
+ module_function :distance
69
+
70
+ private
71
+
72
+ # detects the minimum value out of three arguments. This method is
73
+ # faster than `[a, b, c].min` and puts less GC pressure.
74
+ # See https://github.com/yuki24/did_you_mean/pull/1 for a performance
75
+ # benchmark.
76
+ sig { params(a: Integer, b: Integer, c: Integer).returns(Integer) }
77
+ def min3(a, b, c)
78
+ if a < b && a < c
79
+ a
80
+ elsif b < c
81
+ b
82
+ else
83
+ c
84
+ end
85
+ end
86
+ module_function :min3
87
+ end
88
+ end
89
+ end