gorails 0.1.1 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -1
  3. data/Gemfile.lock +1 -6
  4. data/README.md +41 -12
  5. data/bin/update-deps +95 -0
  6. data/exe/gorails +2 -1
  7. data/gorails.gemspec +0 -2
  8. data/lib/gorails/commands/railsbytes.rb +45 -4
  9. data/lib/gorails/commands/version.rb +15 -0
  10. data/lib/gorails/commands.rb +2 -5
  11. data/lib/gorails/version.rb +1 -1
  12. data/lib/gorails.rb +11 -20
  13. data/vendor/deps/cli-kit/REVISION +1 -0
  14. data/vendor/deps/cli-kit/lib/cli/kit/args/definition.rb +301 -0
  15. data/vendor/deps/cli-kit/lib/cli/kit/args/evaluation.rb +237 -0
  16. data/vendor/deps/cli-kit/lib/cli/kit/args/parser/node.rb +131 -0
  17. data/vendor/deps/cli-kit/lib/cli/kit/args/parser.rb +128 -0
  18. data/vendor/deps/cli-kit/lib/cli/kit/args/tokenizer.rb +132 -0
  19. data/vendor/deps/cli-kit/lib/cli/kit/args.rb +15 -0
  20. data/vendor/deps/cli-kit/lib/cli/kit/base_command.rb +29 -0
  21. data/vendor/deps/cli-kit/lib/cli/kit/command_help.rb +256 -0
  22. data/vendor/deps/cli-kit/lib/cli/kit/command_registry.rb +141 -0
  23. data/vendor/deps/cli-kit/lib/cli/kit/config.rb +137 -0
  24. data/vendor/deps/cli-kit/lib/cli/kit/core_ext.rb +30 -0
  25. data/vendor/deps/cli-kit/lib/cli/kit/error_handler.rb +165 -0
  26. data/vendor/deps/cli-kit/lib/cli/kit/executor.rb +99 -0
  27. data/vendor/deps/cli-kit/lib/cli/kit/ini.rb +94 -0
  28. data/vendor/deps/cli-kit/lib/cli/kit/levenshtein.rb +89 -0
  29. data/vendor/deps/cli-kit/lib/cli/kit/logger.rb +95 -0
  30. data/vendor/deps/cli-kit/lib/cli/kit/opts.rb +284 -0
  31. data/vendor/deps/cli-kit/lib/cli/kit/resolver.rb +67 -0
  32. data/vendor/deps/cli-kit/lib/cli/kit/sorbet_runtime_stub.rb +142 -0
  33. data/vendor/deps/cli-kit/lib/cli/kit/support/test_helper.rb +253 -0
  34. data/vendor/deps/cli-kit/lib/cli/kit/support.rb +10 -0
  35. data/vendor/deps/cli-kit/lib/cli/kit/system.rb +350 -0
  36. data/vendor/deps/cli-kit/lib/cli/kit/util.rb +133 -0
  37. data/vendor/deps/cli-kit/lib/cli/kit/version.rb +7 -0
  38. data/vendor/deps/cli-kit/lib/cli/kit.rb +151 -0
  39. data/vendor/deps/cli-ui/REVISION +1 -0
  40. data/vendor/deps/cli-ui/lib/cli/ui/ansi.rb +180 -0
  41. data/vendor/deps/cli-ui/lib/cli/ui/color.rb +98 -0
  42. data/vendor/deps/cli-ui/lib/cli/ui/formatter.rb +216 -0
  43. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_stack.rb +116 -0
  44. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/box.rb +176 -0
  45. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/bracket.rb +149 -0
  46. data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style.rb +112 -0
  47. data/vendor/deps/cli-ui/lib/cli/ui/frame.rb +300 -0
  48. data/vendor/deps/cli-ui/lib/cli/ui/glyph.rb +92 -0
  49. data/vendor/deps/cli-ui/lib/cli/ui/os.rb +58 -0
  50. data/vendor/deps/cli-ui/lib/cli/ui/printer.rb +72 -0
  51. data/vendor/deps/cli-ui/lib/cli/ui/progress.rb +102 -0
  52. data/vendor/deps/cli-ui/lib/cli/ui/prompt/interactive_options.rb +534 -0
  53. data/vendor/deps/cli-ui/lib/cli/ui/prompt/options_handler.rb +36 -0
  54. data/vendor/deps/cli-ui/lib/cli/ui/prompt.rb +354 -0
  55. data/vendor/deps/cli-ui/lib/cli/ui/sorbet_runtime_stub.rb +143 -0
  56. data/vendor/deps/cli-ui/lib/cli/ui/spinner/async.rb +46 -0
  57. data/vendor/deps/cli-ui/lib/cli/ui/spinner/spin_group.rb +292 -0
  58. data/vendor/deps/cli-ui/lib/cli/ui/spinner.rb +82 -0
  59. data/vendor/deps/cli-ui/lib/cli/ui/stdout_router.rb +264 -0
  60. data/vendor/deps/cli-ui/lib/cli/ui/terminal.rb +53 -0
  61. data/vendor/deps/cli-ui/lib/cli/ui/truncater.rb +107 -0
  62. data/vendor/deps/cli-ui/lib/cli/ui/version.rb +6 -0
  63. data/vendor/deps/cli-ui/lib/cli/ui/widgets/base.rb +37 -0
  64. data/vendor/deps/cli-ui/lib/cli/ui/widgets/status.rb +75 -0
  65. data/vendor/deps/cli-ui/lib/cli/ui/widgets.rb +91 -0
  66. data/vendor/deps/cli-ui/lib/cli/ui/wrap.rb +63 -0
  67. data/vendor/deps/cli-ui/lib/cli/ui.rb +356 -0
  68. metadata +59 -30
@@ -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