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 +4 -4
- data/.claude/commands/gemfile/update.md +52 -0
- data/.claude/settings.local.json +6 -2
- data/CHANGELOG.md +24 -0
- data/README.md +1 -0
- data/Rakefile +17 -16
- data/examples/README.md +9 -0
- data/examples/rack/Gemfile.lock +10 -1
- data/examples/rack-echo/Gemfile.lock +10 -1
- data/examples/rails/Gemfile.lock +10 -22
- data/examples/rails-single-file-routing/README.md +38 -3
- data/examples/rails-single-file-routing/config.ru +17 -0
- data/examples/sinatra-classic/Gemfile.lock +3 -2
- data/examples/sinatra-modular/Gemfile.lock +3 -2
- data/lib/jsonrpc/batch_request.rb +1 -1
- data/lib/jsonrpc/batch_response.rb +1 -1
- data/lib/jsonrpc/configuration.rb +13 -0
- data/lib/jsonrpc/error.rb +1 -1
- data/lib/jsonrpc/middleware.rb +3 -2
- data/lib/jsonrpc/notification.rb +1 -1
- data/lib/jsonrpc/parser.rb +9 -7
- data/lib/jsonrpc/railtie.rb +2 -2
- data/lib/jsonrpc/request.rb +12 -84
- data/lib/jsonrpc/response.rb +10 -59
- data/lib/jsonrpc/types.rb +13 -0
- data/lib/jsonrpc/version.rb +1 -1
- data/lib/jsonrpc.rb +2 -0
- metadata +32 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bf6393e7fe408f08dc933ebb6fc73a2541829eab966df8acedd7103a2e4e2c01
|
4
|
+
data.tar.gz: f1f17e444ea3a84cfd69e46316e595eaa00fc0a01e9566e313111dc0e47142b8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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}
|
data/.claude/settings.local.json
CHANGED
@@ -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
|
-
|
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:
|
data/examples/rack/Gemfile.lock
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
PATH
|
2
2
|
remote: ../..
|
3
3
|
specs:
|
4
|
-
jsonrpc-middleware (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
|
+
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)
|
data/examples/rails/Gemfile.lock
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
PATH
|
2
2
|
remote: ../..
|
3
3
|
specs:
|
4
|
-
jsonrpc-middleware (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
|
57
|
+
The server implements these procedures:
|
40
58
|
|
41
59
|
- `echo` - Returns the input message
|
42
|
-
- `ping` - Returns
|
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 '
|
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
|
+
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.
|
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
|
+
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.
|
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)
|
@@ -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
data/lib/jsonrpc/middleware.rb
CHANGED
@@ -324,7 +324,7 @@ module JSONRPC
|
|
324
324
|
|
325
325
|
return [] unless status == 200 && !body.empty?
|
326
326
|
|
327
|
-
app_responses =
|
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
|
-
|
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
|
data/lib/jsonrpc/notification.rb
CHANGED
data/lib/jsonrpc/parser.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/jsonrpc/railtie.rb
CHANGED
@@ -35,7 +35,7 @@ module JSONRPC
|
|
35
35
|
self.response_body = ''
|
36
36
|
''
|
37
37
|
else
|
38
|
-
result_data
|
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
|
47
|
+
MultiJson.dump(result_data)
|
48
48
|
end
|
49
49
|
end
|
50
50
|
end
|
data/lib/jsonrpc/request.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
93
|
+
MultiJson.dump(to_h, *)
|
123
94
|
end
|
124
95
|
|
125
|
-
|
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
|
-
# @
|
98
|
+
# @api public
|
168
99
|
#
|
169
|
-
# @
|
100
|
+
# @example
|
101
|
+
# request.method # => "subtract"
|
170
102
|
#
|
171
|
-
# @return [
|
103
|
+
# @return [String]
|
172
104
|
#
|
173
|
-
def
|
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
|
data/lib/jsonrpc/response.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/jsonrpc/version.rb
CHANGED
data/lib/jsonrpc.rb
CHANGED
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.
|
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.
|
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: []
|