jsonrpc-rails 0.3.1 → 0.5.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 +63 -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 +54 -122
- data/lib/jsonrpc_rails/railtie.rb +82 -46
- 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: 439108bab45d71dabc7ca40fa35042cdaecd7f3dc6988bc3a13fb0d825ef38ac
|
4
|
+
data.tar.gz: 34142d82edff4b8099610d44ca19bc039571fbcc0651a44c24588047a141838a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a9b228edf10da4b05019c87643079e3ebe673fb98913dfba6567877293c28e8d2fd3a6507174d2aec8010d8254bab551631b38ac20e3784281773b4546bafb16
|
7
|
+
data.tar.gz: 3a4cbec99a1dd380a32263d6db57b4550f030aa50c67f29af3b8b25f827dc7d8a5c2be600e282426f36b826a82d393d2a61adda782040aeb0a44f76a455d96ba
|
data/README.md
CHANGED
@@ -78,7 +78,7 @@ end
|
|
78
78
|
The renderer wraps your data in the JSON-RPC 2.0 envelope:
|
79
79
|
- **Success Response:**
|
80
80
|
```json
|
81
|
-
{ "jsonrpc": "2.0", "result": {
|
81
|
+
{ "jsonrpc": "2.0", "result": { }, "id": 1 }
|
82
82
|
```
|
83
83
|
- **Error Response (using numeric code):**
|
84
84
|
```json
|
@@ -101,59 +101,84 @@ The gem automatically inserts `JSONRPC_Rails::Middleware::Validator` into your a
|
|
101
101
|
|
102
102
|
1. **Parses** the JSON body. Returns a JSON-RPC `Parse error (-32700)` if parsing fails.
|
103
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.
|
104
|
-
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.
|
105
105
|
4. **Passes** the request to the next middleware or your controller action.
|
106
106
|
|
107
107
|
In your controller action, you can access the validated payload like this:
|
108
108
|
|
109
109
|
```ruby
|
110
|
+
# app/controllers/my_api_controller.rb
|
110
111
|
class MyApiController < ApplicationController
|
112
|
+
# POST /rpc
|
111
113
|
def process
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
responses
|
117
|
-
|
118
|
-
end.compact # Remove nil responses from notifications
|
119
|
-
render json: responses, status: (responses.empty? ? :no_content : :ok)
|
120
|
-
else
|
121
|
-
# Handle single request
|
122
|
-
response = handle_single_request(jsonrpc_payload)
|
123
|
-
if response # Check if it was a notification
|
124
|
-
# Use the gem's renderer for consistency
|
125
|
-
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
|
126
120
|
else
|
127
|
-
|
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
|
128
134
|
end
|
129
135
|
end
|
130
136
|
end
|
131
137
|
|
132
138
|
private
|
133
139
|
|
140
|
+
# Map one JSON_RPC::Request|Notification → Response|nil
|
134
141
|
def handle_single_request(req)
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
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)
|
155
180
|
else
|
156
|
-
|
181
|
+
JSON_RPC::Response.new(id: id, result: outcome)
|
157
182
|
end
|
158
183
|
end
|
159
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,194 +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
|
17
|
+
CONTENT_TYPE = "application/json"
|
18
|
+
ENV_PAYLOAD_KEY = :jsonrpc
|
22
19
|
|
20
|
+
# @param app [#call]
|
21
|
+
# @param paths [Array<String, Regexp, Proc>] paths to validate
|
23
22
|
def initialize(app, paths = nil)
|
24
|
-
@app
|
25
|
-
|
23
|
+
@app = app
|
26
24
|
@paths = Array(paths || Rails.configuration.jsonrpc_rails.validated_paths)
|
27
25
|
end
|
28
26
|
|
27
|
+
# Rack entry point
|
29
28
|
def call(env)
|
30
29
|
return @app.call(env) unless validate_path?(env["PATH_INFO"])
|
31
|
-
|
32
|
-
# Only process POST requests with the correct Content-Type
|
33
30
|
return @app.call(env) unless env["REQUEST_METHOD"] == "POST" &&
|
34
31
|
env["CONTENT_TYPE"]&.start_with?(CONTENT_TYPE)
|
35
32
|
|
36
|
-
# Read and parse the request body
|
37
|
-
# Safely read and rewind
|
38
33
|
body = env["rack.input"].read
|
39
34
|
env["rack.input"].rewind
|
40
|
-
payload = parse_json(body)
|
41
35
|
|
42
|
-
|
43
|
-
# If parsing fails (returns nil), pass through
|
44
|
-
return @app.call(env) unless payload
|
36
|
+
raw_payload = parse_json(body)
|
45
37
|
|
46
|
-
|
47
|
-
should_validate = false
|
48
|
-
is_batch = false
|
38
|
+
return jsonrpc_error_response(:invalid_request) unless raw_payload.is_a?(Hash) || raw_payload.is_a?(Array)
|
49
39
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
is_batch = false
|
55
|
-
when Array
|
56
|
-
# Validate Array only if ALL elements are Hashes AND at least one has 'jsonrpc' key
|
57
|
-
all_hashes = payload.all? { |el| el.is_a?(Hash) }
|
58
|
-
any_jsonrpc = payload.any? { |el| el.is_a?(Hash) && el.key?("jsonrpc") }
|
59
|
-
if all_hashes && any_jsonrpc
|
60
|
-
should_validate = true
|
61
|
-
is_batch = true
|
62
|
-
end
|
63
|
-
# 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)
|
64
44
|
end
|
45
|
+
return validity unless validity == :valid
|
65
46
|
|
66
|
-
|
67
|
-
return @app.call(env) unless should_validate
|
68
|
-
|
69
|
-
# --- Proceed with strict validation ---
|
70
|
-
validation_result, _ = is_batch ? validate_batch(payload) : validate_single(payload)
|
71
|
-
|
72
|
-
# If strict validation failed (e.g., wrong version, missing method, invalid batch element), return the error
|
73
|
-
return validation_result unless validation_result == :valid
|
74
|
-
|
75
|
-
# Store the validated payload (original structure) in env for the controller
|
76
|
-
env[ENV_PAYLOAD_KEY] = payload
|
47
|
+
env[ENV_PAYLOAD_KEY] = convert_to_objects(raw_payload)
|
77
48
|
|
78
|
-
# Proceed to the next middleware/app
|
79
49
|
@app.call(env)
|
80
50
|
end
|
81
51
|
|
82
52
|
private
|
83
53
|
|
84
|
-
# Parses the JSON body string using ActiveSupport::JSON. Returns parsed data or nil on failure.
|
85
54
|
def parse_json(body)
|
86
55
|
return nil if body.nil? || body.strip.empty?
|
87
56
|
|
88
57
|
ActiveSupport::JSON.decode(body)
|
89
58
|
rescue ActiveSupport::JSON.parse_error
|
90
|
-
# Return nil if parsing fails, allowing the request to pass through
|
91
59
|
nil
|
92
60
|
end
|
93
61
|
|
94
62
|
def validate_path?(path)
|
95
63
|
return false if @paths.empty?
|
96
64
|
|
97
|
-
@paths.any? do |
|
98
|
-
case
|
99
|
-
when String then path ==
|
100
|
-
when Regexp then
|
101
|
-
when Proc then
|
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)
|
102
70
|
else false
|
103
71
|
end
|
104
72
|
end
|
105
73
|
end
|
106
74
|
|
107
|
-
#
|
108
|
-
|
109
|
-
# has no extraneous keys.
|
110
|
-
# Returns true if valid, false otherwise.
|
75
|
+
# -------------------- structure validation --------------------------------
|
76
|
+
|
111
77
|
def validate_single_structure(obj)
|
112
|
-
# Must be a Hash
|
113
78
|
return false unless obj.is_a?(Hash)
|
114
|
-
|
115
|
-
# Must have 'jsonrpc' key with value '2.0'
|
116
79
|
return false unless obj["jsonrpc"] == "2.0"
|
117
|
-
|
118
|
-
# Must have 'method' key with a String value
|
119
80
|
return false unless obj["method"].is_a?(String)
|
120
81
|
|
121
|
-
|
122
|
-
if obj.key?("params") && !obj["params"].is_a?(Array) && !obj["params"].is_a?(Hash)
|
123
|
-
return false
|
124
|
-
end
|
125
|
-
|
126
|
-
# Optional 'id' must be a String, Number (Integer), or Null if present
|
127
|
-
if obj.key?("id") && ![ String, Integer, NilClass ].include?(obj["id"].class)
|
128
|
-
return false
|
129
|
-
end
|
82
|
+
return false if obj.key?("params") && !obj["params"].is_a?(Array) && !obj["params"].is_a?(Hash)
|
130
83
|
|
131
|
-
|
132
|
-
allowed_keys = %w[jsonrpc method params id]
|
133
|
-
return false unless (obj.keys - allowed_keys).empty?
|
84
|
+
return false if obj.key?("id") && ![ String, Integer, NilClass ].include?(obj["id"].class)
|
134
85
|
|
135
|
-
|
86
|
+
allowed = %w[jsonrpc method params id]
|
87
|
+
(obj.keys - allowed).empty?
|
136
88
|
end
|
137
89
|
|
138
|
-
|
139
|
-
# Validates a single JSON-RPC request object (must be a Hash).
|
140
|
-
# Returns [:valid, nil] on success.
|
141
|
-
# Returns [error_response_tuple, nil] on failure.
|
142
90
|
def validate_single(obj)
|
143
|
-
# Assumes obj is a Hash due to check in `call`
|
144
91
|
if validate_single_structure(obj)
|
145
92
|
[ :valid, nil ]
|
146
93
|
else
|
147
|
-
# Generate error response if structure is invalid (e.g., missing 'jsonrpc')
|
148
94
|
[ jsonrpc_error_response(:invalid_request), nil ]
|
149
95
|
end
|
150
96
|
end
|
151
97
|
|
152
|
-
# Validates a batch JSON-RPC request (must be an Array).
|
153
|
-
# Returns [:valid, nil] if the batch structure is valid.
|
154
|
-
# Returns [error_response_tuple, nil] if the batch is empty or any element is invalid.
|
155
98
|
def validate_batch(batch)
|
156
|
-
|
157
|
-
# Batch request must be a non-empty array according to spec
|
158
|
-
unless batch.is_a?(Array) && !batch.empty?
|
159
|
-
return [ jsonrpc_error_response(:invalid_request), nil ]
|
160
|
-
end
|
161
|
-
|
162
|
-
# Find first invalid element - stops processing as soon as it finds one
|
163
|
-
invalid_element = batch.find { |element| !validate_single_structure(element) }
|
99
|
+
return [ jsonrpc_error_response(:invalid_request), nil ] unless batch.is_a?(Array) && !batch.empty?
|
164
100
|
|
165
|
-
|
166
|
-
|
167
|
-
return [ jsonrpc_error_response(:invalid_request), nil ]
|
168
|
-
end
|
169
|
-
|
170
|
-
# All elements passed structural validation
|
171
|
-
[ :valid, nil ]
|
101
|
+
invalid = batch.find { |el| !validate_single_structure(el) }
|
102
|
+
invalid ? [ jsonrpc_error_response(:invalid_request), nil ] : [ :valid, nil ]
|
172
103
|
end
|
173
104
|
|
174
|
-
#
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
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) }
|
182
113
|
else
|
183
|
-
|
114
|
+
raw # should never get here after validation
|
184
115
|
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# ------------------ error response helper ---------------------------------
|
185
119
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
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
|
190
126
|
|
191
|
-
[
|
192
|
-
status,
|
193
|
-
{ "Content-Type" => CONTENT_TYPE },
|
194
|
-
[ response_body ]
|
195
|
-
]
|
127
|
+
[ status, { "Content-Type" => CONTENT_TYPE }, [ payload ] ]
|
196
128
|
end
|
197
129
|
end
|
198
130
|
end
|
@@ -1,70 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# lib/jsonrpc_rails/railtie.rb
|
1
4
|
require_relative "middleware/validator"
|
2
5
|
|
3
6
|
module JSONRPC_Rails
|
4
|
-
# Use Rails::Railtie to integrate with the Rails application
|
5
7
|
class Railtie < Rails::Railtie
|
6
8
|
config.jsonrpc_rails = ActiveSupport::OrderedOptions.new
|
7
|
-
config.jsonrpc_rails.validated_paths = []
|
8
|
-
|
9
|
+
config.jsonrpc_rails.validated_paths = []
|
10
|
+
|
9
11
|
initializer "jsonrpc-rails.add_validator_middleware" do |app|
|
10
12
|
app.middleware.use JSONRPC_Rails::Middleware::Validator
|
11
13
|
end
|
12
14
|
|
15
|
+
# -----------------------------------------------------------------------
|
16
|
+
# Renderer
|
17
|
+
# -----------------------------------------------------------------------
|
13
18
|
initializer "jsonrpc-rails.add_renderers" do
|
14
19
|
ActiveSupport.on_load(:action_controller) do
|
15
20
|
ActionController::Renderers.add :jsonrpc do |obj, options|
|
16
|
-
|
17
|
-
|
18
|
-
error_input = options[:error] # Can be nil, Symbol, Hash, or JsonRpcError
|
19
|
-
payload_obj = obj # The main object passed to render
|
21
|
+
response_id = options[:id]
|
22
|
+
error_opt = options[:error]
|
20
23
|
|
21
24
|
begin
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
25
|
+
payload =
|
26
|
+
case obj
|
27
|
+
# ─── Already JSON-RPC objects ───────────────────────────────
|
28
|
+
when JSON_RPC::Response,
|
29
|
+
JSON_RPC::Request,
|
30
|
+
JSON_RPC::Notification
|
31
|
+
obj.to_h
|
32
|
+
|
33
|
+
# ─── Batch of objects ──────────────────────────────────────
|
34
|
+
when Array
|
35
|
+
unless obj.all? { |o| o.is_a?(JSON_RPC::Response) ||
|
36
|
+
o.is_a?(JSON_RPC::Request) ||
|
37
|
+
o.is_a?(JSON_RPC::Notification) }
|
38
|
+
raise ArgumentError, "Batch contains non-JSON-RPC objects"
|
39
|
+
end
|
40
|
+
obj.map(&:to_h)
|
41
|
+
|
42
|
+
# ─── Legacy “result + :error” path ─────────────────────────
|
43
|
+
else
|
44
|
+
case error_opt
|
45
|
+
when nil, false
|
46
|
+
JSON_RPC::Response.new(id: response_id,
|
47
|
+
result: obj).to_h
|
48
|
+
|
49
|
+
when Symbol
|
50
|
+
msg = obj.is_a?(Hash) ? obj[:message] : nil
|
51
|
+
dat = obj.is_a?(Hash) ? obj[:data] : nil
|
52
|
+
err = JSON_RPC::JsonRpcError.build(error_opt,
|
53
|
+
message: msg,
|
54
|
+
data: dat)
|
55
|
+
JSON_RPC::Response.new(id: response_id,
|
56
|
+
error: err).to_h
|
57
|
+
|
58
|
+
when Integer
|
59
|
+
defaults = JSON_RPC::JsonRpcError.find_by_code(error_opt)
|
60
|
+
msg = obj.is_a?(Hash) ? obj[:message] : nil
|
61
|
+
msg ||= defaults&.fetch(:message, "Unknown error")
|
62
|
+
|
63
|
+
dat = obj.is_a?(Hash) ? obj[:data] : nil
|
64
|
+
hash = { code: error_opt, message: msg }
|
65
|
+
hash[:data] = dat if dat
|
66
|
+
JSON_RPC::Response.new(id: response_id,
|
67
|
+
error: hash).to_h
|
68
|
+
|
69
|
+
when JSON_RPC::JsonRpcError
|
70
|
+
JSON_RPC::Response.new(id: response_id,
|
71
|
+
error: error_opt.to_h).to_h
|
72
|
+
|
73
|
+
when Hash
|
74
|
+
JSON_RPC::Response.new(id: response_id,
|
75
|
+
error: error_opt).to_h
|
76
|
+
|
77
|
+
else
|
78
|
+
raise ArgumentError,
|
79
|
+
":error must be Symbol, Integer, Hash, or JSON_RPC::JsonRpcError " \
|
80
|
+
"(got #{error_opt.class})"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
50
84
|
rescue ArgumentError => e
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
# Consider logging the error e.message
|
85
|
+
internal = JSON_RPC::JsonRpcError.new(:internal_error,
|
86
|
+
message: "Server error: #{e.message}")
|
87
|
+
payload = { jsonrpc: "2.0", error: internal.to_h, id: response_id }
|
88
|
+
|
56
89
|
rescue JSON_RPC::JsonRpcError => e
|
57
|
-
|
58
|
-
response_payload = { jsonrpc: "2.0", error: e.to_h, id: response_id }
|
59
|
-
# Consider logging the error e.message
|
90
|
+
payload = { jsonrpc: "2.0", error: e.to_h, id: response_id }
|
60
91
|
end
|
61
92
|
|
62
|
-
|
63
|
-
# Set the proper MIME type and convert the hash to JSON.
|
64
93
|
self.content_type ||= Mime[:json]
|
65
|
-
self.response_body =
|
94
|
+
self.response_body = payload.to_json
|
66
95
|
end
|
67
96
|
end
|
68
97
|
end
|
98
|
+
|
99
|
+
initializer "jsonrpc-rails.controller_helpers" do
|
100
|
+
ActiveSupport.on_load(:action_controller) do
|
101
|
+
require_relative "controller_helpers"
|
102
|
+
include JSONRPC_Rails::ControllerHelpers
|
103
|
+
end
|
104
|
+
end
|
69
105
|
end
|
70
106
|
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.5.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
|