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.
@@ -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