jsonrpc-rails 0.2.0 → 0.4.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 +79 -38
- data/Rakefile +10 -8
- data/lib/json_rpc/json_rpc_error.rb +3 -0
- data/lib/json_rpc/notification.rb +6 -0
- data/lib/json_rpc/parser.rb +28 -0
- data/lib/json_rpc/request.rb +11 -5
- data/lib/json_rpc/response.rb +14 -10
- data/lib/jsonrpc-rails.rb +1 -0
- data/lib/jsonrpc_rails/controller_helpers.rb +20 -0
- data/lib/jsonrpc_rails/middleware/validator.rb +66 -117
- data/lib/jsonrpc_rails/railtie.rb +17 -5
- data/lib/jsonrpc_rails/version.rb +3 -1
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5ca6d0bdf5ffc4f71a7be0e4669bad7bd1a215a647d9e5604be69b40caa86acd
|
4
|
+
data.tar.gz: 27359e3bc0eeaab09c659fd618f20eb15448293180d4f437fd0cd1deff1584ef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fc155ba77be68184eaeefc330a0f1829e5c0854da1cce49b7abddc870cbc158eccac3881d0bf4d00af932609aae4b84abd99868761d2ccb074840456c9d6e0b8
|
7
|
+
data.tar.gz: 866302ed5d32d518fbd3fc6be6cfd34c7a0e7d3f4e4466cd96cb99a102ab5795b9f29a14501e3ed96332e22472892d764087fc0668898e11dbe2fb90b9ff16ab
|
data/README.md
CHANGED
@@ -38,6 +38,22 @@ gem install jsonrpc-rails
|
|
38
38
|
### Rendering Responses
|
39
39
|
|
40
40
|
Once installed, **jsonrpc-rails** registers a custom renderer with Rails.
|
41
|
+
|
42
|
+
Enable validation where you need it
|
43
|
+
|
44
|
+
Add this to config/application.rb (or an environment file):
|
45
|
+
```ruby
|
46
|
+
# Validate only the JSON‑RPC endpoints you expose
|
47
|
+
config.jsonrpc_rails.validated_paths = [
|
48
|
+
"/rpc", # exact string
|
49
|
+
%r{\A/api/v\d+/rpc\z}, # regexp
|
50
|
+
->(p) { p.start_with? "/rpc/private" } # lambda / proc
|
51
|
+
]
|
52
|
+
```
|
53
|
+
|
54
|
+
Leave the array empty (default) and the middleware is effectively off.
|
55
|
+
Use [/.*\z/] if you really want it on everywhere.
|
56
|
+
|
41
57
|
In your controllers, you can render JSON-RPC responses like so:
|
42
58
|
|
43
59
|
```ruby
|
@@ -62,7 +78,7 @@ end
|
|
62
78
|
The renderer wraps your data in the JSON-RPC 2.0 envelope:
|
63
79
|
- **Success Response:**
|
64
80
|
```json
|
65
|
-
{ "jsonrpc": "2.0", "result": {
|
81
|
+
{ "jsonrpc": "2.0", "result": { }, "id": 1 }
|
66
82
|
```
|
67
83
|
- **Error Response (using numeric code):**
|
68
84
|
```json
|
@@ -85,59 +101,84 @@ The gem automatically inserts `JSONRPC_Rails::Middleware::Validator` into your a
|
|
85
101
|
|
86
102
|
1. **Parses** the JSON body. Returns a JSON-RPC `Parse error (-32700)` if parsing fails.
|
87
103
|
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[
|
104
|
+
3. **Stores** the validated, parsed payload (the original Ruby Hash or Array) in `request.env[:jsonrpc]` if validation succeeds.
|
89
105
|
4. **Passes** the request to the next middleware or your controller action.
|
90
106
|
|
91
107
|
In your controller action, you can access the validated payload like this:
|
92
108
|
|
93
109
|
```ruby
|
110
|
+
# app/controllers/my_api_controller.rb
|
94
111
|
class MyApiController < ApplicationController
|
112
|
+
# POST /rpc
|
95
113
|
def process
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
responses
|
101
|
-
|
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
|
114
|
+
if jsonrpc_batch?
|
115
|
+
# ── batch ───────────────────────────────────────────────────────────────
|
116
|
+
responses = jsonrpc.filter_map { |req| handle_single_request(req) } # strip nil (notifications)
|
117
|
+
|
118
|
+
if responses.empty?
|
119
|
+
head :no_content
|
110
120
|
else
|
111
|
-
|
121
|
+
# respond with an array of hashes; to_h keeps the structs internal
|
122
|
+
render json: responses.map(&:to_h), status: :ok
|
123
|
+
end
|
124
|
+
else
|
125
|
+
# ── single ──────────────────────────────────────────────────────────────
|
126
|
+
response = handle_single_request(jsonrpc)
|
127
|
+
|
128
|
+
if response # request (has id)
|
129
|
+
render jsonrpc: response.result,
|
130
|
+
id: response.id,
|
131
|
+
error: response.error
|
132
|
+
else # notification
|
133
|
+
head :no_content
|
112
134
|
end
|
113
135
|
end
|
114
136
|
end
|
115
137
|
|
116
138
|
private
|
117
139
|
|
140
|
+
# Map one JSON_RPC::Request|Notification → Response|nil
|
118
141
|
def handle_single_request(req)
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
142
|
+
# Notifications have id == nil. Return nil = no response.
|
143
|
+
return nil if req.id.nil?
|
144
|
+
|
145
|
+
result_or_error =
|
146
|
+
case req.method
|
147
|
+
when "add"
|
148
|
+
add(*Array(req.params))
|
149
|
+
when "subtract"
|
150
|
+
subtract(*Array(req.params))
|
151
|
+
else
|
152
|
+
method_not_found
|
153
|
+
end
|
154
|
+
|
155
|
+
build_response(req.id, result_or_error)
|
156
|
+
end
|
157
|
+
|
158
|
+
# ───────────────── helper methods ──────────────────────────────────────────
|
159
|
+
def add(*nums)
|
160
|
+
return invalid_params unless nums.all? { |n| n.is_a?(Numeric) }
|
161
|
+
nums.sum
|
162
|
+
end
|
163
|
+
|
164
|
+
def subtract(a = nil, b = nil)
|
165
|
+
return invalid_params unless a.is_a?(Numeric) && b.is_a?(Numeric)
|
166
|
+
a - b
|
167
|
+
end
|
168
|
+
|
169
|
+
def invalid_params
|
170
|
+
JSON_RPC::JsonRpcError.build(:invalid_params)
|
171
|
+
end
|
172
|
+
|
173
|
+
def method_not_found
|
174
|
+
JSON_RPC::JsonRpcError.build(:method_not_found)
|
175
|
+
end
|
176
|
+
|
177
|
+
def build_response(id, outcome)
|
178
|
+
if outcome.is_a?(JSON_RPC::JsonRpcError)
|
179
|
+
JSON_RPC::Response.new(id: id, error: outcome)
|
139
180
|
else
|
140
|
-
|
181
|
+
JSON_RPC::Response.new(id: id, result: outcome)
|
141
182
|
end
|
142
183
|
end
|
143
184
|
end
|
data/Rakefile
CHANGED
@@ -1,20 +1,22 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'rake/testtask'
|
3
5
|
|
4
6
|
# Configure the default test task
|
5
7
|
Rake::TestTask.new do |t|
|
6
|
-
t.libs <<
|
7
|
-
t.pattern =
|
8
|
+
t.libs << 'test'
|
9
|
+
t.pattern = 'test/**/*_test.rb' # Find tests recursively
|
8
10
|
t.verbose = true # Show test output
|
9
11
|
end
|
10
12
|
|
11
13
|
# Load engine tasks (might define other test tasks, but the default is now configured)
|
12
|
-
APP_RAKEFILE = File.expand_path(
|
13
|
-
load
|
14
|
+
APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__)
|
15
|
+
load 'rails/tasks/engine.rake'
|
14
16
|
|
15
17
|
# Load other tasks
|
16
|
-
load
|
17
|
-
require
|
18
|
+
load 'rails/tasks/statistics.rake'
|
19
|
+
require 'bundler/gem_tasks'
|
18
20
|
|
19
21
|
# Ensure the default task runs our configured test task
|
20
22
|
task default: :test
|
@@ -12,6 +12,10 @@ module JSON_RPC
|
|
12
12
|
super
|
13
13
|
end
|
14
14
|
|
15
|
+
def self.from_h(h)
|
16
|
+
new(method: h["method"], params: h["params"])
|
17
|
+
end
|
18
|
+
|
15
19
|
# Returns a hash representation of the notification, ready for JSON serialization.
|
16
20
|
#
|
17
21
|
# @return [Hash] The hash representation.
|
@@ -24,5 +28,7 @@ module JSON_RPC
|
|
24
28
|
hash[:params] = params unless params.nil?
|
25
29
|
hash
|
26
30
|
end
|
31
|
+
|
32
|
+
def as_json(*) = to_h
|
27
33
|
end
|
28
34
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSON_RPC
|
4
|
+
module Parser
|
5
|
+
# Convert one JSON-RPC hash into a typed object.
|
6
|
+
def self.object_from_hash(h)
|
7
|
+
# Order matters: responses have id *plus* result/error,
|
8
|
+
# so check for those keys first.
|
9
|
+
if h.key?("result") || h.key?("error")
|
10
|
+
Response.from_h(h)
|
11
|
+
elsif h.key?("id")
|
12
|
+
Request.from_h(h) # request (id may be nil)
|
13
|
+
else
|
14
|
+
Notification.from_h(h) # no id ⇒ notification
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Convert raw JSON string into typed object(s).
|
19
|
+
def self.array_from_json(json)
|
20
|
+
raw = ActiveSupport::JSON.decode(json)
|
21
|
+
case raw
|
22
|
+
when Hash then object_from_hash(raw)
|
23
|
+
when Array then raw.map { |h| object_from_hash(h) }
|
24
|
+
else raw # let validator scream later
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/json_rpc/request.rb
CHANGED
@@ -16,6 +16,10 @@ module JSON_RPC
|
|
16
16
|
super
|
17
17
|
end
|
18
18
|
|
19
|
+
def self.from_h(h)
|
20
|
+
new(id: h["id"], method: h["method"], params: h["params"])
|
21
|
+
end
|
22
|
+
|
19
23
|
# Returns a hash representation of the request, ready for JSON serialization.
|
20
24
|
#
|
21
25
|
# @return [Hash] The hash representation.
|
@@ -30,6 +34,8 @@ module JSON_RPC
|
|
30
34
|
hash
|
31
35
|
end
|
32
36
|
|
37
|
+
def as_json(*) = to_h
|
38
|
+
|
33
39
|
private
|
34
40
|
|
35
41
|
# Validates the ID type according to JSON-RPC 2.0 spec.
|
@@ -38,11 +44,11 @@ module JSON_RPC
|
|
38
44
|
# @param id [Object] The ID to validate.
|
39
45
|
# @raise [JSON_RPC::JsonRpcError] if the ID type is invalid.
|
40
46
|
def validate_id_type(id)
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
47
|
+
return if id.is_a?(String) || id.is_a?(Numeric) || id.nil?
|
48
|
+
|
49
|
+
# Using :invalid_request as the error type seems more appropriate for a malformed ID type.
|
50
|
+
raise JSON_RPC::JsonRpcError.new(:invalid_request,
|
51
|
+
message: "ID must be a string, number, or null")
|
46
52
|
end
|
47
53
|
end
|
48
54
|
end
|
data/lib/json_rpc/response.rb
CHANGED
@@ -15,6 +15,10 @@ module JSON_RPC
|
|
15
15
|
super(id: id, result: result, error: error_obj)
|
16
16
|
end
|
17
17
|
|
18
|
+
def self.from_h(h)
|
19
|
+
new(id: h["id"], result: h["result"], error: h["error"])
|
20
|
+
end
|
21
|
+
|
18
22
|
# Returns a hash representation of the response, ready for JSON serialization.
|
19
23
|
#
|
20
24
|
# @return [Hash] The hash representation.
|
@@ -29,6 +33,8 @@ module JSON_RPC
|
|
29
33
|
hash
|
30
34
|
end
|
31
35
|
|
36
|
+
def as_json(*) = to_h
|
37
|
+
|
32
38
|
private
|
33
39
|
|
34
40
|
# Validates the response structure according to JSON-RPC 2.0 spec.
|
@@ -40,18 +46,16 @@ module JSON_RPC
|
|
40
46
|
def validate_response(id, result, error_input)
|
41
47
|
# ID must be present (can be null) in a response matching a request.
|
42
48
|
|
43
|
-
if !error_input.nil? && !result.nil?
|
44
|
-
raise ArgumentError, "Response cannot contain both 'result' and 'error'"
|
45
|
-
end
|
49
|
+
raise ArgumentError, "Response cannot contain both 'result' and 'error'" if !error_input.nil? && !result.nil?
|
46
50
|
|
47
51
|
# If id is not null, either result or error MUST be present.
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
52
|
+
return unless !id.nil? && error_input.nil? && result.nil?
|
53
|
+
|
54
|
+
# This check assumes if both are nil, it's invalid for non-null id.
|
55
|
+
# `result: nil` is a valid success response. The check should ideally know
|
56
|
+
# if `result` was explicitly passed as nil vs not passed at all.
|
57
|
+
# Data.define might make this tricky. Let's keep the original logic for now.
|
58
|
+
raise ArgumentError, "Response with non-null ID must contain either 'result' or 'error'"
|
55
59
|
end
|
56
60
|
|
57
61
|
# Processes the error input into a standard error hash.
|
data/lib/jsonrpc-rails.rb
CHANGED
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSONRPC_Rails
|
4
|
+
module ControllerHelpers
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
# Returns a JSON_RPC::Request / Notification / Response
|
9
|
+
# or an Array of them for batch calls.
|
10
|
+
def jsonrpc
|
11
|
+
request.env[:jsonrpc]
|
12
|
+
end
|
13
|
+
|
14
|
+
# Convenience boolean
|
15
|
+
def jsonrpc_batch?
|
16
|
+
jsonrpc.is_a?(Array)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -5,177 +5,126 @@ require "active_support/json"
|
|
5
5
|
|
6
6
|
module JSONRPC_Rails
|
7
7
|
module Middleware
|
8
|
-
# Rack middleware
|
9
|
-
#
|
10
|
-
#
|
8
|
+
# Rack middleware that **validates** incoming JSON-RPC 2.0 payloads
|
9
|
+
# and injects fully-typed Ruby objects (Request / Notification /
|
10
|
+
# Response) into `env[:jsonrpc]` for easy downstream use.
|
11
11
|
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
# it immediately returns the appropriate JSON-RPC 2.0 error response.
|
17
|
-
#
|
18
|
-
# Other valid JSON payloads (e.g., strings, numbers, booleans, null) are passed through.
|
12
|
+
# Validation always runs on the raw Hash/Array first; objects are only
|
13
|
+
# instantiated **after** the structure has been deemed valid, so malformed
|
14
|
+
# IDs or empty batches no longer raise before we can return a proper
|
15
|
+
# -32600 “Invalid Request”.
|
19
16
|
class Validator
|
20
|
-
CONTENT_TYPE
|
21
|
-
ENV_PAYLOAD_KEY
|
22
|
-
|
23
|
-
|
24
|
-
|
17
|
+
CONTENT_TYPE = "application/json"
|
18
|
+
ENV_PAYLOAD_KEY = :jsonrpc
|
19
|
+
|
20
|
+
# @param app [#call]
|
21
|
+
# @param paths [Array<String, Regexp, Proc>] paths to validate
|
22
|
+
def initialize(app, paths = nil)
|
23
|
+
@app = app
|
24
|
+
@paths = Array(paths || Rails.configuration.jsonrpc_rails.validated_paths)
|
25
25
|
end
|
26
26
|
|
27
|
+
# Rack entry point
|
27
28
|
def call(env)
|
28
|
-
|
29
|
+
return @app.call(env) unless validate_path?(env["PATH_INFO"])
|
29
30
|
return @app.call(env) unless env["REQUEST_METHOD"] == "POST" &&
|
30
31
|
env["CONTENT_TYPE"]&.start_with?(CONTENT_TYPE)
|
31
32
|
|
32
|
-
# Read and parse the request body
|
33
|
-
# Safely read and rewind
|
34
33
|
body = env["rack.input"].read
|
35
34
|
env["rack.input"].rewind
|
36
|
-
payload = parse_json(body)
|
37
35
|
|
38
|
-
|
39
|
-
# If parsing fails (returns nil), pass through
|
40
|
-
return @app.call(env) unless payload
|
36
|
+
raw_payload = parse_json(body)
|
41
37
|
|
42
|
-
|
43
|
-
should_validate = false
|
44
|
-
is_batch = false
|
38
|
+
return jsonrpc_error_response(:invalid_request) unless raw_payload.is_a?(Hash) || raw_payload.is_a?(Array)
|
45
39
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
is_batch = false
|
51
|
-
when Array
|
52
|
-
# Validate Array only if ALL elements are Hashes AND at least one has 'jsonrpc' key
|
53
|
-
all_hashes = payload.all? { |el| el.is_a?(Hash) }
|
54
|
-
any_jsonrpc = payload.any? { |el| el.is_a?(Hash) && el.key?("jsonrpc") }
|
55
|
-
if all_hashes && any_jsonrpc
|
56
|
-
should_validate = true
|
57
|
-
is_batch = true
|
58
|
-
end
|
59
|
-
# Note: Empty arrays or arrays not meeting the criteria will pass through
|
40
|
+
validity, = if raw_payload.is_a?(Array)
|
41
|
+
validate_batch(raw_payload)
|
42
|
+
else
|
43
|
+
validate_single(raw_payload)
|
60
44
|
end
|
45
|
+
return validity unless validity == :valid
|
61
46
|
|
62
|
-
|
63
|
-
return @app.call(env) unless should_validate
|
64
|
-
|
65
|
-
# --- Proceed with strict validation ---
|
66
|
-
validation_result, _ = is_batch ? validate_batch(payload) : validate_single(payload)
|
47
|
+
env[ENV_PAYLOAD_KEY] = convert_to_objects(raw_payload)
|
67
48
|
|
68
|
-
# If strict validation failed (e.g., wrong version, missing method, invalid batch element), return the error
|
69
|
-
return validation_result unless validation_result == :valid
|
70
|
-
|
71
|
-
# Store the validated payload (original structure) in env for the controller
|
72
|
-
env[ENV_PAYLOAD_KEY] = payload
|
73
|
-
|
74
|
-
# Proceed to the next middleware/app
|
75
49
|
@app.call(env)
|
76
50
|
end
|
77
51
|
|
78
52
|
private
|
79
53
|
|
80
|
-
# Parses the JSON body string using ActiveSupport::JSON. Returns parsed data or nil on failure.
|
81
54
|
def parse_json(body)
|
82
55
|
return nil if body.nil? || body.strip.empty?
|
83
56
|
|
84
57
|
ActiveSupport::JSON.decode(body)
|
85
58
|
rescue ActiveSupport::JSON.parse_error
|
86
|
-
# Return nil if parsing fails, allowing the request to pass through
|
87
59
|
nil
|
88
60
|
end
|
89
61
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
62
|
+
def validate_path?(path)
|
63
|
+
return false if @paths.empty?
|
64
|
+
|
65
|
+
@paths.any? do |matcher|
|
66
|
+
case matcher
|
67
|
+
when String then path == matcher
|
68
|
+
when Regexp then matcher.match?(path)
|
69
|
+
when Proc then matcher.call(path)
|
70
|
+
else false
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# -------------------- structure validation --------------------------------
|
76
|
+
|
94
77
|
def validate_single_structure(obj)
|
95
|
-
# Must be a Hash
|
96
78
|
return false unless obj.is_a?(Hash)
|
97
|
-
|
98
|
-
# Must have 'jsonrpc' key with value '2.0'
|
99
79
|
return false unless obj["jsonrpc"] == "2.0"
|
100
|
-
|
101
|
-
# Must have 'method' key with a String value
|
102
80
|
return false unless obj["method"].is_a?(String)
|
103
81
|
|
104
|
-
|
105
|
-
if obj.key?("params") && !obj["params"].is_a?(Array) && !obj["params"].is_a?(Hash)
|
106
|
-
return false
|
107
|
-
end
|
108
|
-
|
109
|
-
# Optional 'id' must be a String, Number (Integer), or Null if present
|
110
|
-
if obj.key?("id") && ![ String, Integer, NilClass ].include?(obj["id"].class)
|
111
|
-
return false
|
112
|
-
end
|
82
|
+
return false if obj.key?("params") && !obj["params"].is_a?(Array) && !obj["params"].is_a?(Hash)
|
113
83
|
|
114
|
-
|
115
|
-
allowed_keys = %w[jsonrpc method params id]
|
116
|
-
return false unless (obj.keys - allowed_keys).empty?
|
84
|
+
return false if obj.key?("id") && ![ String, Integer, NilClass ].include?(obj["id"].class)
|
117
85
|
|
118
|
-
|
86
|
+
allowed = %w[jsonrpc method params id]
|
87
|
+
(obj.keys - allowed).empty?
|
119
88
|
end
|
120
89
|
|
121
|
-
|
122
|
-
# Validates a single JSON-RPC request object (must be a Hash).
|
123
|
-
# Returns [:valid, nil] on success.
|
124
|
-
# Returns [error_response_tuple, nil] on failure.
|
125
90
|
def validate_single(obj)
|
126
|
-
# Assumes obj is a Hash due to check in `call`
|
127
91
|
if validate_single_structure(obj)
|
128
92
|
[ :valid, nil ]
|
129
93
|
else
|
130
|
-
# Generate error response if structure is invalid (e.g., missing 'jsonrpc')
|
131
94
|
[ jsonrpc_error_response(:invalid_request), nil ]
|
132
95
|
end
|
133
96
|
end
|
134
97
|
|
135
|
-
# Validates a batch JSON-RPC request (must be an Array).
|
136
|
-
# Returns [:valid, nil] if the batch structure is valid.
|
137
|
-
# Returns [error_response_tuple, nil] if the batch is empty or any element is invalid.
|
138
98
|
def validate_batch(batch)
|
139
|
-
|
140
|
-
# Batch request must be a non-empty array according to spec
|
141
|
-
unless batch.is_a?(Array) && !batch.empty?
|
142
|
-
return [ jsonrpc_error_response(:invalid_request), nil ]
|
143
|
-
end
|
144
|
-
|
145
|
-
# Find first invalid element - stops processing as soon as it finds one
|
146
|
-
invalid_element = batch.find { |element| !validate_single_structure(element) }
|
99
|
+
return [ jsonrpc_error_response(:invalid_request), nil ] unless batch.is_a?(Array) && !batch.empty?
|
147
100
|
|
148
|
-
|
149
|
-
|
150
|
-
return [ jsonrpc_error_response(:invalid_request), nil ]
|
151
|
-
end
|
152
|
-
|
153
|
-
# All elements passed structural validation
|
154
|
-
[ :valid, nil ]
|
101
|
+
invalid = batch.find { |el| !validate_single_structure(el) }
|
102
|
+
invalid ? [ jsonrpc_error_response(:invalid_request), nil ] : [ :valid, nil ]
|
155
103
|
end
|
156
104
|
|
157
|
-
#
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
105
|
+
# ------------------ conversion to typed objects ---------------------------
|
106
|
+
|
107
|
+
def convert_to_objects(raw)
|
108
|
+
case raw
|
109
|
+
when Hash
|
110
|
+
JSON_RPC::Parser.object_from_hash(raw)
|
111
|
+
when Array
|
112
|
+
raw.map { |h| JSON_RPC::Parser.object_from_hash(h) }
|
165
113
|
else
|
166
|
-
|
114
|
+
raw # should never get here after validation
|
167
115
|
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# ------------------ error response helper ---------------------------------
|
168
119
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
120
|
+
# @param error_sym [Symbol]
|
121
|
+
# @param status [Integer]
|
122
|
+
# @return [Array] Rack triplet
|
123
|
+
def jsonrpc_error_response(error_sym, status: 400)
|
124
|
+
error_obj = JSON_RPC::JsonRpcError.build(error_sym)
|
125
|
+
payload = JSON_RPC::Response.new(id: nil, error: error_obj).to_json
|
173
126
|
|
174
|
-
[
|
175
|
-
status,
|
176
|
-
{ "Content-Type" => CONTENT_TYPE },
|
177
|
-
[ response_body ]
|
178
|
-
]
|
127
|
+
[ status, { "Content-Type" => CONTENT_TYPE }, [ payload ] ]
|
179
128
|
end
|
180
129
|
end
|
181
130
|
end
|
@@ -1,10 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "middleware/validator"
|
2
4
|
|
3
5
|
module JSONRPC_Rails
|
4
6
|
# Use Rails::Railtie to integrate with the Rails application
|
5
7
|
class Railtie < Rails::Railtie
|
8
|
+
config.jsonrpc_rails = ActiveSupport::OrderedOptions.new
|
9
|
+
config.jsonrpc_rails.validated_paths = [] # By default, we inject it into the void.
|
6
10
|
# Insert the JSON-RPC Validator middleware early in the stack.
|
7
|
-
# Inserting before Rack::Sendfile, which is typically present early in the stack.
|
8
11
|
initializer "jsonrpc-rails.add_validator_middleware" do |app|
|
9
12
|
app.middleware.use JSONRPC_Rails::Middleware::Validator
|
10
13
|
end
|
@@ -23,7 +26,8 @@ module JSONRPC_Rails
|
|
23
26
|
# Build error from symbol, allowing overrides from payload_obj
|
24
27
|
message_override = payload_obj.is_a?(Hash) ? payload_obj[:message] : nil
|
25
28
|
data_override = payload_obj.is_a?(Hash) ? payload_obj[:data] : nil
|
26
|
-
error_hash = JSON_RPC::JsonRpcError.build(error_input, message: message_override,
|
29
|
+
error_hash = JSON_RPC::JsonRpcError.build(error_input, message: message_override,
|
30
|
+
data: data_override)
|
27
31
|
JSON_RPC::Response.new(id: response_id, error: error_hash)
|
28
32
|
when Integer
|
29
33
|
# Build error from numeric code, allowing overrides from payload_obj
|
@@ -38,7 +42,8 @@ module JSONRPC_Rails
|
|
38
42
|
error_hash[:data] = data_override if data_override
|
39
43
|
JSON_RPC::Response.new(id: response_id, error: error_hash)
|
40
44
|
when ->(ei) { ei } # Catch any other truthy value
|
41
|
-
raise ArgumentError,
|
45
|
+
raise ArgumentError,
|
46
|
+
"The :error option for render :jsonrpc must be a Symbol or an Integer, got: #{error_input.inspect}"
|
42
47
|
# # Original logic (removed): Treat payload_obj as the error hash
|
43
48
|
# JSON_RPC::Response.new(id: response_id, error: payload_obj)
|
44
49
|
else # Falsy (nil, false)
|
@@ -49,7 +54,8 @@ module JSONRPC_Rails
|
|
49
54
|
rescue ArgumentError => e
|
50
55
|
# Handle cases where Response initialization fails (e.g., invalid id/result/error combo)
|
51
56
|
# Respond with an Internal Error according to JSON-RPC spec
|
52
|
-
internal_error = JSON_RPC::JsonRpcError.new(:internal_error,
|
57
|
+
internal_error = JSON_RPC::JsonRpcError.new(:internal_error,
|
58
|
+
message: "Server error generating response: #{e.message}")
|
53
59
|
response_payload = { jsonrpc: "2.0", error: internal_error.to_h, id: response_id }
|
54
60
|
# Consider logging the error e.message
|
55
61
|
rescue JSON_RPC::JsonRpcError => e
|
@@ -58,12 +64,18 @@ module JSONRPC_Rails
|
|
58
64
|
# Consider logging the error e.message
|
59
65
|
end
|
60
66
|
|
61
|
-
|
62
67
|
# Set the proper MIME type and convert the hash to JSON.
|
63
68
|
self.content_type ||= Mime[:json]
|
64
69
|
self.response_body = response_payload.to_json
|
65
70
|
end
|
66
71
|
end
|
67
72
|
end
|
73
|
+
|
74
|
+
initializer "jsonrpc-rails.controller_helpers" do
|
75
|
+
ActiveSupport.on_load(:action_controller) do
|
76
|
+
require_relative "controller_helpers"
|
77
|
+
include JSONRPC_Rails::ControllerHelpers
|
78
|
+
end
|
79
|
+
end
|
68
80
|
end
|
69
81
|
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.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Abdelkader Boudih
|
@@ -38,9 +38,11 @@ files:
|
|
38
38
|
- Rakefile
|
39
39
|
- lib/json_rpc/json_rpc_error.rb
|
40
40
|
- lib/json_rpc/notification.rb
|
41
|
+
- lib/json_rpc/parser.rb
|
41
42
|
- lib/json_rpc/request.rb
|
42
43
|
- lib/json_rpc/response.rb
|
43
44
|
- lib/jsonrpc-rails.rb
|
45
|
+
- lib/jsonrpc_rails/controller_helpers.rb
|
44
46
|
- lib/jsonrpc_rails/middleware/validator.rb
|
45
47
|
- lib/jsonrpc_rails/railtie.rb
|
46
48
|
- lib/jsonrpc_rails/version.rb
|