jsonrpc-middleware 0.1.0 → 0.2.0
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 +4 -4
- data/.aiexclude +4 -0
- data/.claude/commands/document.md +105 -0
- data/.claude/docs/yard.md +602 -0
- data/.claude/settings.local.json +2 -1
- data/.env.example +5 -0
- data/CHANGELOG.md +22 -2
- data/CLAUDE.md +114 -0
- data/README.md +42 -102
- data/Rakefile +59 -1
- data/examples/README.md +37 -0
- data/examples/procedures.rb +6 -1
- data/examples/rack/README.md +26 -1
- data/examples/rack/app.rb +1 -1
- data/examples/rack-echo/README.md +23 -1
- data/examples/rack-single-file/README.md +37 -0
- data/examples/rack-single-file/config.ru +54 -0
- data/examples/rails/.gitignore +21 -0
- data/examples/rails/.ruby-version +1 -0
- data/examples/rails/Gemfile +15 -0
- data/examples/rails/Gemfile.lock +261 -0
- data/examples/rails/README.md +32 -0
- data/examples/rails/Rakefile +8 -0
- data/examples/rails/app/controllers/application_controller.rb +4 -0
- data/examples/rails/app/controllers/jsonrpc_controller.rb +44 -0
- data/examples/rails/bin/dev +4 -0
- data/examples/rails/bin/rails +6 -0
- data/examples/rails/bin/rake +6 -0
- data/examples/rails/bin/setup +28 -0
- data/examples/rails/config/application.rb +47 -0
- data/examples/rails/config/boot.rb +5 -0
- data/examples/rails/config/credentials.yml.enc +1 -0
- data/examples/rails/config/environment.rb +7 -0
- data/examples/rails/config/environments/development.rb +42 -0
- data/examples/rails/config/environments/production.rb +60 -0
- data/examples/rails/config/environments/test.rb +44 -0
- data/examples/rails/config/initializers/cors.rb +18 -0
- data/examples/rails/config/initializers/filter_parameter_logging.rb +10 -0
- data/examples/rails/config/initializers/inflections.rb +18 -0
- data/examples/rails/config/initializers/jsonrpc.rb +62 -0
- data/examples/rails/config/locales/en.yml +31 -0
- data/examples/rails/config/puma.rb +40 -0
- data/examples/rails/config/routes.rb +14 -0
- data/examples/rails/config.ru +8 -0
- data/examples/rails/public/robots.txt +1 -0
- data/examples/rails-single-file/config.ru +71 -0
- data/examples/sinatra-classic/Gemfile +9 -0
- data/examples/sinatra-classic/Gemfile.lock +95 -0
- data/examples/sinatra-classic/README.md +32 -0
- data/examples/sinatra-classic/app.rb +54 -0
- data/examples/sinatra-classic/config.ru +6 -0
- data/examples/sinatra-modular/Gemfile +9 -0
- data/examples/sinatra-modular/Gemfile.lock +95 -0
- data/examples/sinatra-modular/README.md +32 -0
- data/examples/sinatra-modular/app.rb +57 -0
- data/examples/sinatra-modular/config.ru +6 -0
- data/lib/jsonrpc/batch_request.rb +67 -2
- data/lib/jsonrpc/batch_response.rb +56 -0
- data/lib/jsonrpc/configuration.rb +156 -14
- data/lib/jsonrpc/error.rb +83 -2
- data/lib/jsonrpc/errors/internal_error.rb +14 -2
- data/lib/jsonrpc/errors/invalid_params_error.rb +13 -1
- data/lib/jsonrpc/errors/invalid_request_error.rb +8 -0
- data/lib/jsonrpc/errors/method_not_found_error.rb +8 -0
- data/lib/jsonrpc/errors/parse_error.rb +8 -0
- data/lib/jsonrpc/helpers.rb +212 -21
- data/lib/jsonrpc/middleware.rb +211 -5
- data/lib/jsonrpc/notification.rb +58 -0
- data/lib/jsonrpc/parser.rb +30 -0
- data/lib/jsonrpc/railtie.rb +57 -0
- data/lib/jsonrpc/request.rb +68 -1
- data/lib/jsonrpc/response.rb +76 -0
- data/lib/jsonrpc/validator.rb +67 -6
- data/lib/jsonrpc/version.rb +11 -1
- data/lib/jsonrpc.rb +53 -1
- metadata +49 -1
data/lib/jsonrpc/middleware.rb
CHANGED
@@ -3,17 +3,66 @@
|
|
3
3
|
require 'rack'
|
4
4
|
|
5
5
|
module JSONRPC
|
6
|
-
#
|
6
|
+
# Rack middleware for handling JSON-RPC 2.0 requests
|
7
|
+
#
|
8
|
+
# @api public
|
9
|
+
#
|
10
|
+
# This middleware intercepts HTTP POST requests at a specified path and processes them
|
11
|
+
# as JSON-RPC 2.0 messages. It handles parsing, validation, and error responses according
|
12
|
+
# to the JSON-RPC 2.0 specification.
|
13
|
+
#
|
14
|
+
# @example Basic usage
|
15
|
+
# use JSONRPC::Middleware
|
16
|
+
#
|
17
|
+
# @example Custom path
|
18
|
+
# use JSONRPC::Middleware, path: '/api/v1/rpc'
|
19
|
+
#
|
7
20
|
class Middleware
|
21
|
+
# Default path for JSON-RPC requests
|
22
|
+
#
|
23
|
+
# @api public
|
24
|
+
#
|
25
|
+
# @return [String] The default path '/'
|
26
|
+
#
|
8
27
|
DEFAULT_PATH = '/'
|
9
28
|
|
29
|
+
# Initializes the JSON-RPC middleware
|
30
|
+
#
|
31
|
+
# @api public
|
32
|
+
#
|
33
|
+
# @example Basic initialization
|
34
|
+
# middleware = JSONRPC::Middleware.new(app)
|
35
|
+
#
|
36
|
+
# @example With custom path
|
37
|
+
# middleware = JSONRPC::Middleware.new(app, path: '/api/jsonrpc')
|
38
|
+
#
|
39
|
+
# @param app [#call] The Rack application to wrap
|
40
|
+
# @param options [Hash] Configuration options
|
41
|
+
# @option options [String] :path ('/') The path to handle JSON-RPC requests on
|
42
|
+
# @option options [Boolean] :rescue_internal_errors (nil) Override config rescue_internal_errors
|
43
|
+
# @option options [Boolean] :log_internal_errors (true) Override config log_internal_errors
|
44
|
+
#
|
10
45
|
def initialize(app, options = {})
|
11
46
|
@app = app
|
12
47
|
@parser = Parser.new
|
13
48
|
@validator = Validator.new
|
14
49
|
@path = options.fetch(:path, DEFAULT_PATH)
|
50
|
+
@config = JSONRPC.configuration
|
51
|
+
@log_internal_errors = options.fetch(:log_internal_errors, @config.log_internal_errors)
|
52
|
+
@rescue_internal_errors = options.fetch(:rescue_internal_errors, @config.rescue_internal_errors)
|
15
53
|
end
|
16
54
|
|
55
|
+
# Rack application call method
|
56
|
+
#
|
57
|
+
# @api public
|
58
|
+
#
|
59
|
+
# @example Processing a request
|
60
|
+
# status, headers, body = middleware.call(env)
|
61
|
+
#
|
62
|
+
# @param env [Hash] The Rack environment
|
63
|
+
#
|
64
|
+
# @return [Array] Rack response tuple [status, headers, body]
|
65
|
+
#
|
17
66
|
def call(env)
|
18
67
|
@req = Rack::Request.new(env)
|
19
68
|
|
@@ -26,26 +75,59 @@ module JSONRPC
|
|
26
75
|
|
27
76
|
private
|
28
77
|
|
78
|
+
# Determines if the current request should be handled as JSON-RPC
|
79
|
+
#
|
80
|
+
# @api private
|
81
|
+
#
|
82
|
+
# @return [Boolean] true if the request matches the configured path and is a POST request
|
83
|
+
#
|
29
84
|
def jsonrpc_request?
|
30
85
|
@req.path == @path && @req.post?
|
31
86
|
end
|
32
87
|
|
88
|
+
# Handles a JSON-RPC request through the complete processing pipeline
|
89
|
+
#
|
90
|
+
# @api private
|
91
|
+
#
|
92
|
+
# @return [Array] Rack response tuple
|
93
|
+
#
|
94
|
+
# @raise [StandardError] Catches all errors and converts to Internal Error response
|
95
|
+
#
|
33
96
|
def handle_jsonrpc_request
|
34
97
|
parsed_request = parse_request
|
35
98
|
return parsed_request if parsed_request.is_a?(Array) # Early return for parse errors
|
36
99
|
|
37
|
-
|
38
|
-
|
100
|
+
if @config.validate_procedure_signatures
|
101
|
+
validation_result = validate_request(parsed_request)
|
102
|
+
return validation_result if validation_result.is_a?(Array) # Early return for validation errors
|
103
|
+
end
|
39
104
|
|
40
105
|
# Set parsed request in environment and call app
|
41
106
|
store_request_in_env(parsed_request)
|
42
107
|
@app.call(@req.env)
|
43
|
-
rescue StandardError
|
44
|
-
|
108
|
+
rescue StandardError => e
|
109
|
+
log_internal_error(e) if @log_internal_errors
|
110
|
+
|
111
|
+
data = {}
|
112
|
+
data = { class: e.class.name, message: e.message, backtrace: e.backtrace } if @config.render_internal_errors
|
113
|
+
error = InternalError.new(request_id: parsed_request.is_a?(Request) ? parsed_request.id : nil, data:)
|
45
114
|
@req.env['jsonrpc.error'] = error
|
115
|
+
|
116
|
+
raise e unless @rescue_internal_errors
|
117
|
+
|
46
118
|
json_response(200, error.to_response)
|
47
119
|
end
|
48
120
|
|
121
|
+
# Parses the request body into JSON-RPC objects
|
122
|
+
#
|
123
|
+
# @api private
|
124
|
+
#
|
125
|
+
# @return [Request, Notification, BatchRequest, Array] Parsed request or error response
|
126
|
+
#
|
127
|
+
# @raise [ParseError] When JSON parsing fails
|
128
|
+
#
|
129
|
+
# @raise [InvalidRequestError] When request structure is invalid
|
130
|
+
#
|
49
131
|
def parse_request
|
50
132
|
body = read_request_body
|
51
133
|
parsed = @parser.parse(body)
|
@@ -59,6 +141,14 @@ module JSONRPC
|
|
59
141
|
json_response(200, e.to_response)
|
60
142
|
end
|
61
143
|
|
144
|
+
# Validates the parsed request using registered procedure definitions
|
145
|
+
#
|
146
|
+
# @api private
|
147
|
+
#
|
148
|
+
# @param parsed_request [Request, Notification, BatchRequest] The parsed request to validate
|
149
|
+
#
|
150
|
+
# @return [Array, nil] Validation error response or nil if valid
|
151
|
+
#
|
62
152
|
def validate_request(parsed_request)
|
63
153
|
validation_errors = @validator.validate(parsed_request)
|
64
154
|
|
@@ -72,6 +162,14 @@ module JSONRPC
|
|
72
162
|
end
|
73
163
|
end
|
74
164
|
|
165
|
+
# Stores the parsed request in the Rack environment for downstream processing
|
166
|
+
#
|
167
|
+
# @api private
|
168
|
+
#
|
169
|
+
# @param parsed_request [Request, Notification, BatchRequest] The request to store
|
170
|
+
#
|
171
|
+
# @return [void]
|
172
|
+
#
|
75
173
|
def store_request_in_env(parsed_request)
|
76
174
|
case parsed_request
|
77
175
|
when Request
|
@@ -85,10 +183,26 @@ module JSONRPC
|
|
85
183
|
|
86
184
|
# Batch handling methods
|
87
185
|
|
186
|
+
# Checks if any requests in a batch contain parse errors
|
187
|
+
#
|
188
|
+
# @api private
|
189
|
+
#
|
190
|
+
# @param batch_request [BatchRequest] The batch request to check
|
191
|
+
#
|
192
|
+
# @return [Boolean] true if any request is an Error object
|
193
|
+
#
|
88
194
|
def parse_errors?(batch_request)
|
89
195
|
batch_request.requests.any?(Error)
|
90
196
|
end
|
91
197
|
|
198
|
+
# Handles batch requests that contain a mix of parse errors and valid requests
|
199
|
+
#
|
200
|
+
# @api private
|
201
|
+
#
|
202
|
+
# @param batch_request [BatchRequest] The batch request containing mixed errors
|
203
|
+
#
|
204
|
+
# @return [Array] Rack response tuple with error and success responses
|
205
|
+
#
|
92
206
|
def handle_mixed_batch_errors(batch_request)
|
93
207
|
error_responses = collect_parse_error_responses(batch_request)
|
94
208
|
valid_requests = collect_valid_requests(batch_request)
|
@@ -101,6 +215,15 @@ module JSONRPC
|
|
101
215
|
json_response(200, BatchResponse.new(error_responses).to_h)
|
102
216
|
end
|
103
217
|
|
218
|
+
# Handles batch requests with validation errors by building appropriate responses
|
219
|
+
#
|
220
|
+
# @api private
|
221
|
+
#
|
222
|
+
# @param batch_request [BatchRequest] The batch request with validation errors
|
223
|
+
# @param validation_errors [Array] Array of validation errors corresponding to each request
|
224
|
+
#
|
225
|
+
# @return [Array] Rack response tuple with mixed error and success responses
|
226
|
+
#
|
104
227
|
def handle_batch_validation_errors(batch_request, validation_errors)
|
105
228
|
responses = build_ordered_responses(batch_request, validation_errors)
|
106
229
|
valid_requests, indices = extract_valid_requests(batch_request, validation_errors)
|
@@ -113,16 +236,41 @@ module JSONRPC
|
|
113
236
|
json_response(200, BatchResponse.new(responses.compact).to_h)
|
114
237
|
end
|
115
238
|
|
239
|
+
# Collects error responses from parse errors in a batch request
|
240
|
+
#
|
241
|
+
# @api private
|
242
|
+
#
|
243
|
+
# @param batch_request [BatchRequest] The batch request to process
|
244
|
+
#
|
245
|
+
# @return [Array<Response>] Array of error responses
|
246
|
+
#
|
116
247
|
def collect_parse_error_responses(batch_request)
|
117
248
|
batch_request.requests.filter_map do |item|
|
118
249
|
Response.new(id: item.request_id, error: item) if item.is_a?(Error)
|
119
250
|
end
|
120
251
|
end
|
121
252
|
|
253
|
+
# Collects valid (non-error) requests from a batch request
|
254
|
+
#
|
255
|
+
# @api private
|
256
|
+
#
|
257
|
+
# @param batch_request [BatchRequest] The batch request to process
|
258
|
+
#
|
259
|
+
# @return [Array<Request, Notification>] Array of valid requests
|
260
|
+
#
|
122
261
|
def collect_valid_requests(batch_request)
|
123
262
|
batch_request.requests.reject { |item| item.is_a?(Error) }
|
124
263
|
end
|
125
264
|
|
265
|
+
# Builds ordered array of responses maintaining request order for batch processing
|
266
|
+
#
|
267
|
+
# @api private
|
268
|
+
#
|
269
|
+
# @param batch_request [BatchRequest] The original batch request
|
270
|
+
# @param validation_errors [Array] Array of validation errors corresponding to each request
|
271
|
+
#
|
272
|
+
# @return [Array<Response, nil>] Array of responses with nil for valid requests
|
273
|
+
#
|
126
274
|
def build_ordered_responses(batch_request, validation_errors)
|
127
275
|
responses = Array.new(batch_request.requests.size)
|
128
276
|
|
@@ -133,6 +281,15 @@ module JSONRPC
|
|
133
281
|
responses
|
134
282
|
end
|
135
283
|
|
284
|
+
# Extracts valid requests and their indices from a batch with validation errors
|
285
|
+
#
|
286
|
+
# @api private
|
287
|
+
#
|
288
|
+
# @param batch_request [BatchRequest] The batch request to process
|
289
|
+
# @param validation_errors [Array] Array of validation errors
|
290
|
+
#
|
291
|
+
# @return [Array<Array<Request>, Array<Integer>>] Valid requests and their original indices
|
292
|
+
#
|
136
293
|
def extract_valid_requests(batch_request, validation_errors)
|
137
294
|
valid_requests = []
|
138
295
|
valid_indices = []
|
@@ -147,6 +304,14 @@ module JSONRPC
|
|
147
304
|
[valid_requests, valid_indices]
|
148
305
|
end
|
149
306
|
|
307
|
+
# Processes valid batch requests by delegating to the application
|
308
|
+
#
|
309
|
+
# @api private
|
310
|
+
#
|
311
|
+
# @param valid_requests [Array<Request, Notification>] Valid requests to process
|
312
|
+
#
|
313
|
+
# @return [Array<Response>] Array of successful responses from the application
|
314
|
+
#
|
150
315
|
def process_valid_batch_requests(valid_requests)
|
151
316
|
valid_batch = BatchRequest.new(valid_requests)
|
152
317
|
|
@@ -165,6 +330,16 @@ module JSONRPC
|
|
165
330
|
end
|
166
331
|
end
|
167
332
|
|
333
|
+
# Merges successful responses into their original positions
|
334
|
+
#
|
335
|
+
# @api private
|
336
|
+
#
|
337
|
+
# @param responses [Array<Response, nil>] Array of responses with error responses and nils
|
338
|
+
# @param success_responses [Array<Response>] Successful responses from the application
|
339
|
+
# @param valid_indices [Array<Integer>] Original indices of the valid requests
|
340
|
+
#
|
341
|
+
# @return [void]
|
342
|
+
#
|
168
343
|
def merge_success_responses(responses, success_responses, valid_indices)
|
169
344
|
success_responses.each_with_index do |response, app_index|
|
170
345
|
original_index = valid_indices[app_index]
|
@@ -174,10 +349,25 @@ module JSONRPC
|
|
174
349
|
|
175
350
|
# Utility methods
|
176
351
|
|
352
|
+
# Creates a JSON HTTP response
|
353
|
+
#
|
354
|
+
# @api private
|
355
|
+
#
|
356
|
+
# @param status [Integer] HTTP status code
|
357
|
+
# @param body [Hash, String] Response body to serialize as JSON
|
358
|
+
#
|
359
|
+
# @return [Array] Rack response tuple [status, headers, body]
|
360
|
+
#
|
177
361
|
def json_response(status, body)
|
178
362
|
[status, { 'content-type' => 'application/json' }, [body.is_a?(String) ? body : JSON.generate(body)]]
|
179
363
|
end
|
180
364
|
|
365
|
+
# Reads and returns the request body from the Rack environment
|
366
|
+
#
|
367
|
+
# @api private
|
368
|
+
#
|
369
|
+
# @return [String, nil] The request body content or nil if no body
|
370
|
+
#
|
181
371
|
def read_request_body
|
182
372
|
body = @req.env[Rack::RACK_INPUT]
|
183
373
|
return unless body
|
@@ -186,5 +376,21 @@ module JSONRPC
|
|
186
376
|
body.rewind if body.respond_to?(:rewind)
|
187
377
|
body_content
|
188
378
|
end
|
379
|
+
|
380
|
+
# Logs internal errors to stdout with full backtrace
|
381
|
+
#
|
382
|
+
# @api private
|
383
|
+
#
|
384
|
+
# @example Log an internal error
|
385
|
+
# log_internal_error(StandardError.new("Something went wrong"))
|
386
|
+
#
|
387
|
+
# @param error [Exception] The error to log
|
388
|
+
#
|
389
|
+
# @return [void]
|
390
|
+
#
|
391
|
+
def log_internal_error(error)
|
392
|
+
puts "Internal error: #{error.message}"
|
393
|
+
puts error.backtrace.join("\n")
|
394
|
+
end
|
189
395
|
end
|
190
396
|
end
|
data/lib/jsonrpc/notification.rb
CHANGED
@@ -3,6 +3,12 @@
|
|
3
3
|
module JSONRPC
|
4
4
|
# A JSON-RPC 2.0 Notification object
|
5
5
|
#
|
6
|
+
# @api public
|
7
|
+
#
|
8
|
+
# Represents a method call that does not expect a response. Unlike a
|
9
|
+
# Request, a Notification omits the "id" field, indicating that no
|
10
|
+
# response should be sent.
|
11
|
+
#
|
6
12
|
# A Notification is a Request object without an "id" member.
|
7
13
|
# Notifications are not confirmable by definition since they do not have a Response object.
|
8
14
|
#
|
@@ -14,25 +20,53 @@ module JSONRPC
|
|
14
20
|
#
|
15
21
|
class Notification
|
16
22
|
# JSON-RPC protocol version
|
23
|
+
#
|
24
|
+
# @api public
|
25
|
+
#
|
26
|
+
# @example
|
27
|
+
# notification.jsonrpc # => "2.0"
|
28
|
+
#
|
17
29
|
# @return [String]
|
18
30
|
#
|
19
31
|
attr_reader :jsonrpc
|
20
32
|
|
21
33
|
# The method name to invoke
|
34
|
+
#
|
35
|
+
# @api public
|
36
|
+
#
|
37
|
+
# @example
|
38
|
+
# notification.method # => "update"
|
39
|
+
#
|
22
40
|
# @return [String]
|
23
41
|
#
|
24
42
|
attr_reader :method
|
25
43
|
|
26
44
|
# Parameters to pass to the method
|
45
|
+
#
|
46
|
+
# @api public
|
47
|
+
#
|
48
|
+
# @example
|
49
|
+
# notification.params # => [1, 2, 3, 4, 5]
|
50
|
+
#
|
27
51
|
# @return [Hash, Array, nil]
|
28
52
|
#
|
29
53
|
attr_reader :params
|
30
54
|
|
31
55
|
# Creates a new JSON-RPC 2.0 Notification object
|
32
56
|
#
|
57
|
+
# @api public
|
58
|
+
#
|
59
|
+
# @example Create a notification with array parameters
|
60
|
+
# JSONRPC::Notification.new(method: "update", params: [1, 2, 3])
|
61
|
+
#
|
62
|
+
# @example Create a notification with named parameters
|
63
|
+
# JSONRPC::Notification.new(method: "log", params: { level: "info", message: "Hello" })
|
64
|
+
#
|
33
65
|
# @param method [String] the name of the method to be invoked
|
34
66
|
# @param params [Hash, Array, nil] the parameters to be used during method invocation
|
67
|
+
#
|
35
68
|
# @raise [ArgumentError] if method is not a String or is reserved
|
69
|
+
#
|
36
70
|
# @raise [ArgumentError] if params is not a Hash, Array, or nil
|
37
71
|
#
|
38
72
|
def initialize(method:, params: nil)
|
@@ -47,6 +81,11 @@ module JSONRPC
|
|
47
81
|
|
48
82
|
# Converts the notification to a JSON-compatible Hash
|
49
83
|
#
|
84
|
+
# @api public
|
85
|
+
#
|
86
|
+
# @example
|
87
|
+
# notification.to_h # => { jsonrpc: "2.0", method: "update", params: [1, 2, 3] }
|
88
|
+
#
|
50
89
|
# @return [Hash] the notification as a JSON-compatible Hash
|
51
90
|
#
|
52
91
|
def to_h
|
@@ -59,6 +98,15 @@ module JSONRPC
|
|
59
98
|
hash
|
60
99
|
end
|
61
100
|
|
101
|
+
# Converts the notification to a JSON string
|
102
|
+
#
|
103
|
+
# @api public
|
104
|
+
#
|
105
|
+
# @example
|
106
|
+
# notification.to_json # => '{"jsonrpc":"2.0","method":"update","params":[1,2,3]}'
|
107
|
+
#
|
108
|
+
# @return [String] the notification as a JSON string
|
109
|
+
#
|
62
110
|
def to_json(*)
|
63
111
|
to_h.to_json(*)
|
64
112
|
end
|
@@ -67,9 +115,14 @@ module JSONRPC
|
|
67
115
|
|
68
116
|
# Validates that the method name meets JSON-RPC 2.0 requirements
|
69
117
|
#
|
118
|
+
# @api private
|
119
|
+
#
|
70
120
|
# @param method [String] the method name
|
121
|
+
#
|
71
122
|
# @raise [ArgumentError] if method is not a String or is reserved
|
72
123
|
#
|
124
|
+
# @return [void]
|
125
|
+
#
|
73
126
|
def validate_method(method)
|
74
127
|
raise ArgumentError, 'Method must be a String' unless method.is_a?(String)
|
75
128
|
|
@@ -80,9 +133,14 @@ module JSONRPC
|
|
80
133
|
|
81
134
|
# Validates that the params is a valid structure according to JSON-RPC 2.0
|
82
135
|
#
|
136
|
+
# @api private
|
137
|
+
#
|
83
138
|
# @param params [Hash, Array, nil] the parameters
|
139
|
+
#
|
84
140
|
# @raise [ArgumentError] if params is not a Hash, Array, or nil
|
85
141
|
#
|
142
|
+
# @return [void]
|
143
|
+
#
|
86
144
|
def validate_params(params)
|
87
145
|
return if params.nil?
|
88
146
|
|
data/lib/jsonrpc/parser.rb
CHANGED
@@ -20,11 +20,20 @@ module JSONRPC
|
|
20
20
|
class Parser
|
21
21
|
# Parse a JSON-RPC 2.0 message
|
22
22
|
#
|
23
|
+
# @api public
|
24
|
+
#
|
25
|
+
# @example Parse a single request
|
26
|
+
# parser.parse('{"jsonrpc":"2.0","method":"subtract","params":[42,23],"id":1}')
|
27
|
+
#
|
28
|
+
# @example Parse a batch request
|
29
|
+
# parser.parse('[{"jsonrpc":"2.0","method":"sum","params":[1,2],"id":"1"}]')
|
30
|
+
#
|
23
31
|
# @param json [String] the JSON-RPC 2.0 message
|
24
32
|
#
|
25
33
|
# @return [Request, Notification, BatchRequest] the parsed object
|
26
34
|
#
|
27
35
|
# @raise [ParseError] if the JSON is invalid
|
36
|
+
#
|
28
37
|
# @raise [InvalidRequestError] if the request structure is invalid
|
29
38
|
#
|
30
39
|
def parse(json)
|
@@ -45,7 +54,10 @@ module JSONRPC
|
|
45
54
|
|
46
55
|
# Parse a single JSON-RPC 2.0 message
|
47
56
|
#
|
57
|
+
# @api private
|
58
|
+
#
|
48
59
|
# @param data [Hash] the parsed JSON data
|
60
|
+
#
|
49
61
|
# @return [Request, Notification, Error] the parsed request, notification, or error
|
50
62
|
#
|
51
63
|
def parse_single(data)
|
@@ -67,8 +79,12 @@ module JSONRPC
|
|
67
79
|
|
68
80
|
# Parse a batch JSON-RPC 2.0 message
|
69
81
|
#
|
82
|
+
# @api private
|
83
|
+
#
|
70
84
|
# @param data [Array] the array of request data
|
85
|
+
#
|
71
86
|
# @return [BatchRequest] the batch request
|
87
|
+
#
|
72
88
|
# @raise [InvalidRequestError] if the batch is empty
|
73
89
|
#
|
74
90
|
def parse_batch(data)
|
@@ -94,8 +110,12 @@ module JSONRPC
|
|
94
110
|
|
95
111
|
# Parse a single item within a batch, allowing errors to be captured
|
96
112
|
#
|
113
|
+
# @api private
|
114
|
+
#
|
97
115
|
# @param data [Hash] the parsed JSON data for a single item
|
116
|
+
#
|
98
117
|
# @return [Request, Notification] the parsed request or notification
|
118
|
+
#
|
99
119
|
# @raise [InvalidRequestError] if the request structure is invalid
|
100
120
|
#
|
101
121
|
def parse_single_for_batch(data)
|
@@ -117,9 +137,14 @@ module JSONRPC
|
|
117
137
|
|
118
138
|
# Validate the JSON-RPC 2.0 version
|
119
139
|
#
|
140
|
+
# @api private
|
141
|
+
#
|
120
142
|
# @param data [Hash] the request data
|
143
|
+
#
|
121
144
|
# @raise [InvalidRequestError] if the version is missing or invalid
|
122
145
|
#
|
146
|
+
# @return [void]
|
147
|
+
#
|
123
148
|
def validate_jsonrpc_version(data)
|
124
149
|
raise InvalidRequestError.new(data: { details: 'Request must be an object' }) unless data.is_a?(Hash)
|
125
150
|
|
@@ -141,9 +166,14 @@ module JSONRPC
|
|
141
166
|
|
142
167
|
# Validate the request structure according to JSON-RPC 2.0 specification
|
143
168
|
#
|
169
|
+
# @api private
|
170
|
+
#
|
144
171
|
# @param data [Hash] the request data
|
172
|
+
#
|
145
173
|
# @raise [InvalidRequestError] if the request structure is invalid
|
146
174
|
#
|
175
|
+
# @return [void]
|
176
|
+
#
|
147
177
|
def validate_request_structure(data)
|
148
178
|
method = data['method']
|
149
179
|
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSONRPC
|
4
|
+
# @api private
|
5
|
+
class Railtie < ::Rails::Railtie
|
6
|
+
initializer 'jsonrpc.middleware' do |app|
|
7
|
+
app.middleware.use JSONRPC::Middleware
|
8
|
+
end
|
9
|
+
|
10
|
+
initializer 'jsonrpc.renderer' do
|
11
|
+
ActiveSupport.on_load(:action_controller) do
|
12
|
+
Mime::Type.register 'application/json', :jsonrpc
|
13
|
+
|
14
|
+
ActionController::Renderers.add :jsonrpc do |result_data, _options|
|
15
|
+
self.content_type ||= Mime[:jsonrpc]
|
16
|
+
|
17
|
+
# Handle different types of JSON-RPC responses
|
18
|
+
if jsonrpc_request?
|
19
|
+
# Single request - create response with ID and result
|
20
|
+
self.status = 200
|
21
|
+
self.response_body = JSONRPC::Response.new(id: jsonrpc_request.id, result: result_data).to_json
|
22
|
+
elsif jsonrpc_batch?
|
23
|
+
# Batch request - result_data should be an array of responses
|
24
|
+
if result_data.compact.empty?
|
25
|
+
# Batch contained only notifications
|
26
|
+
self.status = 204
|
27
|
+
self.response_body = ''
|
28
|
+
''
|
29
|
+
else
|
30
|
+
result_data.to_json
|
31
|
+
end
|
32
|
+
elsif jsonrpc_notification?
|
33
|
+
# Notification - no response body
|
34
|
+
self.status = 204
|
35
|
+
self.response_body = ''
|
36
|
+
''
|
37
|
+
else
|
38
|
+
# Fallback - treat as regular JSON-RPC response
|
39
|
+
result_data.to_json
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Hook into the controller loading process
|
46
|
+
initializer 'jsonrpc.include_controller_extensions', after: 'action_controller.set_configs' do
|
47
|
+
ActiveSupport.on_load(:action_controller_base) do
|
48
|
+
include JSONRPC::Helpers
|
49
|
+
end
|
50
|
+
|
51
|
+
# Also include in API controllers if you're using Rails API mode
|
52
|
+
ActiveSupport.on_load(:action_controller_api) do
|
53
|
+
include JSONRPC::Helpers
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|