jsonrpc-middleware 0.5.0 → 0.7.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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.aiignore +6 -1
  3. data/.claude/agents/entire-search.md +25 -0
  4. data/.claude/agents/rbs-specialist.md +89 -0
  5. data/.claude/settings.json +84 -0
  6. data/.devcontainer/devcontainer.json +17 -0
  7. data/.dockerignore +16 -0
  8. data/.entire/.gitignore +5 -0
  9. data/.entire/settings.json +4 -0
  10. data/.rubocop.yml +26 -1
  11. data/.tool-versions +1 -1
  12. data/.yard-lint.yml +283 -0
  13. data/AGENTS.md +142 -0
  14. data/CHANGELOG.md +43 -0
  15. data/CLAUDE.md +2 -113
  16. data/Dockerfile +144 -0
  17. data/README.md +10 -17
  18. data/Rakefile +78 -26
  19. data/examples/README.md +9 -0
  20. data/examples/procedures.rb +3 -1
  21. data/examples/rack/Gemfile.lock +11 -2
  22. data/examples/rack-echo/Gemfile.lock +11 -2
  23. data/examples/rails/Gemfile.lock +18 -23
  24. data/examples/rails/config/initializers/jsonrpc.rb +1 -1
  25. data/examples/rails-routing-dsl/config.ru +5 -5
  26. data/examples/rails-single-file/config.ru +1 -1
  27. data/examples/rails-single-file-routing/README.md +38 -3
  28. data/examples/rails-single-file-routing/config.ru +18 -1
  29. data/examples/sinatra-classic/Gemfile.lock +11 -3
  30. data/examples/sinatra-modular/Gemfile.lock +11 -3
  31. data/lib/jsonrpc/batch_request.rb +9 -12
  32. data/lib/jsonrpc/batch_response.rb +7 -9
  33. data/lib/jsonrpc/configuration.rb +43 -4
  34. data/lib/jsonrpc/error.rb +8 -9
  35. data/lib/jsonrpc/errors/internal_error.rb +2 -0
  36. data/lib/jsonrpc/errors/invalid_params_error.rb +2 -0
  37. data/lib/jsonrpc/errors/invalid_request_error.rb +2 -0
  38. data/lib/jsonrpc/errors/method_not_found_error.rb +2 -0
  39. data/lib/jsonrpc/errors/parse_error.rb +2 -0
  40. data/lib/jsonrpc/helpers.rb +6 -0
  41. data/lib/jsonrpc/middleware.rb +15 -13
  42. data/lib/jsonrpc/notification.rb +8 -9
  43. data/lib/jsonrpc/parser.rb +22 -19
  44. data/lib/jsonrpc/railtie/batch_constraint.rb +1 -0
  45. data/lib/jsonrpc/railtie/mapper_extension.rb +2 -2
  46. data/lib/jsonrpc/railtie/method_constraint.rb +9 -0
  47. data/lib/jsonrpc/railtie/routes_dsl.rb +10 -15
  48. data/lib/jsonrpc/railtie.rb +4 -2
  49. data/lib/jsonrpc/request.rb +12 -84
  50. data/lib/jsonrpc/response.rb +11 -60
  51. data/lib/jsonrpc/types.rb +13 -0
  52. data/lib/jsonrpc/validator.rb +14 -4
  53. data/lib/jsonrpc/version.rb +1 -1
  54. data/lib/jsonrpc.rb +5 -0
  55. data/rbs_collection.lock.yaml +476 -0
  56. data/rbs_collection.yaml +21 -0
  57. data/sig/jsonrpc/batch_request.rbs +17 -0
  58. data/sig/jsonrpc/batch_response.rbs +17 -0
  59. data/sig/jsonrpc/configuration.rbs +18 -0
  60. data/sig/jsonrpc/error.rbs +17 -0
  61. data/sig/jsonrpc/errors/internal_error.rbs +5 -0
  62. data/sig/jsonrpc/errors/invalid_params_error.rbs +5 -0
  63. data/sig/jsonrpc/errors/invalid_request_error.rbs +5 -0
  64. data/sig/jsonrpc/errors/method_not_found_error.rbs +5 -0
  65. data/sig/jsonrpc/errors/parse_error.rbs +5 -0
  66. data/sig/jsonrpc/middleware.rbs +20 -3
  67. data/sig/jsonrpc/notification.rbs +15 -0
  68. data/sig/jsonrpc/parser.rbs +7 -1
  69. data/sig/jsonrpc/request.rbs +18 -0
  70. data/sig/jsonrpc/response.rbs +19 -0
  71. data/sig/jsonrpc/validator.rbs +8 -0
  72. data/sig/jsonrpc.rbs +3 -156
  73. data/sig/multi_json.rbs +17 -0
  74. data/sig/type_definitions.rbs +11 -0
  75. data/sig/zeitwerk.rbs +10 -0
  76. metadata +61 -9
  77. data/.claude/commands/document.md +0 -105
  78. data/.claude/commands/test.md +0 -561
  79. data/.claude/docs/yard.md +0 -602
  80. data/.claude/settings.local.json +0 -11
  81. data/.yardstick.yml +0 -22
@@ -57,9 +57,6 @@ module JSONRPC
57
57
 
58
58
  # Define a JSON-RPC method route
59
59
  #
60
- # @param jsonrpc_method [String] the JSON-RPC method name
61
- # @param to [String] the Rails controller action (e.g., 'users#create')
62
- #
63
60
  # @example Map a JSON-RPC method to controller action
64
61
  # method 'user.create', to: 'users#create'
65
62
  #
@@ -68,33 +65,30 @@ module JSONRPC
68
65
  # method 'create', to: 'posts#create' # becomes posts.create
69
66
  # end
70
67
  #
68
+ # @param jsonrpc_method [String] the JSON-RPC method name
69
+ # @param to [String] the Rails controller action (e.g., 'users#create')
70
+ #
71
71
  # @return [void]
72
72
  #
73
73
  def method(jsonrpc_method, to:)
74
74
  full_method_name = build_full_method_name(jsonrpc_method)
75
75
  constraint = JSONRPC::MethodConstraint.new(full_method_name)
76
76
 
77
- @mapper.post @path_prefix, {
78
- to: to,
79
- constraints: constraint
80
- }
77
+ @mapper.post(@path_prefix, to: to, constraints: constraint)
81
78
  end
82
79
 
83
80
  # Define a route for handling JSON-RPC batch requests
84
81
  #
85
- # @param to [String] the Rails controller action (e.g., 'batches#handle')
86
- #
87
82
  # @example Map batch requests to a controller action
88
83
  # batch to: 'batches#handle'
89
84
  #
85
+ # @param to [String] the Rails controller action (e.g., 'batches#handle')
86
+ #
90
87
  # @return [void]
91
88
  def batch(to:)
92
89
  constraint = JSONRPC::BatchConstraint.new
93
90
 
94
- @mapper.post @path_prefix, {
95
- to: to,
96
- constraints: constraint
97
- }
91
+ @mapper.post(@path_prefix, to: to, constraints: constraint)
98
92
  end
99
93
 
100
94
  # Create a namespace for grouping related JSON-RPC methods
@@ -102,8 +96,6 @@ module JSONRPC
102
96
  # Namespaces can be nested to create hierarchical method names.
103
97
  # Each level of nesting adds a dot-separated prefix to the method names.
104
98
  #
105
- # @param name [String] the namespace name
106
- #
107
99
  # @example Single-level namespace
108
100
  # namespace 'posts' do
109
101
  # method 'create', to: 'posts#create' # becomes posts.create
@@ -121,6 +113,8 @@ module JSONRPC
121
113
  # end
122
114
  # end
123
115
  #
116
+ # @param name [String] the namespace name
117
+ #
124
118
  # @return [void]
125
119
  #
126
120
  def namespace(name, &)
@@ -134,6 +128,7 @@ module JSONRPC
134
128
  # Build the full method name including namespaces
135
129
  #
136
130
  # @param method_name [String] the base method name
131
+ #
137
132
  # @return [String] the full method name with namespace prefixes
138
133
  #
139
134
  def build_full_method_name(method_name)
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JSONRPC
4
+ # Integrates JSONRPC middleware, routing DSL, and helpers into Rails applications.
5
+ #
4
6
  # @api private
5
7
  class Railtie < ::Rails::Railtie
6
8
  # Rails routing constraint for matching JSON-RPC method names
@@ -35,7 +37,7 @@ module JSONRPC
35
37
  self.response_body = ''
36
38
  ''
37
39
  else
38
- result_data.to_json
40
+ MultiJson.dump(result_data)
39
41
  end
40
42
  elsif jsonrpc_notification?
41
43
  # Notification - no response body
@@ -44,7 +46,7 @@ module JSONRPC
44
46
  ''
45
47
  else
46
48
  # Fallback - treat as regular JSON-RPC response
47
- result_data.to_json
49
+ MultiJson.dump(result_data)
48
50
  end
49
51
  end
50
52
  end
@@ -13,7 +13,7 @@ module JSONRPC
13
13
  # @example Create a request with named parameters
14
14
  # request = JSONRPC::Request.new(method: "subtract", params: { minuend: 42, subtrahend: 23 }, id: 3)
15
15
  #
16
- class Request
16
+ class Request < Dry::Struct
17
17
  # JSON-RPC protocol version
18
18
  #
19
19
  # @api public
@@ -23,7 +23,7 @@ module JSONRPC
23
23
  #
24
24
  # @return [String]
25
25
  #
26
- attr_reader :jsonrpc
26
+ attribute :jsonrpc, Types::String.default('2.0')
27
27
 
28
28
  # The method name to invoke
29
29
  #
@@ -34,7 +34,7 @@ module JSONRPC
34
34
  #
35
35
  # @return [String]
36
36
  #
37
- attr_reader :method
37
+ attribute :method, Types::String.constrained(format: /\A(?!rpc\.)/)
38
38
 
39
39
  # Parameters to pass to the method
40
40
  #
@@ -45,7 +45,7 @@ module JSONRPC
45
45
  #
46
46
  # @return [Hash, Array, nil]
47
47
  #
48
- attr_reader :params
48
+ attribute? :params, (Types::Hash | Types::Array).optional
49
49
 
50
50
  # The request identifier
51
51
  #
@@ -56,36 +56,7 @@ module JSONRPC
56
56
  #
57
57
  # @return [String, Integer, nil]
58
58
  #
59
- attr_reader :id
60
-
61
- # Creates a new JSON-RPC 2.0 Request object
62
- #
63
- # @api public
64
- #
65
- # @example Create a request with positional parameters
66
- # JSONRPC::Request.new(method: "subtract", params: [42, 23], id: 1)
67
- #
68
- # @param method [String] the name of the method to be invoked
69
- # @param params [Hash, Array, nil] the parameters to be used during method invocation
70
- # @param id [String, Integer, nil] the request identifier
71
- #
72
- # @raise [ArgumentError] if method is not a String or is reserved
73
- #
74
- # @raise [ArgumentError] if params is not a Hash, Array, or nil
75
- #
76
- # @raise [ArgumentError] if id is not a String, Integer, or nil
77
- #
78
- def initialize(method:, id:, params: nil)
79
- @jsonrpc = '2.0'
80
-
81
- validate_method(method)
82
- validate_params(params)
83
- validate_id(id)
84
-
85
- @method = method
86
- @params = params
87
- @id = id
88
- end
59
+ attribute? :id, Types::String | Types::Integer | Types::Nil
89
60
 
90
61
  # Converts the request to a JSON-compatible Hash
91
62
  #
@@ -119,61 +90,18 @@ module JSONRPC
119
90
  # @return [String] the request as a JSON string
120
91
  #
121
92
  def to_json(*)
122
- to_h.to_json(*)
93
+ MultiJson.dump(to_h, *)
123
94
  end
124
95
 
125
- private
126
-
127
- # Validates that the method name meets JSON-RPC 2.0 requirements
128
- #
129
- # @api private
130
- #
131
- # @param method [String] the method name
132
- #
133
- # @raise [ArgumentError] if method is not a String or is reserved
134
- #
135
- # @return [void]
136
- #
137
- def validate_method(method)
138
- raise ArgumentError, 'Method must be a String' unless method.is_a?(String)
139
-
140
- return unless method.start_with?('rpc.')
141
-
142
- raise ArgumentError, "Method names starting with 'rpc.' are reserved"
143
- end
144
-
145
- # Validates that the params is a valid structure according to JSON-RPC 2.0
146
- #
147
- # @api private
148
- #
149
- # @param params [Hash, Array, nil] the parameters
150
- #
151
- # @raise [ArgumentError] if params is not a Hash, Array, or nil
152
- #
153
- # @return [void]
154
- #
155
- def validate_params(params)
156
- return if params.nil?
157
-
158
- return if params.is_a?(Hash) || params.is_a?(Array)
159
-
160
- raise ArgumentError, 'Params must be an Object, Array, or omitted'
161
- end
162
-
163
- # Validates that the id meets JSON-RPC 2.0 requirements
164
- #
165
- # @api private
96
+ # The method name to invoke
166
97
  #
167
- # @param id [String, Integer, nil] the request identifier
98
+ # @api public
168
99
  #
169
- # @raise [ArgumentError] if id is not a String, Integer, or nil
100
+ # @example
101
+ # request.method # => "subtract"
170
102
  #
171
- # @return [void]
103
+ # @return [String]
172
104
  #
173
- def validate_id(id)
174
- return if id.nil?
175
-
176
- raise ArgumentError, 'ID must be a String, Integer, or nil' unless id.is_a?(String) || id.is_a?(Integer)
177
- end
105
+ def method = attributes[:method]
178
106
  end
179
107
  end
@@ -19,7 +19,9 @@ module JSONRPC
19
19
  # error = JSONRPC::Error.new(code: -32601, message: "Method not found")
20
20
  # response = JSONRPC::Response.new(error: error, id: 1)
21
21
  #
22
- class Response
22
+ class Response < Dry::Struct
23
+ transform_keys(&:to_sym)
24
+
23
25
  # JSON-RPC protocol version
24
26
  #
25
27
  # @api public
@@ -29,7 +31,7 @@ module JSONRPC
29
31
  #
30
32
  # @return [String]
31
33
  #
32
- attr_reader :jsonrpc
34
+ attribute :jsonrpc, Types::String.default('2.0')
33
35
 
34
36
  # The result of the method invocation (for success)
35
37
  #
@@ -40,7 +42,7 @@ module JSONRPC
40
42
  #
41
43
  # @return [Object, nil]
42
44
  #
43
- attr_reader :result
45
+ attribute? :result, Types::Any
44
46
 
45
47
  # The error object (for failure)
46
48
  #
@@ -51,7 +53,7 @@ module JSONRPC
51
53
  #
52
54
  # @return [JSONRPC::Error, nil]
53
55
  #
54
- attr_reader :error
56
+ attribute? :error, Types.Instance(JSONRPC::Error).optional
55
57
 
56
58
  # The request identifier
57
59
  #
@@ -62,7 +64,7 @@ module JSONRPC
62
64
  #
63
65
  # @return [String, Integer, nil]
64
66
  #
65
- attr_reader :id
67
+ attribute? :id, Types::String | Types::Integer | Types::Nil
66
68
 
67
69
  # Creates a new JSON-RPC 2.0 Response object
68
70
  #
@@ -80,21 +82,9 @@ module JSONRPC
80
82
  # @param id [String, Integer, nil] the request identifier
81
83
  #
82
84
  # @raise [ArgumentError] if both result and error are present or both are nil
83
- #
84
85
  # @raise [ArgumentError] if error is present but not a JSONRPC::Error
85
- #
86
86
  # @raise [ArgumentError] if id is not a String, Integer, or nil
87
87
  #
88
- def initialize(id:, result: nil, error: nil)
89
- @jsonrpc = '2.0'
90
-
91
- validate_result_and_error(result, error)
92
- validate_id(id)
93
-
94
- @result = result
95
- @error = error
96
- @id = id
97
- end
98
88
 
99
89
  # Checks if the response is successful
100
90
  #
@@ -106,7 +96,7 @@ module JSONRPC
106
96
  # @return [Boolean] true if the response contains a result, false if it contains an error
107
97
  #
108
98
  def success?
109
- !@result.nil?
99
+ !result.nil?
110
100
  end
111
101
 
112
102
  # Checks if the response is an error
@@ -119,7 +109,7 @@ module JSONRPC
119
109
  # @return [Boolean] true if the response contains an error, false if it contains a result
120
110
  #
121
111
  def error?
122
- !@error.nil?
112
+ !error.nil?
123
113
  end
124
114
 
125
115
  # Converts the response to a JSON-compatible Hash
@@ -156,48 +146,9 @@ module JSONRPC
156
146
  # @return [String] the response as a JSON string
157
147
  #
158
148
  def to_json(*)
159
- to_h.to_json(*)
149
+ MultiJson.dump(to_h, *)
160
150
  end
161
151
 
162
- private
163
-
164
- # Validates that exactly one of result or error is present
165
- #
166
- # @api private
167
- #
168
- # @param result [Object, nil] the result
169
- # @param error [JSONRPC::Error, nil] the error
170
- #
171
- # @raise [ArgumentError] if both result and error are present or both are nil
172
- #
173
- # @raise [ArgumentError] if error is present but not a JSONRPC::Error
174
- #
175
- # @return [void]
176
- #
177
- def validate_result_and_error(result, error)
178
- raise ArgumentError, 'Either result or error must be present' if result.nil? && error.nil?
179
-
180
- raise ArgumentError, 'Response cannot contain both result and error' if !result.nil? && !error.nil?
181
-
182
- return unless !error.nil? && !error.is_a?(Error)
183
-
184
- raise ArgumentError, 'Error must be a JSONRPC::Error'
185
- end
186
-
187
- # Validates that the id meets JSON-RPC 2.0 requirements
188
- #
189
- # @api private
190
- #
191
- # @param id [String, Integer, nil] the request identifier
192
- #
193
- # @raise [ArgumentError] if id is not a String, Integer, or nil
194
- #
195
- # @return [void]
196
- #
197
- def validate_id(id)
198
- return if id.nil?
199
-
200
- raise ArgumentError, 'ID must be a String, Integer, or nil' unless id.is_a?(String) || id.is_a?(Integer)
201
- end
152
+ alias to_response to_h
202
153
  end
203
154
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONRPC
4
+ # Container for dry-types
5
+ #
6
+ # @api private
7
+ #
8
+ # @see https://dry-rb.org/gems/dry-types/ Dry::Types documentation
9
+ #
10
+ module Types
11
+ send(:include, Dry.Types()) # Uses send to fix a YARD documentation bug
12
+ end
13
+ end
@@ -13,6 +13,16 @@ module JSONRPC
13
13
  # error = validator.validate(request)
14
14
  #
15
15
  class Validator
16
+ # Initializes the Validator
17
+ #
18
+ # @api public
19
+ #
20
+ # @param logger [Logger] the logger instance for diagnostic output
21
+ #
22
+ def initialize(logger: JSONRPC.configuration.logger)
23
+ @logger = logger
24
+ end
25
+
16
26
  # Validates a single request, notification or a batch
17
27
  #
18
28
  # @api public
@@ -92,7 +102,7 @@ module JSONRPC
92
102
  request_id: extract_request_id(request_or_notification),
93
103
  data: {
94
104
  method: request_or_notification.method,
95
- params: validation_result.errors.to_h
105
+ params: validation_result.errors.to_h # rubocop:disable Rails/DeprecatedActiveModelErrorsMethods
96
106
  }
97
107
  )
98
108
  end
@@ -100,8 +110,8 @@ module JSONRPC
100
110
  nil
101
111
  rescue StandardError => e
102
112
  if JSONRPC.configuration.log_request_validation_errors
103
- puts "Validation error: #{e.message}"
104
- puts e.backtrace.join("\n")
113
+ @logger.error("Validation error: #{e.message}")
114
+ @logger.error(e.backtrace.join("\n"))
105
115
  end
106
116
 
107
117
  InternalError.new(request_id: extract_request_id(request_or_notification))
@@ -111,7 +121,7 @@ module JSONRPC
111
121
  #
112
122
  # @api private
113
123
  #
114
- # @param request [Request, Notification] the request
124
+ # @param request [Request, Notification] A request or notification to be validated
115
125
  # @param procedure [Configuration::Procedure] the procedure configuration
116
126
  #
117
127
  # @return [Hash, InvalidParamsError] prepared params or error
@@ -11,5 +11,5 @@ module JSONRPC
11
11
  #
12
12
  # @return [String] The current version number
13
13
  #
14
- VERSION = '0.5.0'
14
+ VERSION = '0.7.0'
15
15
  end
data/lib/jsonrpc.rb CHANGED
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'logger'
3
4
  require 'zeitwerk'
5
+ require 'dry-struct'
4
6
  require 'dry-validation'
7
+ require 'multi_json'
5
8
 
6
9
  Dry::Validation.load_extensions(:predicates_as_macros)
7
10
 
@@ -70,10 +73,12 @@ loader.enable_reloading
70
73
  loader.collapse("#{__dir__}/jsonrpc/errors")
71
74
  loader.collapse("#{__dir__}/jsonrpc/railtie")
72
75
 
76
+ # :nocov:
73
77
  unless defined?(Rails)
74
78
  loader.ignore("#{__dir__}/jsonrpc/railtie.rb")
75
79
  loader.ignore("#{__dir__}/jsonrpc/railtie/method_constraint.rb")
76
80
  end
81
+ # :nocov:
77
82
 
78
83
  loader.inflector.inflect('jsonrpc' => 'JSONRPC')
79
84
  loader.setup