rucoa 0.2.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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