jsonrpc-rails 0.5.3 → 0.6.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/README.md +78 -2
- data/lib/jsonrpc_rails/middleware/validator.rb +127 -48
- data/lib/jsonrpc_rails/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e098dc20d9c1c05b35e30788afb28d8671fb93be44b4526be851fb9d5800fa80
|
|
4
|
+
data.tar.gz: efa72ded518abf7bec6a360bad11beacde8564d426fb07829c32d1a0e390a7f3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a3521b7da4cd23cda88c2bde3ccbd5dc39077583f3aef93da259a865df80997f6699cd39006e5abcbd34312b55f9092b59bc148bc0e20a075310afa2ba7dd5d4
|
|
7
|
+
data.tar.gz: f42cd489ee6c10e279de3db87dcd849d0f12f8476e4535146b302f26308a1b503ae09fba4e75f9aa35dd0e9642fceb0e174ec3871b53740f5365287e62162fd6
|
data/README.md
CHANGED
|
@@ -54,6 +54,30 @@ config.jsonrpc_rails.validated_paths = [
|
|
|
54
54
|
Leave the array empty (default) and the middleware is effectively off.
|
|
55
55
|
Use [/.*\z/] if you really want it on everywhere.
|
|
56
56
|
|
|
57
|
+
Protocol integrations can reuse the parsing, JSON-RPC validation, typed-object
|
|
58
|
+
conversion, and error handling while adding their own envelope rules:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
config.middleware.use JSONRPC_Rails::Middleware::Validator,
|
|
62
|
+
[ "/mcp" ],
|
|
63
|
+
payload_validator: MyProtocolValidator,
|
|
64
|
+
batch_policy: :reject
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`payload_validator` may implement `valid?(payload)` or `call(payload)` and receives
|
|
68
|
+
the decoded Hash or Array after generic JSON-RPC validation. Batches are allowed by
|
|
69
|
+
default; `batch_policy: :reject` disables them.
|
|
70
|
+
|
|
71
|
+
By default, matching POST requests without an `application/json` content type pass
|
|
72
|
+
through untouched. Protocol endpoints that accept only JSON-RPC can reject those
|
|
73
|
+
requests with HTTP 415 instead:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
config.middleware.use JSONRPC_Rails::Middleware::Validator,
|
|
77
|
+
[ "/rpc" ],
|
|
78
|
+
require_json_content_type: true
|
|
79
|
+
```
|
|
80
|
+
|
|
57
81
|
In your controllers, you can render JSON-RPC responses like so:
|
|
58
82
|
|
|
59
83
|
```ruby
|
|
@@ -100,8 +124,8 @@ You can override the default `message` or add `data` for either method by provid
|
|
|
100
124
|
The gem automatically inserts `JSONRPC_Rails::Middleware::Validator` into your application's middleware stack. This middleware performs the following actions for incoming **POST** requests with `Content-Type: application/json`:
|
|
101
125
|
|
|
102
126
|
1. **Parses** the JSON body. Returns a JSON-RPC `Parse error (-32700)` if parsing fails.
|
|
103
|
-
2. **Validates**
|
|
104
|
-
3. **Stores** the validated
|
|
127
|
+
2. **Validates** request, notification, and response structures against JSON-RPC 2.0, including single and batch payloads. Extension members are accepted, while conflicting reserved envelope members are rejected. Returns a JSON-RPC `Invalid Request (-32600)` error if validation fails. **Note:** For batch payloads, if *any* message is structurally invalid, the entire batch is rejected with a single `Invalid Request (-32600)` error.
|
|
128
|
+
3. **Stores** the validated payload as a typed `JSON_RPC::Request`, `JSON_RPC::Notification`, or `JSON_RPC::Response` (or an Array of those objects) in `request.env[:jsonrpc]`.
|
|
105
129
|
4. **Passes** the request to the next middleware or your controller action.
|
|
106
130
|
|
|
107
131
|
In your controller action, you can access the validated payload like this:
|
|
@@ -184,6 +208,58 @@ class MyApiController < ApplicationController
|
|
|
184
208
|
end
|
|
185
209
|
```
|
|
186
210
|
|
|
211
|
+
## Error Codes
|
|
212
|
+
|
|
213
|
+
The gem implements the complete JSON-RPC 2.0 error specification. All error codes are defined in `lib/json_rpc/json_rpc_error.rb` and can be used either as numeric codes or symbols:
|
|
214
|
+
|
|
215
|
+
| Code | Symbol | Name | Description |
|
|
216
|
+
|------|--------|------|-------------|
|
|
217
|
+
| -32700 | `:parse_error` | Parse Error | Invalid JSON was received by the server |
|
|
218
|
+
| -32600 | `:invalid_request` | Invalid Request | The JSON sent is not a valid Request object |
|
|
219
|
+
| -32601 | `:method_not_found` | Method Not Found | The method does not exist / is not available |
|
|
220
|
+
| -32602 | `:invalid_params` | Invalid Params | Invalid method parameter(s) |
|
|
221
|
+
| -32603 | `:internal_error` | Internal Error | Internal JSON-RPC error |
|
|
222
|
+
| -32000 | `:server_error` | Server Error | Implementation-defined server error |
|
|
223
|
+
|
|
224
|
+
### Usage Examples
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
# Using numeric codes
|
|
228
|
+
render jsonrpc: {}, error: -32601, id: 1
|
|
229
|
+
|
|
230
|
+
# Using symbols
|
|
231
|
+
render jsonrpc: {}, error: :method_not_found, id: 1
|
|
232
|
+
|
|
233
|
+
# Creating error objects
|
|
234
|
+
error = JSON_RPC::JsonRpcError.build(:invalid_params)
|
|
235
|
+
render jsonrpc: JSON_RPC::Response.new(id: 1, error: error)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## JSONRPC Namespace
|
|
239
|
+
|
|
240
|
+
The gem uses two main namespaces to separate core JSON-RPC functionality from Rails integration:
|
|
241
|
+
|
|
242
|
+
### JSON_RPC Module (Core Specification)
|
|
243
|
+
|
|
244
|
+
Contains the core JSON-RPC 2.0 specification objects:
|
|
245
|
+
|
|
246
|
+
- **`JSON_RPC::Request`** - Represents a JSON-RPC request with method, params, and id
|
|
247
|
+
- **`JSON_RPC::Response`** - Represents a JSON-RPC response with result or error
|
|
248
|
+
- **`JSON_RPC::Notification`** - Represents a JSON-RPC notification (request without id)
|
|
249
|
+
- **`JSON_RPC::JsonRpcError`** - Exception class for JSON-RPC errors with standard error codes
|
|
250
|
+
- **`JSON_RPC::Parser`** - Utility for converting hashes to typed JSON-RPC objects
|
|
251
|
+
|
|
252
|
+
### JSONRPC_Rails Module (Rails Integration)
|
|
253
|
+
|
|
254
|
+
Contains Rails-specific integration components:
|
|
255
|
+
|
|
256
|
+
- **`JSONRPC_Rails::Railtie`** - Rails initialization, middleware setup, and renderer registration
|
|
257
|
+
- **`JSONRPC_Rails::Middleware::Validator`** - Rack middleware for validating JSON-RPC requests
|
|
258
|
+
- **`JSONRPC_Rails::ControllerHelpers`** - Helper methods like `jsonrpc_params` and `jsonrpc_params_batch?`
|
|
259
|
+
- **`JSONRPC_Rails::VERSION`** - Current gem version
|
|
260
|
+
|
|
261
|
+
This separation ensures clean architecture where core JSON-RPC logic is independent of Rails, while Rails-specific features are properly isolated.
|
|
262
|
+
|
|
187
263
|
## Testing
|
|
188
264
|
|
|
189
265
|
A dummy Rails application is included within the gem (located in `test/dummy`) to facilitate testing. You can run the tests from the **project root directory** by executing:
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "active_support/json"
|
|
5
|
+
require "rack/media_type"
|
|
6
|
+
require "stringio"
|
|
5
7
|
|
|
6
8
|
module JSONRPC_Rails
|
|
7
9
|
module Middleware
|
|
@@ -14,54 +16,75 @@ module JSONRPC_Rails
|
|
|
14
16
|
# IDs or empty batches no longer raise before we can return a proper
|
|
15
17
|
# -32600 “Invalid Request”.
|
|
16
18
|
class Validator
|
|
17
|
-
CONTENT_TYPE
|
|
18
|
-
ENV_PAYLOAD_KEY
|
|
19
|
+
CONTENT_TYPE = "application/json"
|
|
20
|
+
ENV_PAYLOAD_KEY = :jsonrpc
|
|
21
|
+
BATCH_POLICIES = %i[allow reject].freeze
|
|
22
|
+
RESERVED_MEMBERS = %w[jsonrpc method params id result error].freeze
|
|
19
23
|
|
|
20
24
|
# @param app [#call]
|
|
21
25
|
# @param paths [Array<String, Regexp, Proc>] paths to validate
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
# @param payload_validator [#valid?, #call, nil] optional protocol validator
|
|
27
|
+
# @param batch_policy [Symbol] either :allow or :reject
|
|
28
|
+
# @param require_json_content_type [Boolean] reject matching POST requests
|
|
29
|
+
# that do not use application/json instead of passing them downstream
|
|
30
|
+
def initialize(app, paths = nil, payload_validator: nil, batch_policy: :allow,
|
|
31
|
+
require_json_content_type: false)
|
|
32
|
+
unless BATCH_POLICIES.include?(batch_policy)
|
|
33
|
+
raise ArgumentError, "batch_policy must be one of: #{BATCH_POLICIES.join(", ")}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
@app = app
|
|
24
37
|
@paths = Array(paths || Rails.configuration.jsonrpc_rails.validated_paths)
|
|
38
|
+
@payload_validator = payload_validator
|
|
39
|
+
@batch_policy = batch_policy
|
|
40
|
+
@require_json_content_type = require_json_content_type
|
|
25
41
|
end
|
|
26
42
|
|
|
27
43
|
# Rack entry point
|
|
28
44
|
def call(env)
|
|
29
45
|
return @app.call(env) unless validate_path?(env["PATH_INFO"])
|
|
30
|
-
return @app.call(env) unless env["REQUEST_METHOD"] == "POST"
|
|
31
|
-
env["CONTENT_TYPE"]&.start_with?(CONTENT_TYPE)
|
|
46
|
+
return @app.call(env) unless env["REQUEST_METHOD"] == "POST"
|
|
32
47
|
|
|
33
|
-
|
|
34
|
-
|
|
48
|
+
unless json_content_type?(env["CONTENT_TYPE"])
|
|
49
|
+
return @app.call(env) unless @require_json_content_type
|
|
35
50
|
|
|
36
|
-
|
|
51
|
+
return jsonrpc_error_response(
|
|
52
|
+
:server_error,
|
|
53
|
+
status: 415,
|
|
54
|
+
message: "Unsupported Media Type: Content-Type must be application/json"
|
|
55
|
+
)
|
|
56
|
+
end
|
|
37
57
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
58
|
+
body = env["rack.input"].read
|
|
59
|
+
# Replace consumed input with fresh StringIO for downstream middleware
|
|
60
|
+
# This is Rack 3.0+ compatible and works with all input stream types
|
|
61
|
+
env["rack.input"] = StringIO.new(body)
|
|
62
|
+
|
|
63
|
+
begin
|
|
64
|
+
raw_payload = ActiveSupport::JSON.decode(body)
|
|
65
|
+
rescue ActiveSupport::JSON.parse_error
|
|
66
|
+
return jsonrpc_error_response(:parse_error)
|
|
41
67
|
end
|
|
42
68
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
else
|
|
46
|
-
validate_single(raw_payload)
|
|
69
|
+
unless raw_payload.is_a?(Hash) || raw_payload.is_a?(Array)
|
|
70
|
+
return jsonrpc_error_response(:invalid_request)
|
|
47
71
|
end
|
|
48
|
-
return validity unless validity == :valid
|
|
49
72
|
|
|
50
|
-
|
|
73
|
+
return invalid_request_response(raw_payload) unless valid_batch_policy?(raw_payload)
|
|
74
|
+
return invalid_request_response(raw_payload) unless valid_payload_structure?(raw_payload)
|
|
75
|
+
return invalid_request_response(raw_payload) unless valid_protocol_payload?(raw_payload)
|
|
76
|
+
|
|
77
|
+
begin
|
|
78
|
+
env[ENV_PAYLOAD_KEY] = convert_to_objects(raw_payload)
|
|
79
|
+
rescue JSON_RPC::JsonRpcError, ArgumentError, TypeError
|
|
80
|
+
return invalid_request_response(raw_payload)
|
|
81
|
+
end
|
|
51
82
|
|
|
52
83
|
@app.call(env)
|
|
53
84
|
end
|
|
54
85
|
|
|
55
86
|
private
|
|
56
87
|
|
|
57
|
-
def parse_json(body)
|
|
58
|
-
return nil if body.nil? || body.strip.empty?
|
|
59
|
-
|
|
60
|
-
ActiveSupport::JSON.decode(body)
|
|
61
|
-
rescue ActiveSupport::JSON.parse_error
|
|
62
|
-
nil
|
|
63
|
-
end
|
|
64
|
-
|
|
65
88
|
def validate_path?(path)
|
|
66
89
|
return false if @paths.empty?
|
|
67
90
|
|
|
@@ -75,35 +98,86 @@ module JSONRPC_Rails
|
|
|
75
98
|
end
|
|
76
99
|
end
|
|
77
100
|
|
|
101
|
+
def json_content_type?(content_type)
|
|
102
|
+
media_type = Rack::MediaType.type(content_type)&.strip
|
|
103
|
+
media_type&.casecmp?(CONTENT_TYPE) || false
|
|
104
|
+
end
|
|
105
|
+
|
|
78
106
|
# -------------------- structure validation --------------------------------
|
|
79
107
|
|
|
80
|
-
def
|
|
108
|
+
def valid_payload_structure?(payload)
|
|
109
|
+
if payload.is_a?(Array)
|
|
110
|
+
!payload.empty? && payload.all? { |message| valid_message_structure?(message) }
|
|
111
|
+
else
|
|
112
|
+
valid_message_structure?(payload)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def valid_message_structure?(obj)
|
|
81
117
|
return false unless obj.is_a?(Hash)
|
|
82
118
|
return false unless obj["jsonrpc"] == "2.0"
|
|
83
|
-
return false unless obj["method"].is_a?(String)
|
|
84
119
|
|
|
85
|
-
|
|
120
|
+
if obj.key?("method")
|
|
121
|
+
valid_call_structure?(obj)
|
|
122
|
+
elsif obj.key?("result") || obj.key?("error")
|
|
123
|
+
valid_response_structure?(obj)
|
|
124
|
+
else
|
|
125
|
+
false
|
|
126
|
+
end
|
|
127
|
+
end
|
|
86
128
|
|
|
87
|
-
|
|
129
|
+
def valid_call_structure?(obj)
|
|
130
|
+
return false unless obj["method"].is_a?(String)
|
|
131
|
+
return false unless valid_params?(obj)
|
|
132
|
+
return false if obj.key?("id") && !valid_id?(obj["id"])
|
|
88
133
|
|
|
89
|
-
|
|
90
|
-
(obj.keys - allowed).empty?
|
|
134
|
+
no_conflicting_members?(obj, %w[jsonrpc method params id])
|
|
91
135
|
end
|
|
92
136
|
|
|
93
|
-
def
|
|
94
|
-
|
|
95
|
-
|
|
137
|
+
def valid_response_structure?(obj)
|
|
138
|
+
return false unless obj.key?("id") && valid_id?(obj["id"])
|
|
139
|
+
return false if obj.key?("result") == obj.key?("error")
|
|
140
|
+
|
|
141
|
+
if obj.key?("result")
|
|
142
|
+
no_conflicting_members?(obj, %w[jsonrpc id result])
|
|
96
143
|
else
|
|
97
|
-
|
|
98
|
-
|
|
144
|
+
valid_error_structure?(obj["error"]) &&
|
|
145
|
+
no_conflicting_members?(obj, %w[jsonrpc id error])
|
|
99
146
|
end
|
|
100
147
|
end
|
|
101
148
|
|
|
102
|
-
def
|
|
103
|
-
return
|
|
149
|
+
def valid_error_structure?(error)
|
|
150
|
+
return false unless error.is_a?(Hash)
|
|
151
|
+
return false unless error["code"].is_a?(Integer)
|
|
152
|
+
return false unless error["message"].is_a?(String)
|
|
153
|
+
|
|
154
|
+
true
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def no_conflicting_members?(obj, allowed_members)
|
|
158
|
+
(obj.keys & (RESERVED_MEMBERS - allowed_members)).empty?
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def valid_params?(obj)
|
|
162
|
+
!obj.key?("params") || obj["params"].is_a?(Array) || obj["params"].is_a?(Hash)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def valid_id?(id)
|
|
166
|
+
id.is_a?(String) || id.is_a?(Numeric) || id.nil?
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def valid_batch_policy?(payload)
|
|
170
|
+
!payload.is_a?(Array) || @batch_policy == :allow
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def valid_protocol_payload?(payload)
|
|
174
|
+
return true unless @payload_validator
|
|
104
175
|
|
|
105
|
-
|
|
106
|
-
|
|
176
|
+
if @payload_validator.respond_to?(:valid?)
|
|
177
|
+
@payload_validator.valid?(payload)
|
|
178
|
+
else
|
|
179
|
+
@payload_validator.call(payload)
|
|
180
|
+
end
|
|
107
181
|
end
|
|
108
182
|
|
|
109
183
|
# ------------------ conversion to typed objects ---------------------------
|
|
@@ -123,21 +197,26 @@ module JSONRPC_Rails
|
|
|
123
197
|
|
|
124
198
|
# @param error_sym [Symbol]
|
|
125
199
|
# @param status [Integer]
|
|
126
|
-
# @param id [String,
|
|
200
|
+
# @param id [String, Numeric, nil]
|
|
201
|
+
# @param message [String, nil]
|
|
127
202
|
# @return [Array] Rack triplet
|
|
128
|
-
def jsonrpc_error_response(error_sym, status: 400, id: nil)
|
|
129
|
-
error_obj = JSON_RPC::JsonRpcError.build(error_sym)
|
|
203
|
+
def jsonrpc_error_response(error_sym, status: 400, id: nil, message: nil)
|
|
204
|
+
error_obj = JSON_RPC::JsonRpcError.build(error_sym, message: message)
|
|
130
205
|
payload = JSON_RPC::Response.new(id: id, error: error_obj).to_json
|
|
131
206
|
|
|
132
|
-
[ status, { "
|
|
207
|
+
[ status, { "content-type" => CONTENT_TYPE }, [ payload ] ]
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def invalid_request_response(raw_payload)
|
|
211
|
+
jsonrpc_error_response(:invalid_request, id: extract_id_from_raw_payload(raw_payload))
|
|
133
212
|
end
|
|
134
213
|
|
|
135
214
|
# Extract ID from raw payload if possible
|
|
136
215
|
def extract_id_from_raw_payload(raw)
|
|
137
216
|
return nil unless raw.is_a?(Hash)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
217
|
+
|
|
218
|
+
id = raw["id"]
|
|
219
|
+
id if valid_id?(id)
|
|
141
220
|
end
|
|
142
221
|
end
|
|
143
222
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jsonrpc-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Abdelkader Boudih
|
|
@@ -67,7 +67,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
68
|
version: '0'
|
|
69
69
|
requirements: []
|
|
70
|
-
rubygems_version:
|
|
70
|
+
rubygems_version: 4.0.10
|
|
71
71
|
specification_version: 4
|
|
72
72
|
summary: A Railtie-based gem that brings JSON-RPC 2.0 support to your Rails application.
|
|
73
73
|
test_files: []
|