jsonrpc-middleware 0.5.0 → 0.6.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: 5d8775dfa68b34e4513883b9bdbfb565a708902efe694727bb19d517d4539cd7
4
- data.tar.gz: 8f087e14952785c53470895871bb175e0e48fd9aab560b9906cd17b509d6f4e2
3
+ metadata.gz: bf6393e7fe408f08dc933ebb6fc73a2541829eab966df8acedd7103a2e4e2c01
4
+ data.tar.gz: f1f17e444ea3a84cfd69e46316e595eaa00fc0a01e9566e313111dc0e47142b8
5
5
  SHA512:
6
- metadata.gz: fd7b7f8ffb647ad4e08a487307bfa58f7a2f33fbc0d5e0f248800f7ac17bc1c01cfb0c7c5d3d6804601ac7b48d5a248084ea1a39bf3c1f38fc84628776e9c7b2
7
- data.tar.gz: cad4b6b454c561cc04ef2c4452722f562a9902a1c45020e709ffd793d3a2fb0fb1d1c4556a0f3867c6e30339a6fc24c58c0b38762b9aa3940eedd9c488324b09
6
+ metadata.gz: 103c23a06d7d49ad68ed3d7249f24cf7a7c52cf82d72a9a0af68b1da4d2ec1869e8e80ef0207be9d4aaf5e85caf7433094f3f1e4dc52e812b4a5cf6cf074f735
7
+ data.tar.gz: eb0dcf3da91d3176de5507c1a9c604ffefdc59bfbd4476a0355d74fc85f301674b89f8b2a743b9eca4168dc3b410a43de3a056079674d64e7651fc7c2c69ada1
@@ -0,0 +1,52 @@
1
+ ---
2
+ allowed-tools: Bash(bundle :*), Bash(git :*), Read, Edit, MultiEdit, Glob
3
+ description: Update Gemfile dependencies to latest minor versions
4
+ argument-hint: [gemfile] [commit]
5
+ ---
6
+
7
+ Update the dependencies in the specified Gemfile (or ./Gemfile if no path provided) to their latest minor versions while
8
+ preserving major version constraints. Only update MAJOR.MINOR versions, never PATCH versions unless explicitly needed.
9
+
10
+ Steps:
11
+ 1. Read the Gemfile at the specified path (or ./Gemfile if $ARGUMENTS is empty)
12
+ 2. Read the corresponding Gemfile.lock to get current resolved versions
13
+ 3. Run `bundle outdated --only-explicit` to check for available minor updates of explicitly declared gems
14
+ 4. For each gem in Gemfile, check if Gemfile.lock has a newer minor version than the current Gemfile constraint allows
15
+ 5. Update gem version constraints to match the minor version from Gemfile.lock or latest available, whichever is newer (MAJOR.MINOR format)
16
+ 6. Use pessimistic version constraints (~> MAJOR.MINOR) to prevent automatic patch updates
17
+ 7. Preserve any existing version operators but ensure they follow minor-only update strategy
18
+ 8. Run `bundle update` to apply the changes
19
+ 7. Skip step 8, 9 and 10 if --commit flag is not provided
20
+ 8. Stage Gemfile (only if not gitignored)
21
+ 9. Verify if Gemfile.lock is tracked and not gitignored. If both conditions are met, stage it for commit.
22
+ 10. Create a git commit with message 'Update development dependencies' and a description listing all updated gems with their old and new versions like:
23
+
24
+ <commit-message>
25
+ Updated gems:
26
+ - rubocop: 1.75.2 → 1.78.0
27
+ - rubocop-yard: 0.10.0 → 1.0.0
28
+ </commit-message>
29
+
30
+ 11. If any dependencies were updated, respond only with the update message. And if the user has chose to commit,
31
+ include the update commit message. Otherwise, respond only with the no op message.
32
+
33
+ <update-message>
34
+ Updated gems:
35
+ - rbs: 3.8 → 3.9
36
+ - rubocop: 1.78 → 1.80
37
+ - rubocop-rspec: 3.6 → 3.7
38
+
39
+ <update-commit-message>The changes have been committed with the message "Update development dependencies".</update-commit-message>
40
+ </update-message>
41
+
42
+ <no-op-message>All dependencies are up to date.</no-op-message>
43
+
44
+ Key bundle outdated flags used:
45
+ - `--only-explicit`: Only show gems explicitly listed in Gemfile (not dependencies)
46
+ - No `--local` flag to ensure remote gem sources are checked for latest versions
47
+
48
+ Arguments:
49
+ - `gemfile`: Gemfile path (defaults to ./Gemfile if not provided)
50
+ - `--commit`: Create a git commit after updating dependencies with message 'Update development dependencies' and a description listing all updated gems with their old and new versions
51
+
52
+ Gemfile path: ${ARGUMENTS:-./Gemfile}
@@ -4,8 +4,12 @@
4
4
  "Bash(bundle exec rspec:*)",
5
5
  "Bash(bundle exec rubocop:*)",
6
6
  "Bash(bundle exec rake:*)",
7
- "Bash(mkdir:*)"
7
+ "Bash(mkdir:*)",
8
+ "Bash(bin/console:*)",
9
+ "Bash(bundle exec ruby:*)",
10
+ "Bash(git:*)",
11
+ "Bash(grep:*)"
8
12
  ]
9
13
  },
10
14
  "enableAllProjectMcpServers": false
11
- }
15
+ }
data/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.6.0] - 2025-09-17
9
+
10
+ ### Added
11
+ - MultiJson support for improved JSON performance and flexibility
12
+ - Configuration option to select JSON adapter (`JSONRPC.configuration.json_adapter`)
13
+ - Enhanced error handling with adapter and input preview details
14
+ - Better performance through optimized JSON parsing
15
+ - Enhanced examples
16
+ - Batch JSON-RPC request handling in Rails single-file routing example
17
+ - Smart home control API example using Rails routing DSL
18
+ - Updated documentation with batch request usage examples
19
+
20
+ ### Changed
21
+ - Replaced JSON gem with MultiJson throughout the codebase
22
+ - All JSON parsing now uses MultiJson for better adapter support
23
+ - Updated documentation to mention optimized JSON handling
24
+ - Refactored Request and Response classes to use dry-struct
25
+ - Simplified class definitions with automatic type checking
26
+ - Reduced code complexity while maintaining functionality
27
+ - Enhanced Rails integration
28
+ - Improved example applications with better error handling
29
+ - Updated Gemfile.lock files across all examples
30
+
8
31
  ## [0.5.0] - 2025-07-22
9
32
 
10
33
  ### Added
@@ -135,6 +158,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
135
158
  - Helper methods for request and response processing
136
159
  - Examples for basic and advanced usage scenarios
137
160
 
161
+ [0.6.0]: https://github.com/wilsonsilva/jsonrpc-middleware/compare/v0.5.0...v0.6.0
138
162
  [0.5.0]: https://github.com/wilsonsilva/jsonrpc-middleware/compare/v0.4.0...v0.5.0
139
163
  [0.4.0]: https://github.com/wilsonsilva/jsonrpc-middleware/compare/v0.3.0...v0.4.0
140
164
  [0.3.0]: https://github.com/wilsonsilva/jsonrpc-middleware/compare/v0.2.0...v0.3.0
data/README.md CHANGED
@@ -41,6 +41,7 @@ A Rack middleware implementing the JSON-RPC 2.0 protocol that integrates easily
41
41
  - **Error handling**: Comprehensive error handling with standard JSON-RPC error responses
42
42
  - **Request validation**: Define request parameter specifications and validations
43
43
  - **Helpers**: Convenient helper methods to simplify request and response processing
44
+ - **Optimized JSON handling**: Uses MultiJSON to automatically select the fastest available JSON library
44
45
 
45
46
  ## 🏗️ Architecture
46
47
 
data/Rakefile CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'English' # For $CHILD_STATUS
3
4
  require 'bundler/audit/task'
4
5
  require 'bundler/gem_tasks'
5
6
  require 'rspec/core/rake_task'
@@ -100,53 +101,53 @@ namespace :examples do
100
101
  desc 'Run bundle install on all example folders'
101
102
  task :bundle_install do
102
103
  examples_dir = File.join(Dir.pwd, 'examples')
103
-
104
+
104
105
  unless Dir.exist?(examples_dir)
105
106
  puts 'Examples directory not found'
106
107
  exit 1
107
108
  end
108
-
109
+
109
110
  example_folders = Dir.glob(File.join(examples_dir, '*')).select { |path| Dir.exist?(path) }
110
-
111
+
111
112
  if example_folders.empty?
112
113
  puts 'No example folders found'
113
114
  return
114
115
  end
115
-
116
+
116
117
  puts "Found #{example_folders.length} example folders:"
117
118
  example_folders.each { |folder| puts " - #{File.basename(folder)}" }
118
119
  puts
119
-
120
+
120
121
  failed_folders = []
121
-
122
+
122
123
  example_folders.each do |folder|
123
124
  gemfile_path = File.join(folder, 'Gemfile')
124
-
125
+
125
126
  unless File.exist?(gemfile_path)
126
127
  puts "Skipping #{File.basename(folder)} - no Gemfile found"
127
128
  next
128
129
  end
129
-
130
+
130
131
  puts "Running bundle install in #{File.basename(folder)}..."
131
-
132
+
132
133
  Dir.chdir(folder) do
133
134
  system('bundle install')
134
-
135
- unless $?.success?
135
+
136
+ if $CHILD_STATUS.success?
137
+ puts " ✓ Successfully installed gems in #{File.basename(folder)}"
138
+ else
136
139
  failed_folders << File.basename(folder)
137
140
  puts " ✗ Failed to bundle install in #{File.basename(folder)}"
138
- else
139
- puts " ✓ Successfully installed gems in #{File.basename(folder)}"
140
141
  end
141
142
  end
142
-
143
+
143
144
  puts
144
145
  end
145
-
146
+
146
147
  if failed_folders.empty?
147
148
  puts 'All example folders processed successfully!'
148
149
  else
149
- puts "Failed to process #{failed_folders.length} folders: #{failed_folders.join(', ')}"
150
+ puts "Failed to process #{failed_folders.length} folders: #{failed_folders.join(", ")}"
150
151
  exit 1
151
152
  end
152
153
  end
data/examples/README.md CHANGED
@@ -10,6 +10,7 @@ This directory contains example implementations of JSON-RPC servers using the js
10
10
  - [**rails**](./rails/) - Calculator server using Rails
11
11
  - [**rails-single-file**](./rails-single-file/) - Echo server using Rails with bundler/inline
12
12
  - [**rails-single-file-routing**](./rails-single-file-routing/) - Echo server using Rails with method-specific routing
13
+ - [**rails-routing-dsl**](./rails-routing-dsl/) - Smart home control server showcasing Rails JSON-RPC routing DSL
13
14
  - [**sinatra-classic**](./sinatra-classic/) - Calculator server using classic Sinatra
14
15
  - [**sinatra-modular**](./sinatra-modular/) - Calculator server using modular Sinatra
15
16
 
@@ -27,6 +28,14 @@ The echo examples implement:
27
28
 
28
29
  - `echo` - Returns the input message
29
30
 
31
+ The rails-routing-dsl example implements a smart home control API:
32
+
33
+ - `on` / `off` - Control main home automation system
34
+ - `lights.on` / `lights.off` - Control lights
35
+ - `climate.on` / `climate.off` - Control climate system
36
+ - `climate.fan.on` / `climate.fan.off` - Control climate fan
37
+ - Batch request support for multiple operations
38
+
30
39
  ## Running Examples
31
40
 
32
41
  Each example directory contains its own README with specific instructions. Generally:
@@ -1,8 +1,10 @@
1
1
  PATH
2
2
  remote: ../..
3
3
  specs:
4
- jsonrpc-middleware (0.4.0)
4
+ jsonrpc-middleware (0.5.0)
5
+ dry-struct (~> 1.8)
5
6
  dry-validation (~> 1.11)
7
+ multi_json (~> 1.17)
6
8
  zeitwerk (~> 2.7)
7
9
 
8
10
  GEM
@@ -32,6 +34,11 @@ GEM
32
34
  dry-logic (~> 1.5)
33
35
  dry-types (~> 1.8)
34
36
  zeitwerk (~> 2.6)
37
+ dry-struct (1.8.0)
38
+ dry-core (~> 1.1)
39
+ dry-types (~> 1.8, >= 1.8.2)
40
+ ice_nine (~> 0.11)
41
+ zeitwerk (~> 2.6)
35
42
  dry-types (1.8.3)
36
43
  bigdecimal (~> 3.0)
37
44
  concurrent-ruby (~> 1.0)
@@ -45,7 +52,9 @@ GEM
45
52
  dry-initializer (~> 3.2)
46
53
  dry-schema (~> 1.14)
47
54
  zeitwerk (~> 2.6)
55
+ ice_nine (0.11.2)
48
56
  logger (1.7.0)
57
+ multi_json (1.17.0)
49
58
  nio4r (2.7.4)
50
59
  puma (6.6.0)
51
60
  nio4r (~> 2.0)
@@ -1,8 +1,10 @@
1
1
  PATH
2
2
  remote: ../..
3
3
  specs:
4
- jsonrpc-middleware (0.4.0)
4
+ jsonrpc-middleware (0.5.0)
5
+ dry-struct (~> 1.8)
5
6
  dry-validation (~> 1.11)
7
+ multi_json (~> 1.17)
6
8
  zeitwerk (~> 2.7)
7
9
 
8
10
  GEM
@@ -32,6 +34,11 @@ GEM
32
34
  dry-logic (~> 1.5)
33
35
  dry-types (~> 1.8)
34
36
  zeitwerk (~> 2.6)
37
+ dry-struct (1.8.0)
38
+ dry-core (~> 1.1)
39
+ dry-types (~> 1.8, >= 1.8.2)
40
+ ice_nine (~> 0.11)
41
+ zeitwerk (~> 2.6)
35
42
  dry-types (1.8.3)
36
43
  bigdecimal (~> 3.0)
37
44
  concurrent-ruby (~> 1.0)
@@ -45,7 +52,9 @@ GEM
45
52
  dry-initializer (~> 3.2)
46
53
  dry-schema (~> 1.14)
47
54
  zeitwerk (~> 2.6)
55
+ ice_nine (0.11.2)
48
56
  logger (1.7.0)
57
+ multi_json (1.17.0)
49
58
  nio4r (2.7.4)
50
59
  puma (6.6.0)
51
60
  nio4r (~> 2.0)
@@ -1,8 +1,10 @@
1
1
  PATH
2
2
  remote: ../..
3
3
  specs:
4
- jsonrpc-middleware (0.4.0)
4
+ jsonrpc-middleware (0.5.0)
5
+ dry-struct (~> 1.8)
5
6
  dry-validation (~> 1.11)
7
+ multi_json (~> 1.17)
6
8
  zeitwerk (~> 2.7)
7
9
 
8
10
  GEM
@@ -113,6 +115,11 @@ GEM
113
115
  dry-logic (~> 1.5)
114
116
  dry-types (~> 1.8)
115
117
  zeitwerk (~> 2.6)
118
+ dry-struct (1.8.0)
119
+ dry-core (~> 1.1)
120
+ dry-types (~> 1.8, >= 1.8.2)
121
+ ice_nine (~> 0.11)
122
+ zeitwerk (~> 2.6)
116
123
  dry-types (1.8.3)
117
124
  bigdecimal (~> 3.0)
118
125
  concurrent-ruby (~> 1.0)
@@ -132,6 +139,7 @@ GEM
132
139
  activesupport (>= 6.1)
133
140
  i18n (1.14.7)
134
141
  concurrent-ruby (~> 1.0)
142
+ ice_nine (0.11.2)
135
143
  io-console (0.8.1)
136
144
  irb (1.15.2)
137
145
  pp (>= 0.6.0)
@@ -149,6 +157,7 @@ GEM
149
157
  marcel (1.0.4)
150
158
  mini_mime (1.1.5)
151
159
  minitest (5.25.5)
160
+ multi_json (1.17.0)
152
161
  net-imap (0.5.9)
153
162
  date
154
163
  net-protocol
@@ -159,22 +168,8 @@ GEM
159
168
  net-smtp (0.5.1)
160
169
  net-protocol
161
170
  nio4r (2.7.4)
162
- nokogiri (1.18.8-aarch64-linux-gnu)
163
- racc (~> 1.4)
164
- nokogiri (1.18.8-aarch64-linux-musl)
165
- racc (~> 1.4)
166
- nokogiri (1.18.8-arm-linux-gnu)
167
- racc (~> 1.4)
168
- nokogiri (1.18.8-arm-linux-musl)
169
- racc (~> 1.4)
170
171
  nokogiri (1.18.8-arm64-darwin)
171
172
  racc (~> 1.4)
172
- nokogiri (1.18.8-x86_64-darwin)
173
- racc (~> 1.4)
174
- nokogiri (1.18.8-x86_64-linux-gnu)
175
- racc (~> 1.4)
176
- nokogiri (1.18.8-x86_64-linux-musl)
177
- racc (~> 1.4)
178
173
  pp (0.6.2)
179
174
  prettyprint
180
175
  prettyprint (0.2.0)
@@ -242,14 +237,7 @@ GEM
242
237
  zeitwerk (2.7.3)
243
238
 
244
239
  PLATFORMS
245
- aarch64-linux-gnu
246
- aarch64-linux-musl
247
- arm-linux-gnu
248
- arm-linux-musl
249
240
  arm64-darwin
250
- x86_64-darwin
251
- x86_64-linux-gnu
252
- x86_64-linux-musl
253
241
 
254
242
  DEPENDENCIES
255
243
  debug
@@ -10,6 +10,10 @@ Uses constraints to route JSON-RPC requests to different Rails controller action
10
10
  class App < Rails::Application
11
11
  # ...
12
12
  routes.append do
13
+ # Handle batch requests
14
+ post '/', to: 'jsonrpc#ping_or_echo', constraints: JSONRPC::BatchConstraint.new
15
+
16
+ # Handle individual method requests
13
17
  post '/', to: 'jsonrpc#echo', constraints: JSONRPC::MethodConstraint.new('echo')
14
18
  post '/', to: 'jsonrpc#ping', constraints: JSONRPC::MethodConstraint.new('ping')
15
19
  end
@@ -25,6 +29,20 @@ class JsonrpcController < ActionController::Base
25
29
  def ping
26
30
  render jsonrpc: 'pong'
27
31
  end
32
+
33
+ # POST /
34
+ def ping_or_echo
35
+ results = jsonrpc_batch.process_each do |request_or_notification|
36
+ case request_or_notification.method
37
+ when 'echo'
38
+ request_or_notification.params
39
+ when 'ping'
40
+ 'pong'
41
+ end
42
+ end
43
+
44
+ render jsonrpc: results
45
+ end
28
46
  end
29
47
  ```
30
48
 
@@ -36,18 +54,35 @@ bundle exec rackup
36
54
 
37
55
  ## API
38
56
 
39
- The server implements an echo API with these procedures:
57
+ The server implements these procedures:
40
58
 
41
59
  - `echo` - Returns the input message
42
- - `ping` - Returns "pong"
60
+ - `ping` - Returns `'pong'`
43
61
 
44
62
  ## Example Requests
45
63
 
64
+ Echo request:
46
65
  ```sh
47
66
  curl -X POST http://localhost:9292 \
48
67
  -H "Content-Type: application/json" \
49
68
  -d '{"jsonrpc": "2.0", "method": "echo", "params": {"message": "Hello, World!"}, "id": 1}'
69
+ ```
70
+
71
+ Ping request:
72
+ ```sh
73
+ curl -X POST http://localhost:9292 \
74
+ -H "Content-Type: application/json" \
75
+ -d '{"jsonrpc": "2.0", "method": "ping", "params": {}, "id": 2}'
76
+ ```
77
+
78
+ Batch request with multiple methods:
79
+ ```sh
80
+ # Batch request with multiple methods
50
81
  curl -X POST http://localhost:9292 \
51
82
  -H "Content-Type: application/json" \
52
- -d '{"jsonrpc": "2.0""method": "ping", "params": {}, "id": 2}'
83
+ -d '[
84
+ {"jsonrpc": "2.0", "method": "echo", "params": {"message": "Hello from batch!"}, "id": 1},
85
+ {"jsonrpc": "2.0", "method": "ping", "params": {}, "id": 2},
86
+ {"jsonrpc": "2.0", "method": "echo", "params": {"message": "Another echo"}, "id": 3}
87
+ ]'
53
88
  ```
@@ -37,6 +37,10 @@ class App < Rails::Application
37
37
  config.hosts.clear
38
38
 
39
39
  routes.append do
40
+ # Handle batch requests
41
+ post '/', to: 'jsonrpc#ping_or_echo', constraints: JSONRPC::BatchConstraint.new
42
+
43
+ # Handle individual method requests
40
44
  post '/', to: 'jsonrpc#echo', constraints: JSONRPC::MethodConstraint.new('echo')
41
45
  post '/', to: 'jsonrpc#ping', constraints: JSONRPC::MethodConstraint.new('ping')
42
46
  end
@@ -51,6 +55,19 @@ class JsonrpcController < ActionController::Base
51
55
  def ping
52
56
  render jsonrpc: 'pong'
53
57
  end
58
+
59
+ def ping_or_echo
60
+ results = jsonrpc_batch.process_each do |request_or_notification|
61
+ case request_or_notification.method
62
+ when 'echo'
63
+ request_or_notification.params
64
+ when 'ping'
65
+ 'pong'
66
+ end
67
+ end
68
+
69
+ render jsonrpc: results
70
+ end
54
71
  end
55
72
 
56
73
  App.initialize!
@@ -1,8 +1,9 @@
1
1
  PATH
2
2
  remote: ../..
3
3
  specs:
4
- jsonrpc-middleware (0.4.0)
4
+ jsonrpc-middleware (0.5.0)
5
5
  dry-validation (~> 1.11)
6
+ multi_json (~> 1.17)
6
7
  zeitwerk (~> 2.7)
7
8
 
8
9
  GEM
@@ -47,7 +48,7 @@ GEM
47
48
  dry-schema (~> 1.14)
48
49
  zeitwerk (~> 2.6)
49
50
  logger (1.7.0)
50
- multi_json (1.16.0)
51
+ multi_json (1.17.0)
51
52
  mustermann (3.0.3)
52
53
  ruby2_keywords (~> 0.0.1)
53
54
  nio4r (2.7.4)
@@ -1,8 +1,9 @@
1
1
  PATH
2
2
  remote: ../..
3
3
  specs:
4
- jsonrpc-middleware (0.4.0)
4
+ jsonrpc-middleware (0.5.0)
5
5
  dry-validation (~> 1.11)
6
+ multi_json (~> 1.17)
6
7
  zeitwerk (~> 2.7)
7
8
 
8
9
  GEM
@@ -47,7 +48,7 @@ GEM
47
48
  dry-schema (~> 1.14)
48
49
  zeitwerk (~> 2.6)
49
50
  logger (1.7.0)
50
- multi_json (1.16.0)
51
+ multi_json (1.17.0)
51
52
  mustermann (3.0.3)
52
53
  ruby2_keywords (~> 0.0.1)
53
54
  nio4r (2.7.4)
@@ -75,7 +75,7 @@ module JSONRPC
75
75
  # @return [String] the JSON-formatted batch
76
76
  #
77
77
  def to_json(*)
78
- to_h.to_json(*)
78
+ MultiJson.dump(to_h, *)
79
79
  end
80
80
 
81
81
  # Implements the Enumerable contract by yielding each request in the batch
@@ -75,7 +75,7 @@ module JSONRPC
75
75
  # @return [String] the JSON-formatted batch response
76
76
  #
77
77
  def to_json(*)
78
- to_h.to_json(*)
78
+ MultiJson.dump(to_h, *)
79
79
  end
80
80
 
81
81
  # Implements the Enumerable contract by yielding each response in the batch
@@ -101,6 +101,19 @@ module JSONRPC
101
101
  #
102
102
  attr_reader :validate_procedure_signatures
103
103
 
104
+ # JSON adapter to use (optional)
105
+ #
106
+ # @api public
107
+ #
108
+ # @example
109
+ # config.json_adapter = :oj
110
+ #
111
+ # @return [Symbol, nil] the JSON adapter to use
112
+ #
113
+ def json_adapter=(adapter)
114
+ MultiJson.use(adapter)
115
+ end
116
+
104
117
  # Initializes a new Configuration instance
105
118
  #
106
119
  # @api public
data/lib/jsonrpc/error.rb CHANGED
@@ -128,7 +128,7 @@ module JSONRPC
128
128
  # @return [String] the error as a JSON string
129
129
  #
130
130
  def to_json(*)
131
- to_h.to_json(*)
131
+ MultiJson.dump(to_h, *)
132
132
  end
133
133
 
134
134
  # Converts the error to a complete JSON-RPC response
@@ -324,7 +324,7 @@ module JSONRPC
324
324
 
325
325
  return [] unless status == 200 && !body.empty?
326
326
 
327
- app_responses = JSON.parse(body.join)
327
+ app_responses = MultiJson.load(body.join)
328
328
  app_responses.map do |resp|
329
329
  Response.new(id: resp['id'], result: resp['result'], error: resp['error'])
330
330
  end
@@ -359,7 +359,8 @@ module JSONRPC
359
359
  # @return [Array] Rack response tuple [status, headers, body]
360
360
  #
361
361
  def json_response(status, body)
362
- [status, { 'content-type' => 'application/json' }, [body.is_a?(String) ? body : JSON.generate(body)]]
362
+ json_body = body.is_a?(String) ? body : MultiJson.dump(body)
363
+ [status, { 'content-type' => 'application/json' }, [json_body]]
363
364
  end
364
365
 
365
366
  # Reads and returns the request body from the Rack environment
@@ -108,7 +108,7 @@ module JSONRPC
108
108
  # @return [String] the notification as a JSON string
109
109
  #
110
110
  def to_json(*)
111
- to_h.to_json(*)
111
+ MultiJson.dump(to_h, *)
112
112
  end
113
113
 
114
114
  private
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
-
5
3
  module JSONRPC
6
4
  # JSON-RPC 2.0 Parser for converting raw JSON into JSONRPC objects
7
5
  #
@@ -37,17 +35,21 @@ module JSONRPC
37
35
  # @raise [InvalidRequestError] if the request structure is invalid
38
36
  #
39
37
  def parse(json)
40
- begin
41
- data = JSON.parse(json)
42
- rescue JSON::ParserError => e
43
- raise ParseError.new(data: { details: e.message })
44
- end
38
+ data = MultiJson.load(json)
45
39
 
46
40
  if data.is_a?(Array)
47
41
  parse_batch(data)
48
42
  else
49
43
  parse_single(data)
50
44
  end
45
+ rescue MultiJson::ParseError => e
46
+ raise ParseError.new(
47
+ data: {
48
+ details: e.message,
49
+ adapter: MultiJson.adapter.name,
50
+ input_preview: json[0..100]
51
+ }
52
+ )
51
53
  end
52
54
 
53
55
  private
@@ -35,7 +35,7 @@ module JSONRPC
35
35
  self.response_body = ''
36
36
  ''
37
37
  else
38
- result_data.to_json
38
+ MultiJson.dump(result_data)
39
39
  end
40
40
  elsif jsonrpc_notification?
41
41
  # Notification - no response body
@@ -44,7 +44,7 @@ module JSONRPC
44
44
  ''
45
45
  else
46
46
  # Fallback - treat as regular JSON-RPC response
47
- result_data.to_json
47
+ MultiJson.dump(result_data)
48
48
  end
49
49
  end
50
50
  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
  #
@@ -85,16 +87,6 @@ module JSONRPC
85
87
  #
86
88
  # @raise [ArgumentError] if id is not a String, Integer, or nil
87
89
  #
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
90
 
99
91
  # Checks if the response is successful
100
92
  #
@@ -106,7 +98,7 @@ module JSONRPC
106
98
  # @return [Boolean] true if the response contains a result, false if it contains an error
107
99
  #
108
100
  def success?
109
- !@result.nil?
101
+ !result.nil?
110
102
  end
111
103
 
112
104
  # Checks if the response is an error
@@ -119,7 +111,7 @@ module JSONRPC
119
111
  # @return [Boolean] true if the response contains an error, false if it contains a result
120
112
  #
121
113
  def error?
122
- !@error.nil?
114
+ !error.nil?
123
115
  end
124
116
 
125
117
  # Converts the response to a JSON-compatible Hash
@@ -156,48 +148,7 @@ module JSONRPC
156
148
  # @return [String] the response as a JSON string
157
149
  #
158
150
  def to_json(*)
159
- to_h.to_json(*)
160
- end
161
-
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)
151
+ MultiJson.dump(to_h, *)
201
152
  end
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
+ include Dry.Types()
12
+ end
13
+ end
@@ -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.6.0'
15
15
  end
data/lib/jsonrpc.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'zeitwerk'
4
+ require 'dry-struct'
4
5
  require 'dry-validation'
6
+ require 'multi_json'
5
7
 
6
8
  Dry::Validation.load_extensions(:predicates_as_macros)
7
9
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jsonrpc-middleware
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wilson Silva
@@ -9,6 +9,20 @@ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: dry-struct
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.8'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.8'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: dry-validation
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -23,6 +37,20 @@ dependencies:
23
37
  - - "~>"
24
38
  - !ruby/object:Gem::Version
25
39
  version: '1.11'
40
+ - !ruby/object:Gem::Dependency
41
+ name: multi_json
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.17'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.17'
26
54
  - !ruby/object:Gem::Dependency
27
55
  name: zeitwerk
28
56
  requirement: !ruby/object:Gem::Requirement
@@ -47,6 +75,7 @@ extra_rdoc_files: []
47
75
  files:
48
76
  - ".aiignore"
49
77
  - ".claude/commands/document.md"
78
+ - ".claude/commands/gemfile/update.md"
50
79
  - ".claude/commands/test.md"
51
80
  - ".claude/docs/yard.md"
52
81
  - ".claude/settings.local.json"
@@ -144,6 +173,7 @@ files:
144
173
  - lib/jsonrpc/railtie/routes_dsl.rb
145
174
  - lib/jsonrpc/request.rb
146
175
  - lib/jsonrpc/response.rb
176
+ - lib/jsonrpc/types.rb
147
177
  - lib/jsonrpc/validator.rb
148
178
  - lib/jsonrpc/version.rb
149
179
  - sig/jsonrpc.rbs
@@ -171,7 +201,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
171
201
  - !ruby/object:Gem::Version
172
202
  version: '0'
173
203
  requirements: []
174
- rubygems_version: 3.7.0
204
+ rubygems_version: 3.7.2
175
205
  specification_version: 4
176
206
  summary: Rack middleware implementing the JSON-RPC 2.0 protocol.
177
207
  test_files: []