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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 145a4a20db9c6d49fa01ca8b9eb0c1a853288195a78c099b4ecc9eefc4f551d8
4
- data.tar.gz: c5ac9e090225a393917f0cda10c6d8a4d0593b428659d596539c1999edc3edcb
3
+ metadata.gz: 439108bab45d71dabc7ca40fa35042cdaecd7f3dc6988bc3a13fb0d825ef38ac
4
+ data.tar.gz: 34142d82edff4b8099610d44ca19bc039571fbcc0651a44c24588047a141838a
5
5
  SHA512:
6
- metadata.gz: a5d77db449217fddce82825a2daac7019d9c2133759dfd23cf01c1eb10fb4ec9d91b24ceca98fadb244b011a6becb15fe40256ab5d318cc4e65a5d23b2842247
7
- data.tar.gz: fdcf40b192d2f132d577c47195b007e67172e574f99936c3d74f2c30d31e7cc956a6dabba74ef8362777deaec2fb3f4a974736aa14991fd9e33ccba2bd28faf3
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": { ... }, "id": 1 }
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['jsonrpc.payload']` if validation succeeds.
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
- jsonrpc_payload = request.env['jsonrpc.payload']
113
-
114
- if jsonrpc_payload.is_a?(Array)
115
- # Handle batch request
116
- responses = jsonrpc_payload.map do |request_object|
117
- handle_single_request(request_object)
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
- head :no_content # No response for notification
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
- method = req['method']
136
- params = req['params']
137
- id = req['id'] # Will be nil for notifications
138
-
139
- result = case method
140
- when 'add'
141
- params.is_a?(Array) ? params.sum : { code: -32602, message: "Invalid params" }
142
- when 'subtract'
143
- params.is_a?(Array) && params.size == 2 ? params[0] - params[1] : { code: -32602, message: "Invalid params" }
144
- else
145
- { code: -32601, message: "Method not found" }
146
- end
147
-
148
- # Only return a response structure if it's a request (has an id)
149
- if id
150
- if result.is_a?(Hash) && result[:code] # Check if result is an error hash
151
- { id: id, error: result }
152
- else
153
- { id: id, result: result }
154
- end
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
- nil # No response for notifications
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
- require "bundler/setup"
2
- require "rake/testtask"
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 << "test"
7
- t.pattern = "test/**/*_test.rb" # Find tests recursively
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("test/dummy/Rakefile", __dir__)
13
- load "rails/tasks/engine.rake"
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 "rails/tasks/statistics.rake"
17
- require "bundler/gem_tasks"
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
@@ -86,5 +86,8 @@ module JSON_RPC
86
86
  hash[:data] = data if data
87
87
  hash
88
88
  end
89
+
90
+ # For ActiveSupport::JSON
91
+ def as_json(*) = to_h
89
92
  end
90
93
  end
@@ -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
@@ -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
- 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
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
@@ -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
- 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
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
@@ -6,6 +6,7 @@ require_relative "json_rpc/json_rpc_error"
6
6
  require_relative "json_rpc/request"
7
7
  require_relative "json_rpc/response"
8
8
  require_relative "json_rpc/notification"
9
+ require_relative "json_rpc/parser"
9
10
 
10
11
  module JSONRPC_Rails
11
12
  end
@@ -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 to strictly validate incoming JSON-RPC 2.0 requests.
9
- # It checks for correct Content-Type, parses JSON, and validates the structure
10
- # of Hashes and non-empty Arrays according to JSON-RPC 2.0 spec.
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
- # If validation passes, it stores the parsed payload in `request.env['jsonrpc.payload']`
13
- # and passes the request down the stack.
14
- #
15
- # If JSON parsing fails, or if the payload is a Hash/Array and fails JSON-RPC validation,
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 = "application/json"
21
- ENV_PAYLOAD_KEY = :"jsonrpc.payload"
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 = 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
- # Handle JSON parsing errors
43
- # If parsing fails (returns nil), pass through
44
- return @app.call(env) unless payload
36
+ raw_payload = parse_json(body)
45
37
 
46
- # Determine if we should proceed with strict validation based on payload type and content
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
- case payload
51
- when Hash
52
- # Validate single Hash only if 'jsonrpc' key is present
53
- should_validate = payload.key?("jsonrpc")
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
- # If conditions for validation are not met, pass through
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 |m|
98
- case m
99
- when String then path == m
100
- when Regexp then m.match?(path)
101
- when Proc then m.call(path)
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
- # Performs strict validation on a single object to ensure it conforms
108
- # to the JSON-RPC 2.0 structure (jsonrpc, method, params, id) and
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
- # Optional 'params' must be an Array or Hash if present
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
- # Check for extraneous keys
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
- true # Structure is valid
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
- # Assumes batch is an Array due to check in `call`
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
- # If an invalid element was found, return an error response immediately
166
- if invalid_element
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
- # Generates a Rack response tuple for a given JSON-RPC error.
175
- # Middleware-level errors always have id: nil.
176
- # @param error_type [Symbol, JSON_RPC::JsonRpcError] The error symbol or object.
177
- # @param status [Integer] The HTTP status code.
178
- # @return [Array] Rack response tuple.
179
- def jsonrpc_error_response(error_type, status: 400)
180
- error_obj = if error_type.is_a?(JSON_RPC::JsonRpcError)
181
- error_type
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
- JSON_RPC::JsonRpcError.new(error_type)
114
+ raw # should never get here after validation
184
115
  end
116
+ end
117
+
118
+ # ------------------ error response helper ---------------------------------
185
119
 
186
- response_body = JSON_RPC::Response.new(
187
- id: nil, # Middleware errors have null id
188
- error: error_obj.to_h,
189
- ).to_json
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 = [] # By default, we inject it into the void.
8
- # Insert the JSON-RPC Validator middleware early in the stack.
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
- # Use the Response class to build the payload
17
- response_id = options[:id] # ID is required for Response
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
- response_obj = case error_input
23
- when Symbol
24
- # Build error from symbol, allowing overrides from payload_obj
25
- message_override = payload_obj.is_a?(Hash) ? payload_obj[:message] : nil
26
- data_override = payload_obj.is_a?(Hash) ? payload_obj[:data] : nil
27
- error_hash = JSON_RPC::JsonRpcError.build(error_input, message: message_override, data: data_override)
28
- JSON_RPC::Response.new(id: response_id, error: error_hash)
29
- when Integer
30
- # Build error from numeric code, allowing overrides from payload_obj
31
- error_code = error_input
32
- default_details = JSON_RPC::JsonRpcError.find_by_code(error_code)
33
- message_override = payload_obj.is_a?(Hash) ? payload_obj[:message] : nil
34
- data_override = payload_obj.is_a?(Hash) ? payload_obj[:data] : nil
35
- error_hash = {
36
- code: error_code,
37
- message: message_override || default_details&.fetch(:message, "Unknown error") # Use override, default, or generic
38
- }
39
- error_hash[:data] = data_override if data_override
40
- JSON_RPC::Response.new(id: response_id, error: error_hash)
41
- when ->(ei) { ei } # Catch any other truthy value
42
- raise ArgumentError, "The :error option for render :jsonrpc must be a Symbol or an Integer, got: #{error_input.inspect}"
43
- # # Original logic (removed): Treat payload_obj as the error hash
44
- # JSON_RPC::Response.new(id: response_id, error: payload_obj)
45
- else # Falsy (nil, false)
46
- # Treat payload_obj as the result
47
- JSON_RPC::Response.new(id: response_id, result: payload_obj)
48
- end
49
- response_payload = response_obj.to_h
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
- # Handle cases where Response initialization fails (e.g., invalid id/result/error combo)
52
- # Respond with an Internal Error according to JSON-RPC spec
53
- internal_error = JSON_RPC::JsonRpcError.new(:internal_error, message: "Server error generating response: #{e.message}")
54
- response_payload = { jsonrpc: "2.0", error: internal_error.to_h, id: response_id }
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
- # Handle specific JsonRpcError during Response processing (e.g., invalid error symbol)
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 = response_payload.to_json
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONRPC_Rails
2
- VERSION = "0.3.1"
4
+ VERSION = "0.5.0"
3
5
  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.3.1
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