Class: PuppetEditorServices::Protocol::JsonRPC
- Defined in:
- lib/puppet_editor_services/protocol/json_rpc.rb
Constant Summary collapse
- CODE_INVALID_JSON =
-32_700
- MSG_INVALID_JSON =
'invalid JSON'- CODE_INVALID_REQUEST =
-32_600
- MSG_INVALID_REQ_JSONRPC =
"invalid request: doesn't include \"jsonrpc\": \"2.0\""- MSG_INVALID_REQ_ID =
'invalid request: wrong id'- MSG_INVALID_REQ_METHOD =
'invalid request: wrong method'- MSG_INVALID_REQ_PARAMS =
'invalid request: wrong params'- CODE_METHOD_NOT_FOUND =
-32_601
- MSG_METHOD_NOT_FOUND =
'method not found'- CODE_INVALID_PARAMS =
-32_602
- MSG_INVALID_PARAMS =
'invalid parameter(s)'- CODE_INTERNAL_ERROR =
-32_603
- MSG_INTERNAL_ERROR =
'internal error'- PARSING_ERROR_RESPONSE =
'{"jsonrpc":"2.0","id":null,"error":{' \ "\"code\":#{CODE_INVALID_JSON}," \ "\"message\":\"#{MSG_INVALID_JSON}\"}}"
- BATCH_NOT_SUPPORTED_RESPONSE =
'{"jsonrpc":"2.0","id":null,"error":{' \ '"code":-32099,' \ '"message":"batch mode not implemented"}}'
- KEY_JSONRPC =
'jsonrpc'- JSONRPC_VERSION =
'2.0'- KEY_ID =
'id'- KEY_METHOD =
'method'- KEY_PARAMS =
'params'- KEY_RESULT =
'result'- KEY_ERROR =
'error'- KEY_CODE =
'code'- KEY_MESSAGE =
'message'
Instance Attribute Summary
Attributes inherited from Base
Instance Method Summary collapse
-
#add_client_request(request) ⇒ Object
Stores the request so it can later be correlated with an incoming repsonse.
-
#client_request!(id) ⇒ Object
Retrieve the request to a client.
-
#client_request_id! ⇒ Object
Thread-safe way to get a new request id.
- #encode_and_send(object) ⇒ Object
- #extract_headers(raw_header) ⇒ Object
-
#initialize(connection) ⇒ JsonRPC
constructor
A new instance of JsonRPC.
- #receive_data(data) ⇒ Object
- #receive_json_message_as_hash(json_obj) ⇒ Object
-
#receive_json_message_as_string(content) ⇒ Object
Seperate method so async JSON processing can be supported.
- #reply_error(id, code, message) ⇒ Object
-
#send_client_request(rpc_method, params) ⇒ Object
region Server-to-Client request/response methods.
- #send_json_string(string) ⇒ Object
Methods inherited from Base
#close_connection, #connection_error?
Constructor Details
#initialize(connection) ⇒ JsonRPC
Returns a new instance of JsonRPC.
47 48 49 50 51 52 53 54 55 56 |
# File 'lib/puppet_editor_services/protocol/json_rpc.rb', line 47 def initialize(connection) super @state = :data @buffer = [] @request_sequence_id = 0 @requests = {} @request_mutex = Mutex.new end |
Instance Method Details
#add_client_request(request) ⇒ Object
Stores the request so it can later be correlated with an incoming repsonse
220 221 222 223 224 |
# File 'lib/puppet_editor_services/protocol/json_rpc.rb', line 220 def add_client_request(request) @request_mutex.synchronize do @requests[request.id] = request end end |
#client_request!(id) ⇒ Object
Retrieve the request to a client. Note that this removes it from the requests queue.
228 229 230 231 232 233 234 235 236 237 |
# File 'lib/puppet_editor_services/protocol/json_rpc.rb', line 228 def client_request!(id) value = nil @request_mutex.synchronize do unless @requests[id].nil? value = @requests[id] @requests.delete(id) end end value end |
#client_request_id! ⇒ Object
Thread-safe way to get a new request id
209 210 211 212 213 214 215 216 |
# File 'lib/puppet_editor_services/protocol/json_rpc.rb', line 209 def client_request_id! value = nil @request_mutex.synchronize do value = @request_sequence_id @request_sequence_id += 1 end value end |
#encode_and_send(object) ⇒ Object
120 121 122 |
# File 'lib/puppet_editor_services/protocol/json_rpc.rb', line 120 def encode_and_send(object) send_json_string(::JSON.generate(object)) end |
#extract_headers(raw_header) ⇒ Object
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/puppet_editor_services/protocol/json_rpc.rb', line 58 def extract_headers(raw_header) header = {} raw_header.split("\r\n").each do |item| name, value = item.split(':', 2) if name.casecmp('Content-Length').zero? header['Content-Length'] = value.strip.to_i elsif name.casecmp('Content-Type').zero? header['Content-Length'] = value.strip else raise("Unknown header #{name} in JSON message") end end header end |
#receive_data(data) ⇒ Object
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/puppet_editor_services/protocol/json_rpc.rb', line 74 def receive_data(data) # Inspired by https://github.com/PowerShell/PowerShellEditorServices/blob/dba65155c38d3d9eeffae5f0358b5a3ad0215fac/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageReader.cs return if data.empty? return if @state == :ignore # TODO: Thread/Atomic safe? probably not @buffer += data.bytes.to_a while @buffer.length > 4 # Check if we have enough data for the headers # Need to find the first instance of '\r\n\r\n' offset = 0 while offset < @buffer.length - 4 break if @buffer[offset] == 13 && @buffer[offset + 1] == 10 && @buffer[offset + 2] == 13 && @buffer[offset + 3] == 10 offset += 1 end return unless offset < @buffer.length - 4 # Extract the headers raw_header = @buffer.slice(0, offset).pack('C*').force_encoding('ASCII') # Note the headers are always ASCII encoded headers = extract_headers(raw_header) raise('Missing Content-Length header') if headers['Content-Length'].nil? # Now we have the headers and the content length, do we have enough data now 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 return if @buffer.length < minimum_buf_length # Extract the message content 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 # Purge the buffer @buffer = @buffer.slice(minimum_buf_length, @buffer.length - minimum_buf_length) @buffer = [] if @buffer.nil? PuppetEditorServices.(:debug, "--- INBOUND\n#{content}\n---") (content) end end |
#receive_json_message_as_hash(json_obj) ⇒ Object
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 |
# File 'lib/puppet_editor_services/protocol/json_rpc.rb', line 140 def (json_obj) # There's no need to convert it to an object quite yet # Need to validate that this is indeed a valid message id = json_obj[KEY_ID] unless json_obj[KEY_JSONRPC] == JSONRPC_VERSION PuppetEditorServices.(:error, 'Invalid JSON RPC version') reply_error id, CODE_INVALID_REQUEST, MSG_INVALID_REQ_JSONRPC return false end # Requests must have an ID and Method is_request = json_obj.key?(KEY_ID) && json_obj.key?(KEY_METHOD) # Notifications must have a Method but no ID is_notification = json_obj.key?(KEY_METHOD) && !json_obj.key?(KEY_ID) # Responses must have an ID, no Method but one of Result or Error is_response = json_obj.key?(KEY_ID) && !json_obj.key?(KEY_METHOD) && (json_obj.key?(KEY_RESULT) || json_obj.key?(KEY_ERROR)) # The 'params' attribute must be a hash or an array if (params = json_obj[KEY_PARAMS]) && !(params.is_a?(Array) || params.is_a?(Hash)) reply_error id, CODE_INVALID_REQUEST, MSG_INVALID_REQ_PARAMS return false end # Requests and Responses must have an ID that is either a string or integer if (is_request || is_response) && !(id.is_a?(String) || id.is_a?(Integer)) reply_error nil, CODE_INVALID_REQUEST, MSG_INVALID_REQ_ID return false end # Requests and Notifications must have a method if (is_request || is_notification) && !((json_obj[KEY_METHOD]).is_a? String) reply_error id, CODE_INVALID_REQUEST, MSG_INVALID_REQ_METHOD return false end # Responses must have a matching request originating from this instance # Otherwise ignore it if is_response original_request = client_request!(json_obj[KEY_ID]) return false if original_request.nil? end if is_request handler.handle(PuppetEditorServices::Protocol::JsonRPCMessages::RequestMessage.new(json_obj)) return true elsif is_notification handler.handle(PuppetEditorServices::Protocol::JsonRPCMessages::NotificationMessage.new(json_obj)) return true elsif is_response # Responses are special as they need the context of the original request handler.handle(PuppetEditorServices::Protocol::JsonRPCMessages::ResponseMessage.new(json_obj), request: original_request) return true end false end |
#receive_json_message_as_string(content) ⇒ Object
Seperate method so async JSON processing can be supported.
125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
# File 'lib/puppet_editor_services/protocol/json_rpc.rb', line 125 def (content) json_obj = ::JSON.parse(content) return (json_obj) if json_obj.is_a?(Hash) return unless json_obj.is_a?(Array) # Batch: multiple requests/notifications in an array. # NOTE: Not implemented as it doesn't make sense using JSON RPC over pure TCP / UnixSocket. PuppetEditorServices.(:error, 'Batch request received but not implemented') send_json_string BATCH_NOT_SUPPORTED_RESPONSE connection.close_after_writing @state = :ignore end |
#reply_error(id, code, message) ⇒ Object
196 197 198 |
# File 'lib/puppet_editor_services/protocol/json_rpc.rb', line 196 def reply_error(id, code, ) send_json_string ::PuppetEditorServices::Protocol::JsonRPCMessages.reply_error_by_id(id, code, ).to_json end |
#send_client_request(rpc_method, params) ⇒ Object
region Server-to-Client request/response methods
201 202 203 204 205 206 |
# File 'lib/puppet_editor_services/protocol/json_rpc.rb', line 201 def send_client_request(rpc_method, params) request = ::PuppetEditorServices::Protocol::JsonRPCMessages.new_request(client_request_id!, rpc_method, params) encode_and_send(request) add_client_request(request) request.id end |
#send_json_string(string) ⇒ Object
113 114 115 116 117 118 |
# File 'lib/puppet_editor_services/protocol/json_rpc.rb', line 113 def send_json_string(string) PuppetEditorServices.(:debug, "--- OUTBOUND\n#{string}\n---") size = string.bytesize if string.respond_to?(:bytesize) connection.send_data "Content-Length: #{size}\r\n\r\n" + string end |