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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/.aiexclude +4 -0
  3. data/.claude/commands/document.md +105 -0
  4. data/.claude/docs/yard.md +602 -0
  5. data/.claude/settings.local.json +2 -1
  6. data/.env.example +5 -0
  7. data/CHANGELOG.md +22 -2
  8. data/CLAUDE.md +114 -0
  9. data/README.md +42 -102
  10. data/Rakefile +59 -1
  11. data/examples/README.md +37 -0
  12. data/examples/procedures.rb +6 -1
  13. data/examples/rack/README.md +26 -1
  14. data/examples/rack/app.rb +1 -1
  15. data/examples/rack-echo/README.md +23 -1
  16. data/examples/rack-single-file/README.md +37 -0
  17. data/examples/rack-single-file/config.ru +54 -0
  18. data/examples/rails/.gitignore +21 -0
  19. data/examples/rails/.ruby-version +1 -0
  20. data/examples/rails/Gemfile +15 -0
  21. data/examples/rails/Gemfile.lock +261 -0
  22. data/examples/rails/README.md +32 -0
  23. data/examples/rails/Rakefile +8 -0
  24. data/examples/rails/app/controllers/application_controller.rb +4 -0
  25. data/examples/rails/app/controllers/jsonrpc_controller.rb +44 -0
  26. data/examples/rails/bin/dev +4 -0
  27. data/examples/rails/bin/rails +6 -0
  28. data/examples/rails/bin/rake +6 -0
  29. data/examples/rails/bin/setup +28 -0
  30. data/examples/rails/config/application.rb +47 -0
  31. data/examples/rails/config/boot.rb +5 -0
  32. data/examples/rails/config/credentials.yml.enc +1 -0
  33. data/examples/rails/config/environment.rb +7 -0
  34. data/examples/rails/config/environments/development.rb +42 -0
  35. data/examples/rails/config/environments/production.rb +60 -0
  36. data/examples/rails/config/environments/test.rb +44 -0
  37. data/examples/rails/config/initializers/cors.rb +18 -0
  38. data/examples/rails/config/initializers/filter_parameter_logging.rb +10 -0
  39. data/examples/rails/config/initializers/inflections.rb +18 -0
  40. data/examples/rails/config/initializers/jsonrpc.rb +62 -0
  41. data/examples/rails/config/locales/en.yml +31 -0
  42. data/examples/rails/config/puma.rb +40 -0
  43. data/examples/rails/config/routes.rb +14 -0
  44. data/examples/rails/config.ru +8 -0
  45. data/examples/rails/public/robots.txt +1 -0
  46. data/examples/rails-single-file/config.ru +71 -0
  47. data/examples/sinatra-classic/Gemfile +9 -0
  48. data/examples/sinatra-classic/Gemfile.lock +95 -0
  49. data/examples/sinatra-classic/README.md +32 -0
  50. data/examples/sinatra-classic/app.rb +54 -0
  51. data/examples/sinatra-classic/config.ru +6 -0
  52. data/examples/sinatra-modular/Gemfile +9 -0
  53. data/examples/sinatra-modular/Gemfile.lock +95 -0
  54. data/examples/sinatra-modular/README.md +32 -0
  55. data/examples/sinatra-modular/app.rb +57 -0
  56. data/examples/sinatra-modular/config.ru +6 -0
  57. data/lib/jsonrpc/batch_request.rb +67 -2
  58. data/lib/jsonrpc/batch_response.rb +56 -0
  59. data/lib/jsonrpc/configuration.rb +156 -14
  60. data/lib/jsonrpc/error.rb +83 -2
  61. data/lib/jsonrpc/errors/internal_error.rb +14 -2
  62. data/lib/jsonrpc/errors/invalid_params_error.rb +13 -1
  63. data/lib/jsonrpc/errors/invalid_request_error.rb +8 -0
  64. data/lib/jsonrpc/errors/method_not_found_error.rb +8 -0
  65. data/lib/jsonrpc/errors/parse_error.rb +8 -0
  66. data/lib/jsonrpc/helpers.rb +212 -21
  67. data/lib/jsonrpc/middleware.rb +211 -5
  68. data/lib/jsonrpc/notification.rb +58 -0
  69. data/lib/jsonrpc/parser.rb +30 -0
  70. data/lib/jsonrpc/railtie.rb +57 -0
  71. data/lib/jsonrpc/request.rb +68 -1
  72. data/lib/jsonrpc/response.rb +76 -0
  73. data/lib/jsonrpc/validator.rb +67 -6
  74. data/lib/jsonrpc/version.rb +11 -1
  75. data/lib/jsonrpc.rb +53 -1
  76. metadata +49 -1
@@ -3,17 +3,66 @@
3
3
  require 'rack'
4
4
 
5
5
  module JSONRPC
6
- # Middleware for JSON-RPC compliance
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
- validation_result = validate_request(parsed_request)
38
- return validation_result if validation_result.is_a?(Array) # Early return for validation errors
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
- error = InternalError.new(request_id: parsed_request.is_a?(Request) ? parsed_request.id : nil)
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
@@ -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
 
@@ -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