puppet-editor-services 2.0.4

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 (109) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +510 -0
  3. data/CODEOWNERS +2 -0
  4. data/CODE_OF_CONDUCT.md +46 -0
  5. data/CONTRIBUTING.md +54 -0
  6. data/Gemfile +53 -0
  7. data/LICENSE +201 -0
  8. data/README.md +308 -0
  9. data/Rakefile +185 -0
  10. data/bin/puppet-debugserver +8 -0
  11. data/bin/puppet-languageserver +7 -0
  12. data/bin/puppet-languageserver-sidecar +7 -0
  13. data/lib/dsp/dsp.rb +7 -0
  14. data/lib/dsp/dsp_base.rb +62 -0
  15. data/lib/dsp/dsp_protocol.rb +4619 -0
  16. data/lib/lsp/lsp.rb +10 -0
  17. data/lib/lsp/lsp_base.rb +63 -0
  18. data/lib/lsp/lsp_custom.rb +170 -0
  19. data/lib/lsp/lsp_enums.rb +143 -0
  20. data/lib/lsp/lsp_protocol.rb +2785 -0
  21. data/lib/lsp/lsp_protocol_callhierarchy.proposed.rb +239 -0
  22. data/lib/lsp/lsp_protocol_colorprovider.rb +100 -0
  23. data/lib/lsp/lsp_protocol_configuration.rb +82 -0
  24. data/lib/lsp/lsp_protocol_declaration.rb +73 -0
  25. data/lib/lsp/lsp_protocol_foldingrange.rb +129 -0
  26. data/lib/lsp/lsp_protocol_implementation.rb +75 -0
  27. data/lib/lsp/lsp_protocol_progress.rb +200 -0
  28. data/lib/lsp/lsp_protocol_selectionrange.rb +79 -0
  29. data/lib/lsp/lsp_protocol_sematictokens.proposed.rb +340 -0
  30. data/lib/lsp/lsp_protocol_typedefinition.rb +75 -0
  31. data/lib/lsp/lsp_protocol_workspacefolders.rb +174 -0
  32. data/lib/lsp/lsp_types.rb +1534 -0
  33. data/lib/puppet-debugserver/debug_session/break_points.rb +137 -0
  34. data/lib/puppet-debugserver/debug_session/flow_control.rb +161 -0
  35. data/lib/puppet-debugserver/debug_session/hook_handlers.rb +295 -0
  36. data/lib/puppet-debugserver/debug_session/puppet_session_run_mode.rb +66 -0
  37. data/lib/puppet-debugserver/debug_session/puppet_session_state.rb +122 -0
  38. data/lib/puppet-debugserver/hooks.rb +132 -0
  39. data/lib/puppet-debugserver/message_handler.rb +277 -0
  40. data/lib/puppet-debugserver/puppet_debug_session.rb +541 -0
  41. data/lib/puppet-debugserver/puppet_monkey_patches.rb +118 -0
  42. data/lib/puppet-languageserver/client_session_state.rb +119 -0
  43. data/lib/puppet-languageserver/crash_dump.rb +50 -0
  44. data/lib/puppet-languageserver/epp/validation_provider.rb +34 -0
  45. data/lib/puppet-languageserver/facter_helper.rb +25 -0
  46. data/lib/puppet-languageserver/global_queues/sidecar_queue.rb +205 -0
  47. data/lib/puppet-languageserver/global_queues/single_instance_queue.rb +126 -0
  48. data/lib/puppet-languageserver/global_queues/validation_queue.rb +102 -0
  49. data/lib/puppet-languageserver/global_queues.rb +16 -0
  50. data/lib/puppet-languageserver/manifest/completion_provider.rb +331 -0
  51. data/lib/puppet-languageserver/manifest/definition_provider.rb +99 -0
  52. data/lib/puppet-languageserver/manifest/document_symbol_provider.rb +228 -0
  53. data/lib/puppet-languageserver/manifest/folding_provider.rb +226 -0
  54. data/lib/puppet-languageserver/manifest/format_on_type_provider.rb +143 -0
  55. data/lib/puppet-languageserver/manifest/hover_provider.rb +221 -0
  56. data/lib/puppet-languageserver/manifest/signature_provider.rb +169 -0
  57. data/lib/puppet-languageserver/manifest/validation_provider.rb +127 -0
  58. data/lib/puppet-languageserver/message_handler.rb +462 -0
  59. data/lib/puppet-languageserver/providers.rb +18 -0
  60. data/lib/puppet-languageserver/puppet_helper.rb +108 -0
  61. data/lib/puppet-languageserver/puppet_lexer_helper.rb +55 -0
  62. data/lib/puppet-languageserver/puppet_monkey_patches.rb +39 -0
  63. data/lib/puppet-languageserver/puppet_parser_helper.rb +212 -0
  64. data/lib/puppet-languageserver/puppetfile/validation_provider.rb +185 -0
  65. data/lib/puppet-languageserver/server_capabilities.rb +48 -0
  66. data/lib/puppet-languageserver/session_state/document_store.rb +272 -0
  67. data/lib/puppet-languageserver/session_state/language_client.rb +239 -0
  68. data/lib/puppet-languageserver/session_state/object_cache.rb +162 -0
  69. data/lib/puppet-languageserver/sidecar_protocol.rb +532 -0
  70. data/lib/puppet-languageserver/uri_helper.rb +46 -0
  71. data/lib/puppet-languageserver-sidecar/cache/base.rb +36 -0
  72. data/lib/puppet-languageserver-sidecar/cache/filesystem.rb +111 -0
  73. data/lib/puppet-languageserver-sidecar/cache/null.rb +27 -0
  74. data/lib/puppet-languageserver-sidecar/facter_helper.rb +41 -0
  75. data/lib/puppet-languageserver-sidecar/puppet_environment_monkey_patches.rb +52 -0
  76. data/lib/puppet-languageserver-sidecar/puppet_helper.rb +281 -0
  77. data/lib/puppet-languageserver-sidecar/puppet_modulepath_monkey_patches.rb +146 -0
  78. data/lib/puppet-languageserver-sidecar/puppet_monkey_patches.rb +9 -0
  79. data/lib/puppet-languageserver-sidecar/puppet_parser_helper.rb +77 -0
  80. data/lib/puppet-languageserver-sidecar/puppet_strings_helper.rb +399 -0
  81. data/lib/puppet-languageserver-sidecar/puppet_strings_monkey_patches.rb +16 -0
  82. data/lib/puppet-languageserver-sidecar/sidecar_protocol_extensions.rb +16 -0
  83. data/lib/puppet-languageserver-sidecar/workspace.rb +89 -0
  84. data/lib/puppet_debugserver.rb +164 -0
  85. data/lib/puppet_editor_services/connection/base.rb +62 -0
  86. data/lib/puppet_editor_services/connection/stdio.rb +25 -0
  87. data/lib/puppet_editor_services/connection/tcp.rb +34 -0
  88. data/lib/puppet_editor_services/handler/base.rb +16 -0
  89. data/lib/puppet_editor_services/handler/debug_adapter.rb +63 -0
  90. data/lib/puppet_editor_services/handler/json_rpc.rb +133 -0
  91. data/lib/puppet_editor_services/logging.rb +45 -0
  92. data/lib/puppet_editor_services/protocol/base.rb +27 -0
  93. data/lib/puppet_editor_services/protocol/debug_adapter.rb +135 -0
  94. data/lib/puppet_editor_services/protocol/debug_adapter_messages.rb +171 -0
  95. data/lib/puppet_editor_services/protocol/json_rpc.rb +241 -0
  96. data/lib/puppet_editor_services/protocol/json_rpc_messages.rb +200 -0
  97. data/lib/puppet_editor_services/server/base.rb +42 -0
  98. data/lib/puppet_editor_services/server/stdio.rb +85 -0
  99. data/lib/puppet_editor_services/server/tcp.rb +349 -0
  100. data/lib/puppet_editor_services/server.rb +15 -0
  101. data/lib/puppet_editor_services/version.rb +36 -0
  102. data/lib/puppet_editor_services.rb +8 -0
  103. data/lib/puppet_languageserver.rb +263 -0
  104. data/lib/puppet_languageserver_sidecar.rb +361 -0
  105. data/puppet-debugserver +11 -0
  106. data/puppet-editor-services.gemspec +29 -0
  107. data/puppet-languageserver +15 -0
  108. data/puppet-languageserver-sidecar +14 -0
  109. metadata +240 -0
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'puppet_editor_services/logging'
5
+ require 'puppet_editor_services/protocol/debug_adapter_messages'
6
+ require 'puppet_editor_services/protocol/base'
7
+
8
+ module PuppetEditorServices
9
+ module Protocol
10
+ class DebugAdapter < ::PuppetEditorServices::Protocol::Base
11
+ KEY_TYPE = 'type'
12
+
13
+ def initialize(connection)
14
+ super
15
+
16
+ @state = :data
17
+ @buffer = []
18
+
19
+ @request_sequence_id = 0
20
+ # @requests = {}
21
+ @request_seq_mutex = Mutex.new
22
+ end
23
+
24
+ def extract_headers(raw_header)
25
+ header = {}
26
+ raw_header.split("\r\n").each do |item|
27
+ name, value = item.split(':', 2)
28
+
29
+ if name.casecmp('Content-Length').zero?
30
+ header['Content-Length'] = value.strip.to_i
31
+ elsif name.casecmp('Content-Type').zero?
32
+ header['Content-Length'] = value.strip
33
+ else
34
+ raise("Unknown header #{name} in JSON message")
35
+ end
36
+ end
37
+ header
38
+ end
39
+
40
+ def receive_data(data)
41
+ # Inspired by https://github.com/PowerShell/PowerShellEditorServices/blob/dba65155c38d3d9eeffae5f0358b5a3ad0215fac/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageReader.cs
42
+ return if data.empty?
43
+ return if @state == :ignore
44
+
45
+ # TODO: Thread/Atomic safe? probably not
46
+ @buffer += data.bytes.to_a
47
+
48
+ while @buffer.length > 4
49
+ # Check if we have enough data for the headers
50
+ # Need to find the first instance of '\r\n\r\n'
51
+ offset = 0
52
+ while offset < @buffer.length - 4
53
+ break if @buffer[offset] == 13 && @buffer[offset + 1] == 10 && @buffer[offset + 2] == 13 && @buffer[offset + 3] == 10
54
+
55
+ offset += 1
56
+ end
57
+ return unless offset < @buffer.length - 4
58
+
59
+ # Extract the headers
60
+ raw_header = @buffer.slice(0, offset).pack('C*').force_encoding('ASCII') # Note the headers are always ASCII encoded
61
+ headers = extract_headers(raw_header)
62
+ raise('Missing Content-Length header') if headers['Content-Length'].nil?
63
+
64
+ # Now we have the headers and the content length, do we have enough data now
65
+ minimum_buf_length = offset + 3 + headers['Content-Length'] + 1 # Need to add one as we're converting from offset (zero based) to length (1 based) arrays
66
+ return if @buffer.length < minimum_buf_length
67
+
68
+ # Extract the message content
69
+ content = @buffer.slice(offset + 3 + 1, headers['Content-Length']).pack('C*').force_encoding('utf-8') # TODO: default is utf-8. Need to enode based on Content-Type
70
+ # Purge the buffer
71
+ @buffer = @buffer.slice(minimum_buf_length, @buffer.length - minimum_buf_length)
72
+ @buffer = [] if @buffer.nil?
73
+
74
+ PuppetEditorServices.log_message(:debug, "--- INBOUND\n#{content}\n---")
75
+ receive_json_message_as_string(content)
76
+ end
77
+ end
78
+
79
+ def send_json_string(string)
80
+ PuppetEditorServices.log_message(:debug, "--- OUTBOUND\n#{string}\n---")
81
+
82
+ size = string.bytesize if string.respond_to?(:bytesize)
83
+ connection.send_data "Content-Length: #{size}\r\n\r\n" + string
84
+ end
85
+
86
+ def encode_and_send(object)
87
+ # Inject the sequence ID.
88
+ raise "#{object.class} is not a PuppetEditorServices::Protocol::DebugAdapterMessages::ProtocolMessage" unless object.is_a?(PuppetEditorServices::Protocol::DebugAdapterMessages::ProtocolMessage)
89
+
90
+ object.seq = next_sequence_id!
91
+ send_json_string(::JSON.generate(object))
92
+ end
93
+
94
+ # Seperate method so async JSON processing can be supported.
95
+ def receive_json_message_as_string(content)
96
+ json_obj = ::JSON.parse(content)
97
+ return receive_json_message_as_hash(json_obj) if json_obj.is_a?(Hash)
98
+ return unless json_obj.is_a?(Array)
99
+
100
+ # Batch: multiple requests/notifications in an array.
101
+ # NOTE: Not implemented as it doesn't make sense using JSON RPC over pure TCP / UnixSocket.
102
+
103
+ PuppetEditorServices.log_message(:error, 'Batch request received but not implemented')
104
+ send_json_string BATCH_NOT_SUPPORTED_RESPONSE
105
+
106
+ connection.close_after_writing
107
+ @state = :ignore
108
+ end
109
+
110
+ def receive_json_message_as_hash(json_obj)
111
+ # There's no need to convert it to an object quite yet
112
+ # Need to validate that this is indeed a valid message
113
+ unless json_obj[KEY_TYPE] == 'request'
114
+ PuppetEditorServices.log_message(:error, "Unknown protocol message type #{json_obj[KEY_TYPE]}")
115
+ return false
116
+ end
117
+
118
+ handler.handle(PuppetEditorServices::Protocol::DebugAdapterMessages::Request.new(json_obj))
119
+ true
120
+ end
121
+
122
+ private
123
+
124
+ def next_sequence_id!
125
+ value = nil
126
+ @request_seq_mutex.synchronize do
127
+ value = @request_sequence_id
128
+ # TODO: Do we care about integer overflow? Probably not
129
+ @request_sequence_id += 1
130
+ end
131
+ value
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PuppetEditorServices
4
+ module Protocol
5
+ module DebugAdapterMessages
6
+ # Protocol message primitives
7
+ # interface ProtocolMessage {
8
+ # /** Sequence number. */
9
+ # seq: number;
10
+ # /** Message type.
11
+ # Values: 'request', 'response', 'event', etc.
12
+ # */
13
+ # type: string;
14
+ # }
15
+ class ProtocolMessage
16
+ attr_accessor :seq, :type # type: number # type: string
17
+
18
+ def initialize(initial_hash = nil)
19
+ from_h!(initial_hash)
20
+ end
21
+
22
+ def to_json(*options)
23
+ to_h.to_json(options)
24
+ end
25
+
26
+ def to_h
27
+ {
28
+ 'seq' => seq,
29
+ 'type' => type
30
+ }
31
+ end
32
+
33
+ def from_h!(value)
34
+ value = {} if value.nil?
35
+ self.seq = value['seq']
36
+ self.type = value['type']
37
+ self
38
+ end
39
+ end
40
+
41
+ # interface Request extends ProtocolMessage {
42
+ # /** The command to execute. */
43
+ # command: string;
44
+ # /** Object containing arguments for the command. */
45
+ # arguments?: any;
46
+ # }
47
+ class Request < ProtocolMessage
48
+ attr_accessor :command, :arguments # type: string # type: any
49
+
50
+ def initialize(initial_hash = nil)
51
+ super
52
+ self.type = 'request'
53
+ end
54
+
55
+ def from_h!(value)
56
+ value = {} if value.nil?
57
+ super
58
+ self.command = value['command']
59
+ self.arguments = value['arguments']
60
+ self
61
+ end
62
+
63
+ def to_h
64
+ super.tap do |hash|
65
+ hash['command'] = command
66
+ hash['arguments'] = arguments unless arguments.nil?
67
+ end
68
+ end
69
+ end
70
+
71
+ # interface Event extends ProtocolMessage {
72
+ # /** Type of event. */
73
+ # event: string;
74
+ # /** Event-specific information. */
75
+ # body?: any;
76
+ # }
77
+ class Event < ProtocolMessage
78
+ attr_accessor :event, :body # type: string # type: any
79
+
80
+ def initialize(initial_hash = nil)
81
+ super
82
+ self.type = 'event'
83
+ end
84
+
85
+ def from_h!(value)
86
+ value = {} if value.nil?
87
+ super
88
+ self.event = value['event']
89
+ self.body = value['body']
90
+ self
91
+ end
92
+
93
+ def to_h
94
+ super.tap do |hash|
95
+ hash['event'] = event
96
+ hash['body'] = body unless body.nil?
97
+ end
98
+ end
99
+ end
100
+
101
+ # interface Response extends ProtocolMessage {
102
+ # /** Sequence number of the corresponding request. */
103
+ # request_seq: number;
104
+ # /** Outcome of the request. */
105
+ # success: boolean;
106
+ # /** The command requested. */
107
+ # command: string;
108
+ # /** Contains error message if success == false. */
109
+ # message?: string;
110
+ # /** Contains request result if success is true and optional error details if success is false. */
111
+ # body?: any;
112
+ # }
113
+ class Response < ProtocolMessage
114
+ attr_accessor :request_seq, :success, :command, :message, :body # type: number # type: boolean # type: string # type: string # type: any
115
+
116
+ def initialize(initial_hash = nil)
117
+ super
118
+ from_h!(initial_hash) unless initial_hash.nil?
119
+ self.type = 'response'
120
+ end
121
+
122
+ def from_h!(value)
123
+ value = {} if value.nil?
124
+ super
125
+ self.request_seq = value['request_seq']
126
+ self.success = value['success']
127
+ self.command = value['command']
128
+ self.message = value['message']
129
+ self.body = value['body']
130
+ self
131
+ end
132
+
133
+ def to_h
134
+ super.tap do |hash|
135
+ hash['request_seq'] = request_seq
136
+ hash['success'] = success
137
+ hash['command'] = command
138
+ hash['message'] = message unless message.nil?
139
+ hash['body'] = body unless body.nil?
140
+ end
141
+ end
142
+ end
143
+
144
+ # Static message generators
145
+ def self.reply_error(request, message = nil, message_object = nil)
146
+ Response.new(
147
+ 'request_seq' => request.seq,
148
+ 'command' => request.command,
149
+ 'success' => false
150
+ ).tap do |resp|
151
+ resp.message = message unless message.nil?
152
+ resp.body = { 'error' => message_object } unless message_object.nil?
153
+ end
154
+ end
155
+
156
+ def self.reply_success(request, body_content = nil)
157
+ Response.new(
158
+ 'request_seq' => request.seq,
159
+ 'command' => request.command,
160
+ 'success' => true
161
+ ).tap { |resp| resp.body = body_content unless body_content.nil? }
162
+ end
163
+
164
+ def self.new_event(event_name, body_content = nil)
165
+ Event.new(
166
+ 'event' => event_name
167
+ ).tap { |resp| resp.body = body_content unless body_content.nil? }
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,241 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'puppet_editor_services/logging'
5
+ require 'puppet_editor_services/protocol/json_rpc_messages'
6
+ require 'puppet_editor_services/protocol/base'
7
+
8
+ module PuppetEditorServices
9
+ module Protocol
10
+ class JsonRPC < ::PuppetEditorServices::Protocol::Base
11
+ CODE_INVALID_JSON = -32_700
12
+ MSG_INVALID_JSON = 'invalid JSON'
13
+
14
+ CODE_INVALID_REQUEST = -32_600
15
+ MSG_INVALID_REQ_JSONRPC = "invalid request: doesn't include \"jsonrpc\": \"2.0\""
16
+ MSG_INVALID_REQ_ID = 'invalid request: wrong id'
17
+ MSG_INVALID_REQ_METHOD = 'invalid request: wrong method'
18
+ MSG_INVALID_REQ_PARAMS = 'invalid request: wrong params'
19
+
20
+ CODE_METHOD_NOT_FOUND = -32_601
21
+ MSG_METHOD_NOT_FOUND = 'method not found'
22
+
23
+ CODE_INVALID_PARAMS = -32_602
24
+ MSG_INVALID_PARAMS = 'invalid parameter(s)'
25
+
26
+ CODE_INTERNAL_ERROR = -32_603
27
+ MSG_INTERNAL_ERROR = 'internal error'
28
+
29
+ PARSING_ERROR_RESPONSE = '{"jsonrpc":"2.0","id":null,"error":{' \
30
+ "\"code\":#{CODE_INVALID_JSON}," \
31
+ "\"message\":\"#{MSG_INVALID_JSON}\"}}"
32
+
33
+ BATCH_NOT_SUPPORTED_RESPONSE = '{"jsonrpc":"2.0","id":null,"error":{' \
34
+ '"code":-32099,' \
35
+ '"message":"batch mode not implemented"}}'
36
+
37
+ KEY_JSONRPC = 'jsonrpc'
38
+ JSONRPC_VERSION = '2.0'
39
+ KEY_ID = 'id'
40
+ KEY_METHOD = 'method'
41
+ KEY_PARAMS = 'params'
42
+ KEY_RESULT = 'result'
43
+ KEY_ERROR = 'error'
44
+ KEY_CODE = 'code'
45
+ KEY_MESSAGE = 'message'
46
+
47
+ def initialize(connection)
48
+ super
49
+
50
+ @state = :data
51
+ @buffer = []
52
+
53
+ @request_sequence_id = 0
54
+ @requests = {}
55
+ @request_mutex = Mutex.new
56
+ end
57
+
58
+ def extract_headers(raw_header)
59
+ header = {}
60
+ raw_header.split("\r\n").each do |item|
61
+ name, value = item.split(':', 2)
62
+
63
+ if name.casecmp('Content-Length').zero?
64
+ header['Content-Length'] = value.strip.to_i
65
+ elsif name.casecmp('Content-Type').zero?
66
+ header['Content-Length'] = value.strip
67
+ else
68
+ raise("Unknown header #{name} in JSON message")
69
+ end
70
+ end
71
+ header
72
+ end
73
+
74
+ def receive_data(data)
75
+ # Inspired by https://github.com/PowerShell/PowerShellEditorServices/blob/dba65155c38d3d9eeffae5f0358b5a3ad0215fac/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageReader.cs
76
+ return if data.empty?
77
+ return if @state == :ignore
78
+
79
+ # TODO: Thread/Atomic safe? probably not
80
+ @buffer += data.bytes.to_a
81
+
82
+ while @buffer.length > 4
83
+ # Check if we have enough data for the headers
84
+ # Need to find the first instance of '\r\n\r\n'
85
+ offset = 0
86
+ while offset < @buffer.length - 4
87
+ break if @buffer[offset] == 13 && @buffer[offset + 1] == 10 && @buffer[offset + 2] == 13 && @buffer[offset + 3] == 10
88
+
89
+ offset += 1
90
+ end
91
+ return unless offset < @buffer.length - 4
92
+
93
+ # Extract the headers
94
+ raw_header = @buffer.slice(0, offset).pack('C*').force_encoding('ASCII') # Note the headers are always ASCII encoded
95
+ headers = extract_headers(raw_header)
96
+ raise('Missing Content-Length header') if headers['Content-Length'].nil?
97
+
98
+ # Now we have the headers and the content length, do we have enough data now
99
+ minimum_buf_length = offset + 3 + headers['Content-Length'] + 1 # Need to add one as we're converting from offset (zero based) to length (1 based) arrays
100
+ return if @buffer.length < minimum_buf_length
101
+
102
+ # Extract the message content
103
+ content = @buffer.slice(offset + 3 + 1, headers['Content-Length']).pack('C*').force_encoding('utf-8') # TODO: default is utf-8. Need to enode based on Content-Type
104
+ # Purge the buffer
105
+ @buffer = @buffer.slice(minimum_buf_length, @buffer.length - minimum_buf_length)
106
+ @buffer = [] if @buffer.nil?
107
+
108
+ PuppetEditorServices.log_message(:debug, "--- INBOUND\n#{content}\n---")
109
+ receive_json_message_as_string(content)
110
+ end
111
+ end
112
+
113
+ def send_json_string(string)
114
+ PuppetEditorServices.log_message(:debug, "--- OUTBOUND\n#{string}\n---")
115
+
116
+ size = string.bytesize if string.respond_to?(:bytesize)
117
+ connection.send_data "Content-Length: #{size}\r\n\r\n" + string
118
+ end
119
+
120
+ def encode_and_send(object)
121
+ send_json_string(::JSON.generate(object))
122
+ end
123
+
124
+ # Seperate method so async JSON processing can be supported.
125
+ def receive_json_message_as_string(content)
126
+ json_obj = ::JSON.parse(content)
127
+ return receive_json_message_as_hash(json_obj) if json_obj.is_a?(Hash)
128
+ return unless json_obj.is_a?(Array)
129
+
130
+ # Batch: multiple requests/notifications in an array.
131
+ # NOTE: Not implemented as it doesn't make sense using JSON RPC over pure TCP / UnixSocket.
132
+
133
+ PuppetEditorServices.log_message(:error, 'Batch request received but not implemented')
134
+ send_json_string BATCH_NOT_SUPPORTED_RESPONSE
135
+
136
+ connection.close_after_writing
137
+ @state = :ignore
138
+ end
139
+
140
+ def receive_json_message_as_hash(json_obj)
141
+ # There's no need to convert it to an object quite yet
142
+ # Need to validate that this is indeed a valid message
143
+ id = json_obj[KEY_ID]
144
+ unless json_obj[KEY_JSONRPC] == JSONRPC_VERSION
145
+ PuppetEditorServices.log_message(:error, 'Invalid JSON RPC version')
146
+ reply_error id, CODE_INVALID_REQUEST, MSG_INVALID_REQ_JSONRPC
147
+ return false
148
+ end
149
+
150
+ # Requests must have an ID and Method
151
+ is_request = json_obj.key?(KEY_ID) && json_obj.key?(KEY_METHOD)
152
+ # Notifications must have a Method but no ID
153
+ is_notification = json_obj.key?(KEY_METHOD) && !json_obj.key?(KEY_ID)
154
+ # Responses must have an ID, no Method but one of Result or Error
155
+ is_response = json_obj.key?(KEY_ID) && !json_obj.key?(KEY_METHOD) && (json_obj.key?(KEY_RESULT) || json_obj.key?(KEY_ERROR))
156
+
157
+ # The 'params' attribute must be a hash or an array
158
+ if (params = json_obj[KEY_PARAMS]) && !(params.is_a?(Array) || params.is_a?(Hash))
159
+ reply_error id, CODE_INVALID_REQUEST, MSG_INVALID_REQ_PARAMS
160
+ return false
161
+ end
162
+
163
+ # Requests and Responses must have an ID that is either a string or integer
164
+ if (is_request || is_response) && !(id.is_a?(String) || id.is_a?(Integer))
165
+ reply_error nil, CODE_INVALID_REQUEST, MSG_INVALID_REQ_ID
166
+ return false
167
+ end
168
+
169
+ # Requests and Notifications must have a method
170
+ if (is_request || is_notification) && !((json_obj[KEY_METHOD]).is_a? String)
171
+ reply_error id, CODE_INVALID_REQUEST, MSG_INVALID_REQ_METHOD
172
+ return false
173
+ end
174
+
175
+ # Responses must have a matching request originating from this instance
176
+ # Otherwise ignore it
177
+ if is_response
178
+ original_request = client_request!(json_obj[KEY_ID])
179
+ return false if original_request.nil?
180
+ end
181
+
182
+ if is_request
183
+ handler.handle(PuppetEditorServices::Protocol::JsonRPCMessages::RequestMessage.new(json_obj))
184
+ return true
185
+ elsif is_notification
186
+ handler.handle(PuppetEditorServices::Protocol::JsonRPCMessages::NotificationMessage.new(json_obj))
187
+ return true
188
+ elsif is_response
189
+ # Responses are special as they need the context of the original request
190
+ handler.handle(PuppetEditorServices::Protocol::JsonRPCMessages::ResponseMessage.new(json_obj), request: original_request)
191
+ return true
192
+ end
193
+ false
194
+ end
195
+
196
+ def reply_error(id, code, message)
197
+ send_json_string ::PuppetEditorServices::Protocol::JsonRPCMessages.reply_error_by_id(id, code, message).to_json
198
+ end
199
+
200
+ # region Server-to-Client request/response methods
201
+ def send_client_request(rpc_method, params)
202
+ request = ::PuppetEditorServices::Protocol::JsonRPCMessages.new_request(client_request_id!, rpc_method, params)
203
+ encode_and_send(request)
204
+ add_client_request(request)
205
+ request.id
206
+ end
207
+
208
+ # Thread-safe way to get a new request id
209
+ def client_request_id!
210
+ value = nil
211
+ @request_mutex.synchronize do
212
+ value = @request_sequence_id
213
+ @request_sequence_id += 1
214
+ end
215
+ value
216
+ end
217
+
218
+ # Stores the request so it can later be correlated with an
219
+ # incoming repsonse
220
+ def add_client_request(request)
221
+ @request_mutex.synchronize do
222
+ @requests[request.id] = request
223
+ end
224
+ end
225
+
226
+ # Retrieve the request to a client. Note that this removes it
227
+ # from the requests queue.
228
+ def client_request!(id)
229
+ value = nil
230
+ @request_mutex.synchronize do
231
+ unless @requests[id].nil?
232
+ value = @requests[id]
233
+ @requests.delete(id)
234
+ end
235
+ end
236
+ value
237
+ end
238
+ # endregion
239
+ end
240
+ end
241
+ end