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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7c7eaabe38b79dd41bac2752a456d5278e6cc780c3d2ebbe0493c349ff24ee10
4
- data.tar.gz: 51797ce9c50cd5e51b248f8a4376880f7ffcf19245daa8ae2ef1a4b9d5d17cdf
3
+ metadata.gz: 5ca6d0bdf5ffc4f71a7be0e4669bad7bd1a215a647d9e5604be69b40caa86acd
4
+ data.tar.gz: 27359e3bc0eeaab09c659fd618f20eb15448293180d4f437fd0cd1deff1584ef
5
5
  SHA512:
6
- metadata.gz: 3a55b1b369ff4d277b3351c6632a858bd00244cd0799108fbeaf4e64ba2e5b39c4687be0f1855e96df90343541e8ac0ab137684cad4f7af7394a75612b367395
7
- data.tar.gz: 9883f5c131ab3437cde5410f82a21f072824a045e1680ad27d81ef1f4d10d471149a7db639c18d8e2d194586979919224c4e921bae263236023d71934d2a6cc5
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": { ... }, "id": 1 }
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['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.
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
- 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
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
- 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
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
- 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
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
- nil # No response for notifications
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
- 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,177 +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"
22
-
23
- def initialize(app)
24
- @app = app
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
- # Only process POST requests with the correct Content-Type
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
- # Handle JSON parsing errors
39
- # If parsing fails (returns nil), pass through
40
- return @app.call(env) unless payload
36
+ raw_payload = parse_json(body)
41
37
 
42
- # Determine if we should proceed with strict validation based on payload type and content
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
- case payload
47
- when Hash
48
- # Validate single Hash only if 'jsonrpc' key is present
49
- should_validate = payload.key?("jsonrpc")
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
- # If conditions for validation are not met, pass through
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
- # Performs strict validation on a single object to ensure it conforms
91
- # to the JSON-RPC 2.0 structure (jsonrpc, method, params, id) and
92
- # has no extraneous keys.
93
- # Returns true if valid, false otherwise.
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
- # Optional 'params' must be an Array or Hash if present
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
- # Check for extraneous keys
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
- true # Structure is valid
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
- # Assumes batch is an Array due to check in `call`
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
- # If an invalid element was found, return an error response immediately
149
- if invalid_element
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
- # Generates a Rack response tuple for a given JSON-RPC error.
158
- # Middleware-level errors always have id: nil.
159
- # @param error_type [Symbol, JSON_RPC::JsonRpcError] The error symbol or object.
160
- # @param status [Integer] The HTTP status code.
161
- # @return [Array] Rack response tuple.
162
- def jsonrpc_error_response(error_type, status: 400)
163
- error_obj = if error_type.is_a?(JSON_RPC::JsonRpcError)
164
- 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) }
165
113
  else
166
- JSON_RPC::JsonRpcError.new(error_type)
114
+ raw # should never get here after validation
167
115
  end
116
+ end
117
+
118
+ # ------------------ error response helper ---------------------------------
168
119
 
169
- response_body = JSON_RPC::Response.new(
170
- id: nil, # Middleware errors have null id
171
- error: error_obj.to_h,
172
- ).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
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, data: data_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, "The :error option for render :jsonrpc must be a Symbol or an Integer, got: #{error_input.inspect}"
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, message: "Server error generating response: #{e.message}")
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONRPC_Rails
2
- VERSION = "0.2.0"
4
+ VERSION = "0.4.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.2.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