rucoa 0.2.0 → 0.5.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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +6 -0
  3. data/Gemfile +1 -0
  4. data/Gemfile.lock +11 -2
  5. data/README.md +39 -6
  6. data/data/definitions_ruby_3_1 +0 -0
  7. data/images/diagnostics.gif +0 -0
  8. data/images/document-formatting.gif +0 -0
  9. data/images/document-symbol.gif +0 -0
  10. data/images/selection-ranges.gif +0 -0
  11. data/lib/rucoa/cli.rb +2 -2
  12. data/lib/rucoa/configuration.rb +97 -0
  13. data/lib/rucoa/definition_archiver.rb +29 -0
  14. data/lib/rucoa/definition_builders/rbs_constant_definition_builder.rb +44 -0
  15. data/lib/rucoa/definition_builders/rbs_method_definition_builder.rb +106 -0
  16. data/lib/rucoa/definition_builders/yard_method_definition_builder.rb +191 -0
  17. data/lib/rucoa/definition_builders.rb +9 -0
  18. data/lib/rucoa/definition_store.rb +63 -0
  19. data/lib/rucoa/definitions/base.rb +12 -0
  20. data/lib/rucoa/definitions/constant_definition.rb +51 -0
  21. data/lib/rucoa/definitions/method_definition.rb +126 -0
  22. data/lib/rucoa/definitions/method_parameter_definition.rb +30 -0
  23. data/lib/rucoa/definitions.rb +9 -0
  24. data/lib/rucoa/handler_concerns/configuration_requestable.rb +35 -0
  25. data/lib/rucoa/handler_concerns/diagnostics_publishable.rb +172 -0
  26. data/lib/rucoa/handler_concerns.rb +8 -0
  27. data/lib/rucoa/handlers/base.rb +69 -0
  28. data/lib/rucoa/handlers/exit_handler.rb +11 -0
  29. data/lib/rucoa/handlers/initialize_handler.rb +29 -0
  30. data/lib/rucoa/handlers/initialized_handler.rb +23 -0
  31. data/lib/rucoa/handlers/shutdown_handler.rb +12 -0
  32. data/lib/rucoa/handlers/text_document_code_action_handler.rb +104 -0
  33. data/lib/rucoa/handlers/text_document_did_change_handler.rb +12 -0
  34. data/lib/rucoa/handlers/text_document_did_open_handler.rb +38 -0
  35. data/lib/rucoa/handlers/text_document_document_symbol_handler.rb +241 -0
  36. data/lib/rucoa/handlers/text_document_formatting_handler.rb +64 -0
  37. data/lib/rucoa/handlers/text_document_range_formatting_handler.rb +76 -0
  38. data/lib/rucoa/handlers/text_document_selection_range_handler.rb +141 -0
  39. data/lib/rucoa/handlers/text_document_signature_help_handler.rb +68 -0
  40. data/lib/rucoa/handlers/workspace_did_change_configuration_handler.rb +13 -0
  41. data/lib/rucoa/handlers.rb +20 -0
  42. data/lib/rucoa/message_reader.rb +3 -1
  43. data/lib/rucoa/message_writer.rb +3 -0
  44. data/lib/rucoa/node_concerns/name_full_qualifiable.rb +20 -0
  45. data/lib/rucoa/node_concerns.rb +7 -0
  46. data/lib/rucoa/node_inspector.rb +109 -0
  47. data/lib/rucoa/nodes/base.rb +43 -0
  48. data/lib/rucoa/nodes/casgn_node.rb +14 -0
  49. data/lib/rucoa/nodes/class_node.rb +8 -0
  50. data/lib/rucoa/nodes/const_node.rb +12 -0
  51. data/lib/rucoa/nodes/def_node.rb +71 -0
  52. data/lib/rucoa/nodes/defs_node.rb +12 -0
  53. data/lib/rucoa/nodes/lvar_node.rb +25 -0
  54. data/lib/rucoa/nodes/module_node.rb +21 -0
  55. data/lib/rucoa/nodes/sclass_node.rb +8 -0
  56. data/lib/rucoa/nodes/send_node.rb +60 -0
  57. data/lib/rucoa/nodes/str_node.rb +4 -0
  58. data/lib/rucoa/nodes/sym_node.rb +12 -0
  59. data/lib/rucoa/nodes.rb +10 -0
  60. data/lib/rucoa/parser_builder.rb +20 -10
  61. data/lib/rucoa/range.rb +89 -13
  62. data/lib/rucoa/rbs_document_loader.rb +43 -0
  63. data/lib/rucoa/rubocop_autocorrector.rb +1 -1
  64. data/lib/rucoa/rubocop_configuration_checker.rb +42 -0
  65. data/lib/rucoa/rubocop_investigator.rb +1 -1
  66. data/lib/rucoa/server.rb +96 -122
  67. data/lib/rucoa/source.rb +56 -6
  68. data/lib/rucoa/source_store.rb +11 -9
  69. data/lib/rucoa/types/method_type.rb +23 -0
  70. data/lib/rucoa/types.rb +7 -0
  71. data/lib/rucoa/version.rb +1 -1
  72. data/lib/rucoa/yard_glob_document_loader.rb +47 -0
  73. data/lib/rucoa/yard_string_document_loader.rb +70 -0
  74. data/lib/rucoa.rb +14 -4
  75. data/rucoa.gemspec +1 -0
  76. metadata +70 -6
  77. data/lib/rucoa/code_action_provider.rb +0 -102
  78. data/lib/rucoa/diagnostic_provider.rb +0 -159
  79. data/lib/rucoa/formatting_provider.rb +0 -52
  80. data/lib/rucoa/selection_range_provider.rb +0 -97
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rucoa
4
+ class DefinitionStore
5
+ # @return [Array<Rucoa::Definition::Base>]
6
+ attr_accessor :definitions
7
+
8
+ def initialize
9
+ @definitions = []
10
+ end
11
+
12
+ # @param source_path [String]
13
+ # @return [Array<Rucoa::Definition::Base>]
14
+ def update_definitions_defined_in(source_path, definitions:)
15
+ delete_definitions_defined_in(source_path)
16
+ @definitions += definitions
17
+ end
18
+
19
+ # @param full_qualified_name [String]
20
+ # @return [Array<Rucoa::Definitions::Base>]
21
+ def select_by_full_qualified_name(full_qualified_name)
22
+ @definitions.select do |definition|
23
+ definition.full_qualified_name == full_qualified_name
24
+ end
25
+ end
26
+
27
+ # @param namespace [String]
28
+ # @return [Array<Rucoa::Definitions::MethodDefinition>]
29
+ def method_definitions_of(namespace)
30
+ method_definitions.select do |method_definition|
31
+ method_definition.namespace == namespace
32
+ end
33
+ end
34
+
35
+ # @param namespace [String]
36
+ # @return [Array<Rucoa::Definitions::ConstantDefinition>] e.g. File::Separator, File::SEPARATOR, etc.
37
+ def constant_definitions_under(namespace)
38
+ constant_definitions.select do |constant_definition|
39
+ constant_definition.namespace == namespace
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ # @return [Array<Rucoa::Definition::ConstantDefinition>]
46
+ def constant_definitions
47
+ @definitions.grep(Definitions::ConstantDefinition)
48
+ end
49
+
50
+ # @return [Array<Rucoa::Definition::MethodDefinition>]
51
+ def method_definitions
52
+ @definitions.grep(Definitions::MethodDefinition)
53
+ end
54
+
55
+ # @param source_path [String]
56
+ # @return [Array<Rucoa::Definition::Base>]
57
+ def delete_definitions_defined_in(source_path)
58
+ @definitions.delete_if do |definition|
59
+ definition.source_path == source_path
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rucoa
4
+ module Definitions
5
+ class Base
6
+ # @return [String]
7
+ def source_path
8
+ raise ::NotImplementedError
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rucoa
4
+ module Definitions
5
+ # Represents class definition, module definition, or constant assignment.
6
+ class ConstantDefinition < Base
7
+ # @return [String]
8
+ attr_reader :full_qualified_name
9
+
10
+ # @return [String]
11
+ attr_reader :source_path
12
+
13
+ # @param full_qualified_name [String]
14
+ # @param source_path [String]
15
+ def initialize(full_qualified_name:, source_path:)
16
+ super()
17
+ @full_qualified_name = full_qualified_name
18
+ @source_path = source_path
19
+ end
20
+
21
+ # @return [String]
22
+ # @example returns non-full-qualified name
23
+ # definition = Rucoa::Definitions::ConstantDefinition.new(
24
+ # full_qualified_name: 'Foo::Bar::Baz',
25
+ # source_path: '/path/to/foo/bar/baz.rb'
26
+ # )
27
+ # expect(definition.name).to eq('Baz')
28
+ def name
29
+ names.last
30
+ end
31
+
32
+ # @return [String]
33
+ # @example returns namespace
34
+ # definition = Rucoa::Definitions::ConstantDefinition.new(
35
+ # full_qualified_name: 'Foo::Bar::Baz',
36
+ # source_path: '/path/to/foo/bar/baz.rb'
37
+ # )
38
+ # expect(definition.namespace).to eq('Foo::Bar')
39
+ def namespace
40
+ names[..-2].join('::')
41
+ end
42
+
43
+ private
44
+
45
+ # @return [Array<String>]
46
+ def names
47
+ @names ||= full_qualified_name.split('::')
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rucoa
4
+ module Definitions
5
+ class MethodDefinition < Base
6
+ # @return [String, nil]
7
+ attr_reader :description
8
+
9
+ # @return [String]
10
+ attr_reader :method_name
11
+
12
+ # @return [String]
13
+ attr_reader :namespace
14
+
15
+ # @return [String]
16
+ attr_reader :source_path
17
+
18
+ # @param description [String, nil]
19
+ # @param kind [Symbol]
20
+ # @param method_name [String]
21
+ # @param namespace [String]
22
+ # @param source_path [String]
23
+ # @param types [Array<Rucoa::Types::MethodType>]
24
+ def initialize(
25
+ description:,
26
+ kind:,
27
+ method_name:,
28
+ namespace:,
29
+ source_path:,
30
+ types:
31
+ )
32
+ super()
33
+ @description = description
34
+ @kind = kind
35
+ @method_name = method_name
36
+ @namespace = namespace
37
+ @source_path = source_path
38
+ @types = types
39
+ end
40
+
41
+ # @return [String]
42
+ # @example returns qualified name of method
43
+ # method_definition = Rucoa::Definitions::MethodDefinition.new(
44
+ # description: nil,
45
+ # kind: :instance,
46
+ # method_name: 'foo',
47
+ # namespace: 'Foo::Bar',
48
+ # source_path: '/path/to/foo/bar.rb',
49
+ # types: []
50
+ # )
51
+ # expect(method_definition.full_qualified_name).to eq('Foo::Bar#foo')
52
+ def full_qualified_name
53
+ [
54
+ @namespace,
55
+ method_kind_symbol,
56
+ @method_name
57
+ ].join
58
+ end
59
+
60
+ # @todo
61
+ # @return [Array<Rucoa::Definitions::MethodParameter>]
62
+ def parameters
63
+ []
64
+ end
65
+
66
+ # @return [Array<String>]
67
+ # @example returns return types
68
+ # method_definition = Rucoa::Definitions::MethodDefinition.new(
69
+ # description: nil,
70
+ # kind: :instance,
71
+ # method_name: 'foo',
72
+ # namespace: 'Foo::Bar',
73
+ # source_path: '/path/to/foo/bar.rb',
74
+ # types: [
75
+ # Rucoa::Types::MethodType.new(
76
+ # parameters_string: '',
77
+ # return_type: 'String'
78
+ # )
79
+ # ]
80
+ # )
81
+ # expect(method_definition.return_types).to eq(%w[String])
82
+ def return_types
83
+ @types.map(&:return_type)
84
+ end
85
+
86
+ # @return [Array<String>]
87
+ # @example returns signature
88
+ # method_definition = Rucoa::Definitions::MethodDefinition.new(
89
+ # description: nil,
90
+ # kind: :instance,
91
+ # method_name: 'foo',
92
+ # namespace: 'Foo::Bar',
93
+ # source_path: '/path/to/foo/bar.rb',
94
+ # types: [
95
+ # Rucoa::Types::MethodType.new(
96
+ # parameters_string: '?::int base',
97
+ # return_type: 'String'
98
+ # )
99
+ # ]
100
+ # )
101
+ # expect(method_definition.signatures).to eq(['Foo::Bar#foo(?::int base) -> String'])
102
+ def signatures
103
+ @types.map do |type|
104
+ format(
105
+ '%<full_qualified_name>s(%<parameters>s) -> %<return_types>s',
106
+ full_qualified_name: full_qualified_name,
107
+ parameters: type.parameters_string,
108
+ return_types: type.return_type
109
+ )
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ # @return [String]
116
+ def method_kind_symbol
117
+ case @kind
118
+ when :instance
119
+ '#'
120
+ else
121
+ '.'
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rucoa
4
+ module Definitions
5
+ # Represents method parameter definition.
6
+ class MethodParameterDefinition < Base
7
+ # @return [String]
8
+ attr_reader :name
9
+
10
+ # @return [Array<String>]
11
+ attr_reader :types
12
+
13
+ # @param name [String]
14
+ # @param types [Array<String>]
15
+ def initialize(name:, types:)
16
+ super()
17
+ @name = name
18
+ @types = types
19
+ end
20
+
21
+ # @return [Hash{Symbol => Object}]
22
+ def to_hash
23
+ {
24
+ name: name,
25
+ types: types
26
+ }
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rucoa
4
+ module Definitions
5
+ autoload :Base, 'rucoa/definitions/base'
6
+ autoload :ConstantDefinition, 'rucoa/definitions/constant_definition'
7
+ autoload :MethodDefinition, 'rucoa/definitions/method_definition'
8
+ end
9
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rucoa
4
+ module HandlerConcerns
5
+ module ConfigurationRequestable
6
+ include HandlerConcerns::DiagnosticsPublishable
7
+
8
+ private
9
+
10
+ # @return [void]
11
+ def request_workspace_configuration
12
+ write(
13
+ method: 'workspace/configuration',
14
+ params: {
15
+ items: [
16
+ {
17
+ section: 'rucoa'
18
+ }
19
+ ]
20
+ }
21
+ ) do |response|
22
+ configuration.update(response['result'][0])
23
+ publish_diagnostics_on_each_source
24
+ end
25
+ end
26
+
27
+ # @return [void]
28
+ def publish_diagnostics_on_each_source
29
+ source_store.each_uri do |uri|
30
+ publish_diagnostics_on(uri)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rucoa
4
+ module HandlerConcerns
5
+ module DiagnosticsPublishable
6
+ private
7
+
8
+ # @param uri [String]
9
+ # @return [Array<Hash>]
10
+ def diagnostics_on(uri)
11
+ return [] unless configuration.enables_diagnostics?
12
+
13
+ DiagnosticProvider.call(
14
+ source: source_store.get(uri),
15
+ uri: uri
16
+ )
17
+ end
18
+
19
+ # @param uri [String]
20
+ # @return [void]
21
+ def publish_diagnostics_on(uri)
22
+ write(
23
+ method: 'textDocument/publishDiagnostics',
24
+ params: {
25
+ diagnostics: diagnostics_on(uri),
26
+ uri: uri
27
+ }
28
+ )
29
+ end
30
+
31
+ class DiagnosticProvider
32
+ # @param source [Rucoa::Source]
33
+ # @param uri [String]
34
+ # @return [Array<Hash>]
35
+ def self.call(source:, uri:)
36
+ new(
37
+ source: source,
38
+ uri: uri
39
+ ).call
40
+ end
41
+
42
+ # @param source [Rucoa::Source]
43
+ # @param uri [String]
44
+ def initialize(source:, uri:)
45
+ @source = source
46
+ @uri = uri
47
+ end
48
+
49
+ # @return [Array<Hash>]
50
+ def call
51
+ return [] unless RubocopConfigurationChecker.call
52
+
53
+ offenses.map do |offense|
54
+ OffenseToDiagnosticMapper.call(
55
+ offense,
56
+ source: @source,
57
+ uri: @uri
58
+ )
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ # @return [Array<RuboCop::Cop::Offense>]
65
+ def offenses
66
+ RubocopInvestigator.call(source: @source)
67
+ end
68
+
69
+ class OffenseToDiagnosticMapper
70
+ LSP_SEVERITY_NAME_TO_VALUE_MAP = {
71
+ error: 1,
72
+ hint: 4,
73
+ information: 3,
74
+ warning: 2
75
+ }.freeze
76
+ private_constant :LSP_SEVERITY_NAME_TO_VALUE_MAP
77
+
78
+ RUBOCOP_SEVERITY_NAME_TO_LSP_SEVERITY_MAP = {
79
+ convention: LSP_SEVERITY_NAME_TO_VALUE_MAP[:information],
80
+ error: LSP_SEVERITY_NAME_TO_VALUE_MAP[:error],
81
+ fatal: LSP_SEVERITY_NAME_TO_VALUE_MAP[:error],
82
+ info: LSP_SEVERITY_NAME_TO_VALUE_MAP[:information],
83
+ refactor: LSP_SEVERITY_NAME_TO_VALUE_MAP[:hint],
84
+ warning: LSP_SEVERITY_NAME_TO_VALUE_MAP[:warning]
85
+ }.freeze
86
+ private_constant :RUBOCOP_SEVERITY_NAME_TO_LSP_SEVERITY_MAP
87
+
88
+ class << self
89
+ # @param offense [RuboCop::Cop::Offense]
90
+ # @param source [Rucoa::Source]
91
+ # @param uri [String]
92
+ # @return [Hash]
93
+ def call(offense, source:, uri:)
94
+ new(
95
+ offense,
96
+ source: source,
97
+ uri: uri
98
+ ).call
99
+ end
100
+ end
101
+
102
+ # @param offense [RuboCop::Cop::Offense]
103
+ # @param source [Rucoa::Source]
104
+ # @param uri [String]
105
+ def initialize(offense, source:, uri:)
106
+ @offense = offense
107
+ @source = source
108
+ @uri = uri
109
+ end
110
+
111
+ # @return [Hash]
112
+ def call
113
+ {
114
+ code: code,
115
+ data: data,
116
+ message: message,
117
+ range: range,
118
+ severity: severity,
119
+ source: source
120
+ }
121
+ end
122
+
123
+ private
124
+
125
+ # @return [String]
126
+ def code
127
+ @offense.cop_name
128
+ end
129
+
130
+ # @return [Hash]
131
+ def data
132
+ {
133
+ cop_name: @offense.cop_name,
134
+ edits: edits,
135
+ uri: @uri
136
+ }
137
+ end
138
+
139
+ # @return [Array<Hash>, nil]
140
+ def edits
141
+ @offense.corrector&.as_replacements&.map do |range, replacement|
142
+ {
143
+ newText: replacement,
144
+ range: Range.from_parser_range(range).to_vscode_range
145
+ }
146
+ end
147
+ end
148
+
149
+ # @return [String]
150
+ def message
151
+ @offense.message.delete_prefix("#{@offense.cop_name}: ")
152
+ end
153
+
154
+ # @return [Hash]
155
+ def range
156
+ Range.from_parser_range(@offense.location).to_vscode_range
157
+ end
158
+
159
+ # @return [Integer]
160
+ def severity
161
+ RUBOCOP_SEVERITY_NAME_TO_LSP_SEVERITY_MAP.fetch(@offense.severity.name, 1)
162
+ end
163
+
164
+ # @return [String]
165
+ def source
166
+ 'RuboCop'
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rucoa
4
+ module HandlerConcerns
5
+ autoload :ConfigurationRequestable, 'rucoa/handler_concerns/configuration_requestable'
6
+ autoload :DiagnosticsPublishable, 'rucoa/handler_concerns/diagnostics_publishable'
7
+ end
8
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rucoa
4
+ module Handlers
5
+ class Base
6
+ class << self
7
+ # @param server [Rucoa::Server]
8
+ # @param request [Hash]
9
+ # @return [void]
10
+ def call(request:, server:)
11
+ new(
12
+ request: request,
13
+ server: server
14
+ ).call
15
+ end
16
+ end
17
+
18
+ # @param request [Hash]
19
+ # @param server [Rucoa::Server]
20
+ def initialize(request:, server:)
21
+ @request = request
22
+ @server = server
23
+ end
24
+
25
+ # @return [void]
26
+ def call
27
+ raise ::NotImplementedError
28
+ end
29
+
30
+ private
31
+
32
+ # @return [Hash]
33
+ attr_reader :request
34
+
35
+ # @return [Rucoa::Server]
36
+ attr_reader :server
37
+
38
+ # @param message [Hash]
39
+ # @return [void]
40
+ def respond(message)
41
+ write(
42
+ id: request['id'],
43
+ result: message
44
+ )
45
+ end
46
+
47
+ # @return [Rucoa::Configuration]
48
+ def configuration
49
+ @server.configuration
50
+ end
51
+
52
+ # @return [Rucoa::DefinitionStore]
53
+ def definition_store
54
+ @server.definition_store
55
+ end
56
+
57
+ # @return [Rucoa::SourceStore]
58
+ def source_store
59
+ @server.source_store
60
+ end
61
+
62
+ # @param message [Hash]
63
+ # @return [void]
64
+ def write(message, &block)
65
+ @server.write(message, &block)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rucoa
4
+ module Handlers
5
+ class ExitHandler < Base
6
+ def call
7
+ server.finish
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rucoa
4
+ module Handlers
5
+ class InitializeHandler < Base
6
+ def call
7
+ respond(
8
+ capabilities: {
9
+ codeActionProvider: true,
10
+ documentFormattingProvider: true,
11
+ documentRangeFormattingProvider: true,
12
+ documentSymbolProvider: true,
13
+ selectionRangeProvider: true,
14
+ signatureHelpProvider: {
15
+ triggerCharacters: %w[
16
+ (
17
+ ,
18
+ ]
19
+ },
20
+ textDocumentSync: {
21
+ change: 1, # Full
22
+ openClose: true
23
+ }
24
+ }
25
+ )
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rucoa
4
+ module Handlers
5
+ class InitializedHandler < Base
6
+ include HandlerConcerns::ConfigurationRequestable
7
+
8
+ def call
9
+ request_workspace_configuration
10
+ load_definitions
11
+ end
12
+
13
+ private
14
+
15
+ # @return [void]
16
+ def load_definitions
17
+ YardGlobDocumentLoader.call(
18
+ globs: ::YARD::Parser::SourceParser::DEFAULT_PATH_GLOB
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rucoa
4
+ module Handlers
5
+ class ShutdownHandler < Base
6
+ def call
7
+ server.shutting_down = true
8
+ respond(nil)
9
+ end
10
+ end
11
+ end
12
+ end