json_rpc_kit 0.9.0.rc1
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.
- checksums.yaml +7 -0
- data/lib/json_rpc_kit/endpoint.rb +758 -0
- data/lib/json_rpc_kit/errors.rb +82 -0
- data/lib/json_rpc_kit/helpers.rb +81 -0
- data/lib/json_rpc_kit/service.rb +725 -0
- data/lib/json_rpc_kit/transport_options.rb +220 -0
- data/lib/json_rpc_kit/version.rb +5 -0
- data/lib/json_rpc_kit.rb +28 -0
- metadata +62 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module JsonRpcKit
|
|
6
|
+
# JSON-RPC Error handling
|
|
7
|
+
class Error < StandardError
|
|
8
|
+
attr_reader :code, :data
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
# @!visibility private
|
|
12
|
+
def raise_error(code:, message:, data: nil, **)
|
|
13
|
+
error_class = ERROR_CODES.fetch(code, JsonRpcKit::Error)
|
|
14
|
+
raise error_class.new(message, code:, data:) if error_class <= JsonRpcKit::Error
|
|
15
|
+
|
|
16
|
+
raise error_class, message
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @!visibility private
|
|
20
|
+
def rescue_error(id, error)
|
|
21
|
+
code = error.code if error.respond_to?(:code)
|
|
22
|
+
code, = ERROR_CODES.detect { |_code, error_class| error.is_a?(error_class) } unless code.is_a?(Integer)
|
|
23
|
+
data = error.data if error.respond_to?(:data)
|
|
24
|
+
|
|
25
|
+
# If we did not find a code then this is some other kind of error, record it with Internal error code
|
|
26
|
+
# and pass class name in the data.
|
|
27
|
+
data ||= { class_name: error.class.name } unless code
|
|
28
|
+
code ||= -32_603
|
|
29
|
+
|
|
30
|
+
{ jsonrpc: '2.0', id: id, error: { code: code, message: error.message, data: data }.compact }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Create a JSON-RPC error
|
|
35
|
+
# @param message [String]
|
|
36
|
+
# @param code [Integer]
|
|
37
|
+
# @param data [Object] ignored any kw_data is passed
|
|
38
|
+
# @param kw_data [Hash] data as keyword arguments
|
|
39
|
+
def initialize(message, code:, data: nil, **kw_data)
|
|
40
|
+
super(message)
|
|
41
|
+
@code = code
|
|
42
|
+
@data = kw_data.any? ? kw_data : data
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Invalid Request
|
|
47
|
+
class InvalidRequest < Error
|
|
48
|
+
CODE = -32_600
|
|
49
|
+
def initialize(message, code: CODE, data: nil, **kw_data)
|
|
50
|
+
super
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Internal Error
|
|
55
|
+
class InternalError < Error
|
|
56
|
+
CODE = -32_603
|
|
57
|
+
def initialize(message, code: CODE, data: nil, **kw_data)
|
|
58
|
+
super
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Invalid Response
|
|
63
|
+
class InvalidResponse < InternalError; end
|
|
64
|
+
|
|
65
|
+
# Default timeout error for JSON-RPC requests
|
|
66
|
+
class TimeoutError < Error
|
|
67
|
+
CODE = -32_070
|
|
68
|
+
def initialize(message, code: CODE, data: nil, **kw_data)
|
|
69
|
+
super
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Mapping of JSON-RPC error codes to Ruby error classes
|
|
74
|
+
ERROR_CODES = {
|
|
75
|
+
-32_700 => ::JSON::ParserError,
|
|
76
|
+
-32_601 => ::NoMethodError,
|
|
77
|
+
-32_602 => ::ArgumentError,
|
|
78
|
+
InvalidRequest::CODE => InvalidRequest,
|
|
79
|
+
InternalError::CODE => InternalError,
|
|
80
|
+
TimeoutError::CODE => TimeoutError
|
|
81
|
+
}.freeze
|
|
82
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JsonRpcKit
|
|
4
|
+
# Helper functions
|
|
5
|
+
module Helpers
|
|
6
|
+
# rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
7
|
+
# @!visibility private
|
|
8
|
+
def parse(json, content_type: nil, response: false, &)
|
|
9
|
+
raise JSON::ParserError unless !content_type || content_type.start_with?(CONTENT_TYPE)
|
|
10
|
+
|
|
11
|
+
# TODO: Need more unit tests on parsing
|
|
12
|
+
|
|
13
|
+
JSON.parse(json, symbolize_names: true).tap do |rpc|
|
|
14
|
+
error_class = response ? JsonRpcKit::InvalidResponse : JsonRpcKit::InvalidRequest
|
|
15
|
+
|
|
16
|
+
case rpc
|
|
17
|
+
when Hash
|
|
18
|
+
raise error_class, 'Invalid JSON-RPC' unless rpc.key?(:jsonrpc)
|
|
19
|
+
|
|
20
|
+
yield false, **rpc
|
|
21
|
+
when Array
|
|
22
|
+
raise error_class, 'Invalid JSON-RPC batch' unless rpc.size.positive?
|
|
23
|
+
|
|
24
|
+
rpc.each do |item|
|
|
25
|
+
raise error_class, 'Invalid JSON-RPC batch_item' unless item.is_a?(Hash) && item.key?(:jsonrpc)
|
|
26
|
+
|
|
27
|
+
yield true, **item
|
|
28
|
+
end
|
|
29
|
+
else
|
|
30
|
+
raise error_class, "Invalid JSON-RPC - expected List or Object, got #{rpc.class.name}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def parse_request(json, content_type: CONTENT_TYPE)
|
|
36
|
+
parse(json, content_type:) do |_batch_item, id: nil, params: nil, method: nil, **_|
|
|
37
|
+
raise InvalidRequest, 'Invalid method' unless method.is_a?(String)
|
|
38
|
+
raise InvalidRequest, 'Invalid params' unless params.nil? || params.is_a?(Hash) || params.is_a?(Array)
|
|
39
|
+
raise InvalidRequest, 'Invalid id' unless id.nil? || id.is_a?(String) || id.is_a?(Integer)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def parse_response(json, content_type: CONTENT_TYPE, batch: false)
|
|
44
|
+
parse(json, content_type:, response: true) do |batch_item, id: nil, error: nil, **optional|
|
|
45
|
+
raise InvalidResponse, 'JSON-RPC response missing :id' unless id || (batch && !batch_item && error)
|
|
46
|
+
|
|
47
|
+
if error
|
|
48
|
+
unless %i[code message].all? { |k| error.include?(k) }
|
|
49
|
+
raise InvalidResponse, 'JSON-RPC response needs :code and :message'
|
|
50
|
+
end
|
|
51
|
+
else
|
|
52
|
+
raise InvalidResponse, 'JSON-RPC response missing :error or :result' unless optional.include?(:result)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if batch_item
|
|
56
|
+
# A batch item, but not expecting a batch response
|
|
57
|
+
raise InvalidResponse, 'Expected JSON-RPC Object response got List' unless batch
|
|
58
|
+
elsif batch
|
|
59
|
+
# Not a batch item, but expecting a batch. Can be a single error, which is raised immediately
|
|
60
|
+
# because the individual requests have not been fulfilled.
|
|
61
|
+
Errors.raise_error(**error) if error
|
|
62
|
+
raise InvalidResponse, 'Expected JSON-RPC List response or Error'
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
rescue JSON::ParserError
|
|
66
|
+
# Client-side parse error - the response from server is not valid JSON
|
|
67
|
+
raise InvalidResponse, 'Unable to parse JSON-RPC response'
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# rubocop:enable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
71
|
+
|
|
72
|
+
# Helper to convert ruby `method_name` to JSON-RPC `<namespace>.methodName`
|
|
73
|
+
# @param method [Symbol]
|
|
74
|
+
# @param namespace [String]
|
|
75
|
+
# @param camelize [Boolean]
|
|
76
|
+
def ruby_to_json_rpc(method, namespace: nil, camelize: true)
|
|
77
|
+
name = camelize ? method.to_s.gsub(/_([a-z])/) { it[1].upcase } : method.to_s
|
|
78
|
+
namespace ? "#{namespace}.#{name}" : name
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|