jsonrpc-rails 0.1.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +166 -0
- data/Rakefile +20 -0
- data/lib/json_rpc/json_rpc_error.rb +90 -0
- data/lib/json_rpc/notification.rb +28 -0
- data/lib/json_rpc/request.rb +48 -0
- data/lib/json_rpc/response.rb +79 -0
- data/lib/jsonrpc-rails.rb +13 -0
- data/lib/jsonrpc_rails/middleware/validator.rb +169 -0
- data/lib/jsonrpc_rails/railtie.rb +69 -0
- data/lib/jsonrpc_rails/version.rb +3 -0
- metadata +71 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7a329937c7fecde34677567a8df15dab630b5062b30c01f1fcb825b25789e590
|
4
|
+
data.tar.gz: e7d7de880e9adc77839bb29feafdff54a35f56de555a68aa7e9d62413bf2d3fa
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e98d92b3ea32b21fc6b1fafa766a0a9b3a60f9cd0475424753c486e62c872945dd105c0e64f91c9d0eba291918ec5bab6f5d3310d4039ed2b807c9c53d3f6291
|
7
|
+
data.tar.gz: b724be97e378604ede9244d90b76d93f07c0070a1e142ed0d3787c5d81e4df1dd8f1848a3d84fdd28096d6ae6cb7102e86780fe6c2b2fbabbf8a8e1fc8f0615b
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright Abdelkader Boudih
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
# jsonrpc-rails
|
2
|
+
|
3
|
+
[](https://badge.fury.io/rb/jsonrpc-rails)
|
4
|
+
|
5
|
+
**jsonrpc-rails** is a Railtie-based gem that brings JSON-RPC 2.0 support to your Rails application.
|
6
|
+
It integrates into Rails, allowing you to render JSON-RPC responses and validate incoming requests.
|
7
|
+
|
8
|
+
## Features
|
9
|
+
|
10
|
+
- **Rails Integration:** Easily integrate JSON-RPC 2.0 support via a Rails Railtie.
|
11
|
+
- **Custom Renderer:** Render responses with `render jsonrpc:`, automatically wrapping data in the JSON-RPC 2.0 envelope.
|
12
|
+
- **Error Handling:** Built-in support for both success and error responses according to the JSON-RPC 2.0 specification.
|
13
|
+
- **Request Validation:** Includes middleware (`JSON_RPC_Rails::Middleware::Validator`) to strictly validate incoming JSON-RPC 2.0 requests (single and batch) against the specification structure.
|
14
|
+
- **Rails 8+ Compatibility:** Designed specifically for Rails 8 and later versions.
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
Add the following line to your application's Gemfile:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
gem 'jsonrpc-rails'
|
22
|
+
```
|
23
|
+
|
24
|
+
Then run:
|
25
|
+
|
26
|
+
```bash
|
27
|
+
bundle install
|
28
|
+
```
|
29
|
+
|
30
|
+
Or install it directly via:
|
31
|
+
|
32
|
+
```bash
|
33
|
+
gem install jsonrpc-rails
|
34
|
+
```
|
35
|
+
|
36
|
+
## Usage
|
37
|
+
|
38
|
+
### Rendering Responses
|
39
|
+
|
40
|
+
Once installed, **jsonrpc-rails** registers a custom renderer with Rails.
|
41
|
+
In your controllers, you can render JSON-RPC responses like so:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
class TestController < ApplicationController
|
45
|
+
def index
|
46
|
+
# Render a successful JSON-RPC response
|
47
|
+
render jsonrpc: { message: "Hello from JSON-RPC!" }, id: 1
|
48
|
+
end
|
49
|
+
|
50
|
+
def error_code
|
51
|
+
# Render an error using a numeric code (uses default message for standard codes)
|
52
|
+
render jsonrpc: {}, error: -32600, id: 5 # Invalid Request
|
53
|
+
end
|
54
|
+
|
55
|
+
def error_code_override
|
56
|
+
# Render an error using a numeric code, overriding the message and adding data
|
57
|
+
render jsonrpc: { message: "Specific invalid request", data: { field: "xyz" } }, error: -32600, id: 6
|
58
|
+
end
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
The renderer wraps your data in the JSON-RPC 2.0 envelope:
|
63
|
+
- **Success Response:**
|
64
|
+
```json
|
65
|
+
{ "jsonrpc": "2.0", "result": { ... }, "id": 1 }
|
66
|
+
```
|
67
|
+
- **Error Response (using numeric code):**
|
68
|
+
```json
|
69
|
+
{ "jsonrpc": "2.0", "error": { "code": -32600, "message": "Invalid Request" }, "id": 5 }
|
70
|
+
```
|
71
|
+
- **Error Response (using numeric code with override):**
|
72
|
+
```json
|
73
|
+
{ "jsonrpc": "2.0", "error": { "code": -32600, "message": "Specific invalid request", "data": { "field": "xyz" } }, "id": 6 }
|
74
|
+
```
|
75
|
+
|
76
|
+
To render an error response, pass a numeric error code or a predefined Symbol to the `error:` option:
|
77
|
+
- **Numeric Code:** Pass the integer code directly (e.g., `error: -32600`). If the code is a standard JSON-RPC error code (`-32700`, `-32600` to `-32603`, `-32000`), a default message will be used (as shown in the `error_code` example).
|
78
|
+
- **Symbol:** Pass a symbol corresponding to a standard error (e.g., `error: :invalid_request`). The gem will look up the code and default message. (See `lib/json_rpc/json_rpc_error.rb` for available symbols).
|
79
|
+
|
80
|
+
You can override the default `message` or add `data` for either method by providing them in the main hash passed to `render jsonrpc:`, as demonstrated in the `error_code_override` example.
|
81
|
+
|
82
|
+
### Handling Requests
|
83
|
+
|
84
|
+
The gem automatically inserts `JSON_RPC_Rails::Middleware::Validator` into your application's middleware stack. This middleware performs the following actions for incoming **POST** requests with `Content-Type: application/json`:
|
85
|
+
|
86
|
+
1. **Parses** the JSON body. Returns a JSON-RPC `Parse error (-32700)` if parsing fails.
|
87
|
+
2. **Validates** the structure against the JSON-RPC 2.0 specification (single or batch). It performs strict validation, ensuring `jsonrpc: "2.0"`, a string `method`, optional `params` (array/object), optional `id` (string/number/null), and **no extraneous keys**. Returns a JSON-RPC `Invalid Request (-32600)` error if validation fails. **Note:** For batch requests, if *any* individual request within the batch is structurally invalid, the entire batch is rejected with a single `Invalid Request (-32600)` error.
|
88
|
+
3. **Stores** the validated, parsed payload (the original Ruby Hash or Array) in `request.env['jsonrpc.payload']` if validation succeeds.
|
89
|
+
4. **Passes** the request to the next middleware or your controller action.
|
90
|
+
|
91
|
+
In your controller action, you can access the validated payload like this:
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
class MyApiController < ApplicationController
|
95
|
+
def process
|
96
|
+
jsonrpc_payload = request.env['jsonrpc.payload']
|
97
|
+
|
98
|
+
if jsonrpc_payload.is_a?(Array)
|
99
|
+
# Handle batch request
|
100
|
+
responses = jsonrpc_payload.map do |request_object|
|
101
|
+
handle_single_request(request_object)
|
102
|
+
end.compact # Remove nil responses from notifications
|
103
|
+
render json: responses, status: (responses.empty? ? :no_content : :ok)
|
104
|
+
else
|
105
|
+
# Handle single request
|
106
|
+
response = handle_single_request(jsonrpc_payload)
|
107
|
+
if response # Check if it was a notification
|
108
|
+
# Use the gem's renderer for consistency
|
109
|
+
render jsonrpc: response[:result], id: response[:id], error: response.key?(:error) ? response[:error] : nil
|
110
|
+
else
|
111
|
+
head :no_content # No response for notification
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def handle_single_request(req)
|
119
|
+
method = req['method']
|
120
|
+
params = req['params']
|
121
|
+
id = req['id'] # Will be nil for notifications
|
122
|
+
|
123
|
+
result = case method
|
124
|
+
when 'add'
|
125
|
+
params.is_a?(Array) ? params.sum : { code: -32602, message: "Invalid params" }
|
126
|
+
when 'subtract'
|
127
|
+
params.is_a?(Array) && params.size == 2 ? params[0] - params[1] : { code: -32602, message: "Invalid params" }
|
128
|
+
else
|
129
|
+
{ code: -32601, message: "Method not found" }
|
130
|
+
end
|
131
|
+
|
132
|
+
# Only return a response structure if it's a request (has an id)
|
133
|
+
if id
|
134
|
+
if result.is_a?(Hash) && result[:code] # Check if result is an error hash
|
135
|
+
{ id: id, error: result }
|
136
|
+
else
|
137
|
+
{ id: id, result: result }
|
138
|
+
end
|
139
|
+
else
|
140
|
+
nil # No response for notifications
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
```
|
145
|
+
|
146
|
+
## Testing
|
147
|
+
|
148
|
+
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:
|
149
|
+
|
150
|
+
```bash
|
151
|
+
bundle exec rake test
|
152
|
+
```
|
153
|
+
|
154
|
+
The provided tests ensure that the renderer, middleware, and basic integration function correctly.
|
155
|
+
|
156
|
+
## Contributing
|
157
|
+
|
158
|
+
Contributions are very welcome! Feel free to fork the repository, make improvements, and submit pull requests. For bug reports or feature requests, please open an issue on GitHub:
|
159
|
+
|
160
|
+
[https://github.com/seuros/jsonrpc-rails](https://github.com/seuros/jsonrpc-rails)
|
161
|
+
|
162
|
+
## License
|
163
|
+
|
164
|
+
This gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
165
|
+
|
166
|
+
Happy coding with JSON-RPC in Rails!
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
require "rake/testtask"
|
3
|
+
|
4
|
+
# Configure the default test task
|
5
|
+
Rake::TestTask.new do |t|
|
6
|
+
t.libs << "test"
|
7
|
+
t.pattern = "test/**/*_test.rb" # Find tests recursively
|
8
|
+
t.verbose = true # Show test output
|
9
|
+
end
|
10
|
+
|
11
|
+
# Load engine tasks (might define other test tasks, but the default is now configured)
|
12
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
13
|
+
load "rails/tasks/engine.rake"
|
14
|
+
|
15
|
+
# Load other tasks
|
16
|
+
load "rails/tasks/statistics.rake"
|
17
|
+
require "bundler/gem_tasks"
|
18
|
+
|
19
|
+
# Ensure the default task runs our configured test task
|
20
|
+
task default: :test
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSON_RPC
|
4
|
+
# Custom exception class for JSON-RPC errors, based on the JSON-RPC 2.0 specification.
|
5
|
+
class JsonRpcError < StandardError
|
6
|
+
# Define the standard JSON-RPC 2.0 error codes
|
7
|
+
ERROR_CODES = {
|
8
|
+
parse_error: {
|
9
|
+
code: -32_700,
|
10
|
+
message: "Parse error"
|
11
|
+
},
|
12
|
+
invalid_request: {
|
13
|
+
code: -32_600,
|
14
|
+
message: "Invalid Request"
|
15
|
+
},
|
16
|
+
method_not_found: {
|
17
|
+
code: -32_601,
|
18
|
+
message: "Method not found"
|
19
|
+
},
|
20
|
+
invalid_params: {
|
21
|
+
code: -32_602,
|
22
|
+
message: "Invalid params"
|
23
|
+
},
|
24
|
+
internal_error: {
|
25
|
+
code: -32_603,
|
26
|
+
message: "Internal error"
|
27
|
+
},
|
28
|
+
# Implementation-defined server-errors -32000 to -32099
|
29
|
+
server_error: {
|
30
|
+
code: -32_000,
|
31
|
+
message: "Server error"
|
32
|
+
}
|
33
|
+
}.freeze
|
34
|
+
|
35
|
+
# @return [Integer] The error code.
|
36
|
+
# @return [Object] The error data.
|
37
|
+
attr_reader :code, :data
|
38
|
+
|
39
|
+
# Retrieve error details by symbol.
|
40
|
+
#
|
41
|
+
# @param symbol [Symbol] The error symbol.
|
42
|
+
# @raise [ArgumentError] if the error code is unknown.
|
43
|
+
# @return [Hash] The error details.
|
44
|
+
def self.[](symbol)
|
45
|
+
ERROR_CODES[symbol] or raise ArgumentError, "Unknown error symbol: #{symbol}"
|
46
|
+
end
|
47
|
+
|
48
|
+
# Retrieve error details by code.
|
49
|
+
#
|
50
|
+
# @param code [Integer] The error code.
|
51
|
+
# @return [Hash, nil] The error details hash if found, otherwise nil.
|
52
|
+
def self.find_by_code(code)
|
53
|
+
ERROR_CODES.values.find { |details| details[:code] == code }
|
54
|
+
end
|
55
|
+
|
56
|
+
# Build an error hash, allowing custom message or data to override defaults.
|
57
|
+
#
|
58
|
+
# @param symbol [Symbol] The error symbol.
|
59
|
+
# @param message [String, nil] Optional custom message.
|
60
|
+
# @param data [Object, nil] Optional custom data.
|
61
|
+
# @return [Hash] The error hash.
|
62
|
+
def self.build(symbol, message: nil, data: nil)
|
63
|
+
error = self[symbol].dup
|
64
|
+
error[:message] = message if message
|
65
|
+
error[:data] = data if data
|
66
|
+
error
|
67
|
+
end
|
68
|
+
|
69
|
+
# Initialize the error using a symbol key, with optional custom message and data.
|
70
|
+
#
|
71
|
+
# @param symbol [Symbol] The error symbol.
|
72
|
+
# @param message [String, nil] Optional custom message.
|
73
|
+
# @param data [Object, nil] Optional custom data.
|
74
|
+
def initialize(symbol, message: nil, data: nil)
|
75
|
+
error_details = self.class.build(symbol, message: message, data: data)
|
76
|
+
@code = error_details[:code]
|
77
|
+
@data = error_details[:data]
|
78
|
+
super(error_details[:message])
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns a hash formatted for a JSON-RPC error response object (the value of the 'error' key).
|
82
|
+
#
|
83
|
+
# @return [Hash] The error hash.
|
84
|
+
def to_h
|
85
|
+
hash = { code: code, message: message }
|
86
|
+
hash[:data] = data if data
|
87
|
+
hash
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSON_RPC
|
4
|
+
# Represents a JSON-RPC notification.
|
5
|
+
Notification = Data.define(:method, :params) do
|
6
|
+
# Initializes a new Notification.
|
7
|
+
#
|
8
|
+
# @param method [String] The method name.
|
9
|
+
# @param params [Hash, Array, nil] The parameters (optional). Structured value.
|
10
|
+
def initialize(method:, params: nil)
|
11
|
+
# Basic validation could be added here if needed, e.g., method is a non-empty string.
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns a hash representation of the notification, ready for JSON serialization.
|
16
|
+
#
|
17
|
+
# @return [Hash] The hash representation.
|
18
|
+
def to_h
|
19
|
+
hash = {
|
20
|
+
"jsonrpc" => "2.0",
|
21
|
+
method: method
|
22
|
+
}
|
23
|
+
# Include params only if it's not nil, as per JSON-RPC spec
|
24
|
+
hash[:params] = params unless params.nil?
|
25
|
+
hash
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSON_RPC
|
4
|
+
# Represents a JSON-RPC request object.
|
5
|
+
Request = Data.define(:id, :method, :params) do
|
6
|
+
# Initializes a new Request.
|
7
|
+
#
|
8
|
+
# @param id [String, Numeric, nil] The request identifier. Should be String or Numeric according to spec for requests needing a response. Can be nil.
|
9
|
+
# @param method [String] The method name.
|
10
|
+
# @param params [Hash, Array, nil] The parameters (optional). Structured value.
|
11
|
+
# @raise [JSON_RPC::JsonRpcError] if the ID type is invalid.
|
12
|
+
def initialize(id:, method:, params: nil)
|
13
|
+
# Basic validation for ID type (String, Numeric, or null allowed by spec)
|
14
|
+
validate_id_type(id)
|
15
|
+
# Basic validation for method (e.g., non-empty string) could be added.
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns a hash representation of the request, ready for JSON serialization.
|
20
|
+
#
|
21
|
+
# @return [Hash] The hash representation.
|
22
|
+
def to_h
|
23
|
+
hash = {
|
24
|
+
"jsonrpc" => "2.0",
|
25
|
+
id: id, # Include id even if null, spec allows null id
|
26
|
+
method: method
|
27
|
+
}
|
28
|
+
# Include params only if it's not nil
|
29
|
+
hash[:params] = params unless params.nil?
|
30
|
+
hash
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# Validates the ID type according to JSON-RPC 2.0 spec.
|
36
|
+
# Allows String, Numeric, or null.
|
37
|
+
#
|
38
|
+
# @param id [Object] The ID to validate.
|
39
|
+
# @raise [JSON_RPC::JsonRpcError] if the ID type is invalid.
|
40
|
+
def validate_id_type(id)
|
41
|
+
unless id.is_a?(String) || id.is_a?(Numeric) || id.nil?
|
42
|
+
# Using :invalid_request as the error type seems more appropriate for a malformed ID type.
|
43
|
+
raise JSON_RPC::JsonRpcError.new(:invalid_request,
|
44
|
+
message: "ID must be a string, number, or null")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSON_RPC
|
4
|
+
# Represents a JSON-RPC response object.
|
5
|
+
Response = Data.define(:id, :result, :error) do
|
6
|
+
# Initializes a new Response.
|
7
|
+
#
|
8
|
+
# @param id [String, Numeric, nil] The request identifier. Must match the request ID.
|
9
|
+
# @param result [Object, nil] The result data (if successful).
|
10
|
+
# @param error [Hash, JSON_RPC::JsonRpcError, Symbol, nil] The error object/symbol (if failed).
|
11
|
+
# @raise [ArgumentError] if both result and error are provided, or neither is provided for non-null id.
|
12
|
+
def initialize(id:, result: nil, error: nil)
|
13
|
+
validate_response(id, result, error)
|
14
|
+
error_obj = process_error(error)
|
15
|
+
super(id: id, result: result, error: error_obj)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns a hash representation of the response, ready for JSON serialization.
|
19
|
+
#
|
20
|
+
# @return [Hash] The hash representation.
|
21
|
+
def to_h
|
22
|
+
hash = { "jsonrpc" => "2.0", id: id }
|
23
|
+
if error
|
24
|
+
hash[:error] = error # error is already a hash here
|
25
|
+
else
|
26
|
+
# Result must be included, even if null, for successful responses
|
27
|
+
hash[:result] = result
|
28
|
+
end
|
29
|
+
hash
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
# Validates the response structure according to JSON-RPC 2.0 spec.
|
35
|
+
#
|
36
|
+
# @param id [Object] The request ID.
|
37
|
+
# @param result [Object] The result data.
|
38
|
+
# @param error_input [Object] The error data/object/symbol.
|
39
|
+
# @raise [ArgumentError] for invalid combinations.
|
40
|
+
def validate_response(id, result, error_input)
|
41
|
+
# ID must be present (can be null) in a response matching a request.
|
42
|
+
|
43
|
+
if !error_input.nil? && !result.nil?
|
44
|
+
raise ArgumentError, "Response cannot contain both 'result' and 'error'"
|
45
|
+
end
|
46
|
+
|
47
|
+
# If id is not null, either result or error MUST be present.
|
48
|
+
if !id.nil? && error_input.nil? && result.nil?
|
49
|
+
# This check assumes if both are nil, it's invalid for non-null id.
|
50
|
+
# `result: nil` is a valid success response. The check should ideally know
|
51
|
+
# if `result` was explicitly passed as nil vs not passed at all.
|
52
|
+
# Data.define might make this tricky. Let's keep the original logic for now.
|
53
|
+
raise ArgumentError, "Response with non-null ID must contain either 'result' or 'error'"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Processes the error input into a standard error hash.
|
58
|
+
#
|
59
|
+
# @param error_input [Hash, JSON_RPC::JsonRpcError, Symbol, nil] The error information.
|
60
|
+
# @return [Hash, nil] The formatted error hash or nil.
|
61
|
+
def process_error(error_input)
|
62
|
+
case error_input
|
63
|
+
when nil
|
64
|
+
nil
|
65
|
+
when JSON_RPC::JsonRpcError
|
66
|
+
error_input.to_h
|
67
|
+
when Hash
|
68
|
+
# Assume it's already a valid JSON-RPC error object hash
|
69
|
+
error_input
|
70
|
+
when Symbol
|
71
|
+
# Build from a standard error symbol
|
72
|
+
JSON_RPC::JsonRpcError.build(error_input)
|
73
|
+
else
|
74
|
+
# Fallback to internal error if the format is unexpected
|
75
|
+
JSON_RPC::JsonRpcError.build(:internal_error, message: "Invalid error format provided").to_h
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "jsonrpc_rails/railtie"
|
4
|
+
|
5
|
+
require_relative "json_rpc/json_rpc_error"
|
6
|
+
require_relative "json_rpc/request"
|
7
|
+
require_relative "json_rpc/response"
|
8
|
+
require_relative "json_rpc/notification"
|
9
|
+
|
10
|
+
# Define the top-level module for the gem (optional, but good practice)
|
11
|
+
module JSON_RPC_Rails
|
12
|
+
# You might add gem-level configuration or methods here if needed later.
|
13
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module JSON_RPC_Rails
|
6
|
+
module Middleware
|
7
|
+
# Rack middleware to strictly validate incoming JSON-RPC 2.0 requests.
|
8
|
+
# It checks for correct Content-Type, parses JSON, and validates the structure
|
9
|
+
# of Hashes and non-empty Arrays according to JSON-RPC 2.0 spec.
|
10
|
+
#
|
11
|
+
# If validation passes, it stores the parsed payload in `request.env['jsonrpc.payload']`
|
12
|
+
# and passes the request down the stack.
|
13
|
+
#
|
14
|
+
# If JSON parsing fails, or if the payload is a Hash/Array and fails JSON-RPC validation,
|
15
|
+
# it immediately returns the appropriate JSON-RPC 2.0 error response.
|
16
|
+
#
|
17
|
+
# Other valid JSON payloads (e.g., strings, numbers, booleans, null) are passed through.
|
18
|
+
class Validator
|
19
|
+
CONTENT_TYPE = "application/json"
|
20
|
+
ENV_PAYLOAD_KEY = "jsonrpc.payload"
|
21
|
+
|
22
|
+
def initialize(app)
|
23
|
+
@app = app
|
24
|
+
end
|
25
|
+
|
26
|
+
def call(env)
|
27
|
+
request = Rack::Request.new(env)
|
28
|
+
|
29
|
+
# Only process POST requests with the correct Content-Type
|
30
|
+
unless request.post? && request.content_type&.start_with?(CONTENT_TYPE)
|
31
|
+
return @app.call(env)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Read and parse the request body
|
35
|
+
body = request.body.read
|
36
|
+
request.body.rewind # Rewind body for potential downstream middleware/apps
|
37
|
+
payload = parse_json(body)
|
38
|
+
|
39
|
+
# Handle JSON parsing errors
|
40
|
+
return jsonrpc_error_response(:parse_error) unless payload
|
41
|
+
|
42
|
+
# Only attempt JSON-RPC validation if payload is a Hash or Array
|
43
|
+
unless payload.is_a?(Hash) || payload.is_a?(Array)
|
44
|
+
# Pass through other valid JSON types (string, number, boolean, null)
|
45
|
+
return @app.call(env)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Payload is Hash or Array, proceed with JSON-RPC validation
|
49
|
+
is_batch = payload.is_a?(Array)
|
50
|
+
# validate_batch handles the empty array case internally now
|
51
|
+
validation_result, _ = is_batch ? validate_batch(payload) : validate_single(payload)
|
52
|
+
|
53
|
+
# If validation failed, return the generated error response
|
54
|
+
return validation_result unless validation_result == :valid
|
55
|
+
|
56
|
+
# Store the validated payload (original structure) in env for the controller
|
57
|
+
env[ENV_PAYLOAD_KEY] = payload
|
58
|
+
|
59
|
+
# Proceed to the next middleware/app
|
60
|
+
@app.call(env)
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
# Removed jsonrpc_payload? method
|
66
|
+
|
67
|
+
# Parses the JSON body string. Returns parsed data or nil on failure.
|
68
|
+
def parse_json(body)
|
69
|
+
return nil if body.nil? || body.strip.empty?
|
70
|
+
|
71
|
+
JSON.parse(body)
|
72
|
+
rescue JSON::ParserError
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
|
76
|
+
# Performs strict validation on a single object to ensure it conforms
|
77
|
+
# to the JSON-RPC 2.0 structure (jsonrpc, method, params, id) and
|
78
|
+
# has no extraneous keys.
|
79
|
+
# Returns true if valid, false otherwise.
|
80
|
+
def validate_single_structure(obj)
|
81
|
+
# Must be a Hash
|
82
|
+
return false unless obj.is_a?(Hash)
|
83
|
+
|
84
|
+
# Must have 'jsonrpc' key with value '2.0'
|
85
|
+
return false unless obj["jsonrpc"] == "2.0"
|
86
|
+
|
87
|
+
# Must have 'method' key with a String value
|
88
|
+
return false unless obj["method"].is_a?(String)
|
89
|
+
|
90
|
+
# Optional 'params' must be an Array or Hash if present
|
91
|
+
if obj.key?("params") && !obj["params"].is_a?(Array) && !obj["params"].is_a?(Hash)
|
92
|
+
return false
|
93
|
+
end
|
94
|
+
|
95
|
+
# Optional 'id' must be a String, Number (Integer/Float), or Null if present
|
96
|
+
if obj.key?("id") && ![ String, Integer, Float, NilClass ].include?(obj["id"].class)
|
97
|
+
return false
|
98
|
+
end
|
99
|
+
|
100
|
+
# Check for extraneous keys
|
101
|
+
allowed_keys = %w[jsonrpc method params id]
|
102
|
+
return false unless (obj.keys - allowed_keys).empty?
|
103
|
+
|
104
|
+
true # Structure is valid
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
# Validates a single JSON-RPC request object (must be a Hash).
|
109
|
+
# Returns [:valid, nil] on success.
|
110
|
+
# Returns [error_response_tuple, nil] on failure.
|
111
|
+
def validate_single(obj)
|
112
|
+
# Assumes obj is a Hash due to check in `call`
|
113
|
+
if validate_single_structure(obj)
|
114
|
+
[ :valid, nil ]
|
115
|
+
else
|
116
|
+
# Generate error response if structure is invalid (e.g., missing 'jsonrpc')
|
117
|
+
[ jsonrpc_error_response(:invalid_request), nil ]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Validates a batch JSON-RPC request (must be an Array).
|
122
|
+
# Returns [:valid, nil] if the batch structure is valid.
|
123
|
+
# Returns [error_response_tuple, nil] if the batch is empty or any element is invalid.
|
124
|
+
def validate_batch(batch)
|
125
|
+
# Assumes batch is an Array due to check in `call`
|
126
|
+
# Batch request must be a non-empty array according to spec
|
127
|
+
unless batch.is_a?(Array) && !batch.empty?
|
128
|
+
return [ jsonrpc_error_response(:invalid_request), nil ]
|
129
|
+
end
|
130
|
+
|
131
|
+
# Find first invalid element - stops processing as soon as it finds one
|
132
|
+
invalid_element = batch.find { |element| !validate_single_structure(element) }
|
133
|
+
|
134
|
+
# If an invalid element was found, return an error response immediately
|
135
|
+
if invalid_element
|
136
|
+
return [ jsonrpc_error_response(:invalid_request), nil ]
|
137
|
+
end
|
138
|
+
|
139
|
+
# All elements passed structural validation
|
140
|
+
[ :valid, nil ]
|
141
|
+
end
|
142
|
+
|
143
|
+
# Generates a Rack response tuple for a given JSON-RPC error.
|
144
|
+
# Middleware-level errors always have id: nil.
|
145
|
+
# @param error_type [Symbol, JSON_RPC::JsonRpcError] The error symbol or object.
|
146
|
+
# @param status [Integer] The HTTP status code.
|
147
|
+
# @return [Array] Rack response tuple.
|
148
|
+
def jsonrpc_error_response(error_type, status: 400)
|
149
|
+
error_obj = if error_type.is_a?(JSON_RPC::JsonRpcError)
|
150
|
+
error_type
|
151
|
+
else
|
152
|
+
JSON_RPC::JsonRpcError.new(error_type)
|
153
|
+
end
|
154
|
+
|
155
|
+
response_body = {
|
156
|
+
jsonrpc: "2.0",
|
157
|
+
error: error_obj.to_h,
|
158
|
+
id: nil # Middleware errors have null id
|
159
|
+
}.to_json
|
160
|
+
|
161
|
+
[
|
162
|
+
status,
|
163
|
+
{ "Content-Type" => CONTENT_TYPE },
|
164
|
+
[ response_body ]
|
165
|
+
]
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require_relative "middleware/validator"
|
2
|
+
|
3
|
+
module JSON_RPC_Rails
|
4
|
+
# Use Rails::Railtie to integrate with the Rails application
|
5
|
+
class Railtie < Rails::Railtie
|
6
|
+
# Insert the JSON-RPC Validator middleware early in the stack.
|
7
|
+
# Inserting before Rack::Sendfile, which is typically present early in the stack.
|
8
|
+
initializer "jsonrpc-rails.add_validator_middleware" do |app|
|
9
|
+
app.middleware.use JSON_RPC_Rails::Middleware::Validator
|
10
|
+
end
|
11
|
+
|
12
|
+
initializer "jsonrpc-rails.add_renderers" do
|
13
|
+
ActiveSupport.on_load(:action_controller) do
|
14
|
+
ActionController::Renderers.add :jsonrpc do |obj, options|
|
15
|
+
# Use the Response class to build the payload
|
16
|
+
response_id = options[:id] # ID is required for Response
|
17
|
+
error_input = options[:error] # Can be nil, Symbol, Hash, or JsonRpcError
|
18
|
+
payload_obj = obj # The main object passed to render
|
19
|
+
|
20
|
+
begin
|
21
|
+
response_obj = case error_input
|
22
|
+
when Symbol
|
23
|
+
# Build error from symbol, allowing overrides from payload_obj
|
24
|
+
message_override = payload_obj.is_a?(Hash) ? payload_obj[:message] : nil
|
25
|
+
data_override = payload_obj.is_a?(Hash) ? payload_obj[:data] : nil
|
26
|
+
error_hash = JSON_RPC::JsonRpcError.build(error_input, message: message_override, data: data_override)
|
27
|
+
JSON_RPC::Response.new(id: response_id, error: error_hash)
|
28
|
+
when Integer
|
29
|
+
# Build error from numeric code, allowing overrides from payload_obj
|
30
|
+
error_code = error_input
|
31
|
+
default_details = JSON_RPC::JsonRpcError.find_by_code(error_code)
|
32
|
+
message_override = payload_obj.is_a?(Hash) ? payload_obj[:message] : nil
|
33
|
+
data_override = payload_obj.is_a?(Hash) ? payload_obj[:data] : nil
|
34
|
+
error_hash = {
|
35
|
+
code: error_code,
|
36
|
+
message: message_override || default_details&.fetch(:message, "Unknown error") # Use override, default, or generic
|
37
|
+
}
|
38
|
+
error_hash[:data] = data_override if data_override
|
39
|
+
JSON_RPC::Response.new(id: response_id, error: error_hash)
|
40
|
+
when ->(ei) { ei } # Catch any other truthy value
|
41
|
+
raise ArgumentError, "The :error option for render :jsonrpc must be a Symbol or an Integer, got: #{error_input.inspect}"
|
42
|
+
# # Original logic (removed): Treat payload_obj as the error hash
|
43
|
+
# JSON_RPC::Response.new(id: response_id, error: payload_obj)
|
44
|
+
else # Falsy (nil, false)
|
45
|
+
# Treat payload_obj as the result
|
46
|
+
JSON_RPC::Response.new(id: response_id, result: payload_obj)
|
47
|
+
end
|
48
|
+
response_payload = response_obj.to_h
|
49
|
+
rescue ArgumentError => e
|
50
|
+
# Handle cases where Response initialization fails (e.g., invalid id/result/error combo)
|
51
|
+
# Respond with an Internal Error according to JSON-RPC spec
|
52
|
+
internal_error = JSON_RPC::JsonRpcError.new(:internal_error, message: "Server error generating response: #{e.message}")
|
53
|
+
response_payload = { jsonrpc: "2.0", error: internal_error.to_h, id: response_id }
|
54
|
+
# Consider logging the error e.message
|
55
|
+
rescue JSON_RPC::JsonRpcError => e
|
56
|
+
# Handle specific JsonRpcError during Response processing (e.g., invalid error symbol)
|
57
|
+
response_payload = { jsonrpc: "2.0", error: e.to_h, id: response_id }
|
58
|
+
# Consider logging the error e.message
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
# Set the proper MIME type and convert the hash to JSON.
|
63
|
+
self.content_type ||= Mime[:json]
|
64
|
+
self.response_body = response_payload.to_json
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jsonrpc-rails
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Abdelkader Boudih
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-03-27 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: railties
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: 8.0.1
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: 8.0.1
|
26
|
+
description: Integrates into Rails, allowing you to render JSON-RPC responses and
|
27
|
+
validate incoming requests according to the JSON-RPC 2.0 specification. Includes
|
28
|
+
middleware for strict request validation and a custom renderer. Designed for Rails
|
29
|
+
8+.
|
30
|
+
email:
|
31
|
+
- terminale@gmail.com
|
32
|
+
executables: []
|
33
|
+
extensions: []
|
34
|
+
extra_rdoc_files: []
|
35
|
+
files:
|
36
|
+
- MIT-LICENSE
|
37
|
+
- README.md
|
38
|
+
- Rakefile
|
39
|
+
- lib/json_rpc/json_rpc_error.rb
|
40
|
+
- lib/json_rpc/notification.rb
|
41
|
+
- lib/json_rpc/request.rb
|
42
|
+
- lib/json_rpc/response.rb
|
43
|
+
- lib/jsonrpc-rails.rb
|
44
|
+
- lib/jsonrpc_rails/middleware/validator.rb
|
45
|
+
- lib/jsonrpc_rails/railtie.rb
|
46
|
+
- lib/jsonrpc_rails/version.rb
|
47
|
+
homepage: https://github.com/seuros/jsonrpc-rails
|
48
|
+
licenses:
|
49
|
+
- MIT
|
50
|
+
metadata:
|
51
|
+
homepage_uri: https://github.com/seuros/jsonrpc-rails
|
52
|
+
source_code_uri: https://github.com/seuros/jsonrpc-rails
|
53
|
+
changelog_uri: https://github.com/seuros/jsonrpc-rails/blob/main/CHANGELOG.md
|
54
|
+
rdoc_options: []
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
requirements: []
|
68
|
+
rubygems_version: 3.6.5
|
69
|
+
specification_version: 4
|
70
|
+
summary: A Railtie-based gem that brings JSON-RPC 2.0 support to your Rails application.
|
71
|
+
test_files: []
|