jsonrpc-middleware 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +66 -1
- data/README.md +2 -0
- data/Rakefile +56 -0
- data/examples/procedures.rb +1 -5
- data/examples/rack/Gemfile.lock +1 -1
- data/examples/rack-echo/Gemfile.lock +1 -1
- data/examples/rails/Gemfile.lock +1 -1
- data/examples/rails/config/initializers/jsonrpc.rb +1 -5
- data/examples/rails-routing-dsl/README.md +199 -0
- data/examples/rails-routing-dsl/config.ru +146 -0
- data/examples/rails-single-file-routing/README.md +1 -1
- data/examples/rails-single-file-routing/config.ru +1 -5
- data/examples/sinatra-classic/Gemfile.lock +1 -1
- data/examples/sinatra-modular/Gemfile.lock +1 -1
- data/lib/jsonrpc/batch_request.rb +0 -13
- data/lib/jsonrpc/configuration.rb +12 -3
- data/lib/jsonrpc/railtie/batch_constraint.rb +25 -0
- data/lib/jsonrpc/railtie/mapper_extension.rb +34 -0
- data/lib/jsonrpc/railtie/method_constraint.rb +1 -1
- data/lib/jsonrpc/railtie/routes_dsl.rb +147 -0
- data/lib/jsonrpc/railtie.rb +7 -0
- data/lib/jsonrpc/version.rb +1 -1
- metadata +7 -2
- /data/{.aiexclude → .aiignore} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5d8775dfa68b34e4513883b9bdbfb565a708902efe694727bb19d517d4539cd7
|
4
|
+
data.tar.gz: 8f087e14952785c53470895871bb175e0e48fd9aab560b9906cd17b509d6f4e2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fd7b7f8ffb647ad4e08a487307bfa58f7a2f33fbc0d5e0f248800f7ac17bc1c01cfb0c7c5d3d6804601ac7b48d5a248084ea1a39bf3c1f38fc84628776e9c7b2
|
7
|
+
data.tar.gz: cad4b6b454c561cc04ef2c4452722f562a9902a1c45020e709ffd793d3a2fb0fb1d1c4556a0f3867c6e30339a6fc24c58c0b38762b9aa3940eedd9c488324b09
|
data/CHANGELOG.md
CHANGED
@@ -5,10 +5,74 @@ 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.5.0] - 2025-07-22
|
9
|
+
|
10
|
+
### Added
|
11
|
+
- Rails routing DSL for elegant JSON-RPC method mapping with support for namespaces and batch handling:
|
12
|
+
```ruby
|
13
|
+
# In routes.rb
|
14
|
+
jsonrpc '/' do
|
15
|
+
# Handle batch requests
|
16
|
+
batch to: 'batch#handle'
|
17
|
+
|
18
|
+
method 'on', to: 'main#on'
|
19
|
+
method 'off', to: 'main#off'
|
20
|
+
|
21
|
+
namespace 'lights' do
|
22
|
+
method 'on', to: 'lights#on' # becomes lights.on
|
23
|
+
method 'off', to: 'lights#off' # becomes lights.off
|
24
|
+
end
|
25
|
+
|
26
|
+
namespace 'climate' do
|
27
|
+
method 'on', to: 'climate#on' # becomes climate.on
|
28
|
+
method 'off', to: 'climate#off' # becomes climate.off
|
29
|
+
|
30
|
+
namespace 'fan' do
|
31
|
+
method 'on', to: 'fan#on' # becomes climate.fan.on
|
32
|
+
method 'off', to: 'fan#off' # becomes climate.fan.off
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
```
|
37
|
+
- `JSONRPC::BatchConstraint` for routing JSON-RPC batch requests to dedicated controllers:
|
38
|
+
```ruby
|
39
|
+
# Handle batch requests with custom constraint
|
40
|
+
post '/api', to: 'api#handle_batch', constraints: JSONRPC::BatchConstraint.new
|
41
|
+
|
42
|
+
# Or use the DSL (recommended)
|
43
|
+
jsonrpc '/api' do
|
44
|
+
batch to: 'api#handle_batch'
|
45
|
+
end
|
46
|
+
```
|
47
|
+
|
48
|
+
### Changed
|
49
|
+
- Procedure registration now supports optional validation blocks (defaults to empty contract):
|
50
|
+
```ruby
|
51
|
+
# Before: Always required validation block (even if empty)
|
52
|
+
procedure('ping') do
|
53
|
+
params do
|
54
|
+
# No params needed
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# After: Optional validation block
|
59
|
+
procedure('ping') # Uses empty contract by default
|
60
|
+
|
61
|
+
# Still works with validation when needed
|
62
|
+
procedure('add') do
|
63
|
+
params do
|
64
|
+
required(:a).value(:integer)
|
65
|
+
required(:b).value(:integer)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
```
|
69
|
+
- Simplified example configurations by removing unnecessary empty validation blocks
|
70
|
+
- Enhanced Rails integration with automatic DSL registration via Railtie
|
71
|
+
|
8
72
|
## [0.4.0] - 2025-07-18
|
9
73
|
|
10
74
|
### Added
|
11
|
-
- JSONRPC::BatchRequest#process_each method for simplified batch processing
|
75
|
+
- `JSONRPC::BatchRequest#process_each` method for simplified batch processing
|
12
76
|
|
13
77
|
## [0.3.0] - 2025-07-17
|
14
78
|
|
@@ -71,6 +135,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
71
135
|
- Helper methods for request and response processing
|
72
136
|
- Examples for basic and advanced usage scenarios
|
73
137
|
|
138
|
+
[0.5.0]: https://github.com/wilsonsilva/jsonrpc-middleware/compare/v0.4.0...v0.5.0
|
74
139
|
[0.4.0]: https://github.com/wilsonsilva/jsonrpc-middleware/compare/v0.3.0...v0.4.0
|
75
140
|
[0.3.0]: https://github.com/wilsonsilva/jsonrpc-middleware/compare/v0.2.0...v0.3.0
|
76
141
|
[0.2.0]: https://github.com/wilsonsilva/jsonrpc-middleware/compare/v0.1.0...v0.2.0
|
data/README.md
CHANGED
@@ -37,6 +37,7 @@ A Rack middleware implementing the JSON-RPC 2.0 protocol that integrates easily
|
|
37
37
|
- **Spec-compliant**: Fully implements the [JSON-RPC 2.0 specification](https://www.jsonrpc.org/specification)
|
38
38
|
- **Rack middleware integration**: Seamlessly integrates with Rack applications (Rails, Sinatra, Hanami, etc)
|
39
39
|
- **Support for all request types**: Handles single requests, notifications, and batch requests
|
40
|
+
- **Rails routing DSL**: Elegant routing DSL for Rails applications with support for namespaces and batch handling
|
40
41
|
- **Error handling**: Comprehensive error handling with standard JSON-RPC error responses
|
41
42
|
- **Request validation**: Define request parameter specifications and validations
|
42
43
|
- **Helpers**: Convenient helper methods to simplify request and response processing
|
@@ -174,6 +175,7 @@ rake bundle:audit:update # Updates the bundler-audit vulnerability database
|
|
174
175
|
rake clean # Remove any temporary products
|
175
176
|
rake clobber # Remove any generated files
|
176
177
|
rake coverage # Run spec with coverage
|
178
|
+
rake examples:bundle_install # Run bundle install on all example folders (useful after updating the gem version)
|
177
179
|
rake install # Build and install jsonrpc-middleware.gem into system gems
|
178
180
|
rake install:local # Build and install jsonrpc-middleware.gem into system gems without network access
|
179
181
|
rake qa # Test, lint and perform security and documentation audits
|
data/Rakefile
CHANGED
@@ -95,3 +95,59 @@ namespace :yard do
|
|
95
95
|
puts 'Done!'
|
96
96
|
end
|
97
97
|
end
|
98
|
+
|
99
|
+
namespace :examples do
|
100
|
+
desc 'Run bundle install on all example folders'
|
101
|
+
task :bundle_install do
|
102
|
+
examples_dir = File.join(Dir.pwd, 'examples')
|
103
|
+
|
104
|
+
unless Dir.exist?(examples_dir)
|
105
|
+
puts 'Examples directory not found'
|
106
|
+
exit 1
|
107
|
+
end
|
108
|
+
|
109
|
+
example_folders = Dir.glob(File.join(examples_dir, '*')).select { |path| Dir.exist?(path) }
|
110
|
+
|
111
|
+
if example_folders.empty?
|
112
|
+
puts 'No example folders found'
|
113
|
+
return
|
114
|
+
end
|
115
|
+
|
116
|
+
puts "Found #{example_folders.length} example folders:"
|
117
|
+
example_folders.each { |folder| puts " - #{File.basename(folder)}" }
|
118
|
+
puts
|
119
|
+
|
120
|
+
failed_folders = []
|
121
|
+
|
122
|
+
example_folders.each do |folder|
|
123
|
+
gemfile_path = File.join(folder, 'Gemfile')
|
124
|
+
|
125
|
+
unless File.exist?(gemfile_path)
|
126
|
+
puts "Skipping #{File.basename(folder)} - no Gemfile found"
|
127
|
+
next
|
128
|
+
end
|
129
|
+
|
130
|
+
puts "Running bundle install in #{File.basename(folder)}..."
|
131
|
+
|
132
|
+
Dir.chdir(folder) do
|
133
|
+
system('bundle install')
|
134
|
+
|
135
|
+
unless $?.success?
|
136
|
+
failed_folders << File.basename(folder)
|
137
|
+
puts " ✗ Failed to bundle install in #{File.basename(folder)}"
|
138
|
+
else
|
139
|
+
puts " ✓ Successfully installed gems in #{File.basename(folder)}"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
puts
|
144
|
+
end
|
145
|
+
|
146
|
+
if failed_folders.empty?
|
147
|
+
puts 'All example folders processed successfully!'
|
148
|
+
else
|
149
|
+
puts "Failed to process #{failed_folders.length} folders: #{failed_folders.join(', ')}"
|
150
|
+
exit 1
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
data/examples/procedures.rb
CHANGED
data/examples/rack/Gemfile.lock
CHANGED
data/examples/rails/Gemfile.lock
CHANGED
@@ -0,0 +1,199 @@
|
|
1
|
+
# Rails JSON-RPC Routing DSL
|
2
|
+
|
3
|
+
Demonstrates using the Rails routing DSL extension to route JSON-RPC methods to different controller actions for a
|
4
|
+
smart home control system.
|
5
|
+
|
6
|
+
## Highlights
|
7
|
+
|
8
|
+
Uses the `jsonrpc` routing DSL to map JSON-RPC methods to Rails controller actions with clean, readable syntax:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
class App < Rails::Application
|
12
|
+
# ...
|
13
|
+
routes.append do
|
14
|
+
jsonrpc '/' do
|
15
|
+
# Handle batch requests with a dedicated controller
|
16
|
+
batch to: 'batch#handle'
|
17
|
+
|
18
|
+
method 'on', to: 'main#on'
|
19
|
+
method 'off', to: 'main#off'
|
20
|
+
|
21
|
+
namespace 'lights' do
|
22
|
+
method 'on', to: 'lights#on' # becomes lights.on
|
23
|
+
method 'off', to: 'lights#off' # becomes lights.off
|
24
|
+
end
|
25
|
+
|
26
|
+
namespace 'climate' do
|
27
|
+
method 'on', to: 'climate#on' # becomes climate.on
|
28
|
+
method 'off', to: 'climate#off' # becomes climate.off
|
29
|
+
|
30
|
+
namespace 'fan' do
|
31
|
+
method 'on', to: 'fan#on' # becomes climate.fan.on
|
32
|
+
method 'off', to: 'fan#off' # becomes climate.fan.off
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class MainController < ActionController::Base
|
40
|
+
def on
|
41
|
+
render jsonrpc: { device: 'main_system', status: 'on' }
|
42
|
+
end
|
43
|
+
|
44
|
+
def off
|
45
|
+
render jsonrpc: { device: 'main_system', status: 'off' }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class LightsController < ActionController::Base
|
50
|
+
def on
|
51
|
+
render jsonrpc: { device: 'lights', status: 'on' }
|
52
|
+
end
|
53
|
+
|
54
|
+
def off
|
55
|
+
render jsonrpc: { device: 'lights', status: 'off' }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class ClimateController < ActionController::Base
|
60
|
+
def on
|
61
|
+
render jsonrpc: { device: 'climate_system', status: 'on' }
|
62
|
+
end
|
63
|
+
|
64
|
+
def off
|
65
|
+
render jsonrpc: { device: 'climate_system', status: 'off' }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class FanController < ActionController::Base
|
70
|
+
def on
|
71
|
+
render jsonrpc: { device: 'fan', status: 'on' }
|
72
|
+
end
|
73
|
+
|
74
|
+
def off
|
75
|
+
render jsonrpc: { device: 'fan', status: 'off' }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class BatchController < ActionController::Base
|
80
|
+
def handle
|
81
|
+
# Process each request in the batch and collect results
|
82
|
+
results = jsonrpc_batch.process_each do |request_or_notification|
|
83
|
+
case request_or_notification.method
|
84
|
+
when 'on'
|
85
|
+
{ device: 'main_system', status: 'on' }
|
86
|
+
when 'off'
|
87
|
+
{ device: 'main_system', status: 'off' }
|
88
|
+
when 'lights.on'
|
89
|
+
{ device: 'lights', status: 'on' }
|
90
|
+
when 'lights.off'
|
91
|
+
{ device: 'lights', status: 'off' }
|
92
|
+
# ... handle other methods
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
render jsonrpc: results
|
97
|
+
end
|
98
|
+
end
|
99
|
+
```
|
100
|
+
|
101
|
+
## Running
|
102
|
+
|
103
|
+
```sh
|
104
|
+
bundle exec rackup
|
105
|
+
```
|
106
|
+
|
107
|
+
## API
|
108
|
+
|
109
|
+
The server implements smart home controls with these procedures:
|
110
|
+
|
111
|
+
**Root Methods:**
|
112
|
+
- `on` - Turn home automation system on
|
113
|
+
- `off` - Turn home automation system off
|
114
|
+
|
115
|
+
**Lights Namespace:**
|
116
|
+
- `lights.on` - Turn lights on
|
117
|
+
- `lights.off` - Turn lights off
|
118
|
+
|
119
|
+
**Climate Namespace:**
|
120
|
+
- `climate.on` - Turn climate system on
|
121
|
+
- `climate.off` - Turn climate system off
|
122
|
+
|
123
|
+
**Climate Fan Namespace:**
|
124
|
+
- `climate.fan.on` - Turn fan on
|
125
|
+
- `climate.fan.off` - Turn fan off
|
126
|
+
|
127
|
+
**Batch Processing:**
|
128
|
+
- Batch requests are automatically routed to the `BatchController#handle` action
|
129
|
+
- The controller uses `jsonrpc_batch.process_each` to handle each request in the batch
|
130
|
+
- Responses are collected and returned as an array
|
131
|
+
|
132
|
+
## Example Requests
|
133
|
+
|
134
|
+
Turn on the home automation system:
|
135
|
+
```sh
|
136
|
+
curl -X POST http://localhost:9292 \
|
137
|
+
-H "Content-Type: application/json" \
|
138
|
+
-d '{"jsonrpc": "2.0", "method": "on", "params": {}, "id": 1}'
|
139
|
+
```
|
140
|
+
|
141
|
+
Turn off the home automation system:
|
142
|
+
```sh
|
143
|
+
curl -X POST http://localhost:9292 \
|
144
|
+
-H "Content-Type: application/json" \
|
145
|
+
-d '{"jsonrpc": "2.0", "method": "off", "params": {}, "id": 2}'
|
146
|
+
```
|
147
|
+
|
148
|
+
Turn on lights:
|
149
|
+
```sh
|
150
|
+
curl -X POST http://localhost:9292 \
|
151
|
+
-H "Content-Type: application/json" \
|
152
|
+
-d '{"jsonrpc": "2.0", "method": "lights.on", "params": {}, "id": 3}'
|
153
|
+
```
|
154
|
+
|
155
|
+
Turn off the lights:
|
156
|
+
```sh
|
157
|
+
curl -X POST http://localhost:9292 \
|
158
|
+
-H "Content-Type: application/json" \
|
159
|
+
-d '{"jsonrpc": "2.0", "method": "lights.off", "params": {}, "id": 4}'
|
160
|
+
```
|
161
|
+
|
162
|
+
Turn on the climate system:
|
163
|
+
```sh
|
164
|
+
curl -X POST http://localhost:9292 \
|
165
|
+
-H "Content-Type: application/json" \
|
166
|
+
-d '{"jsonrpc": "2.0", "method": "climate.on", "params": {}, "id": 5}'
|
167
|
+
```
|
168
|
+
|
169
|
+
Turn off the climate system:
|
170
|
+
```sh
|
171
|
+
curl -X POST http://localhost:9292 \
|
172
|
+
-H "Content-Type: application/json" \
|
173
|
+
-d '{"jsonrpc": "2.0", "method": "climate.off", "params": {}, "id": 6}'
|
174
|
+
```
|
175
|
+
|
176
|
+
Turn on fan:
|
177
|
+
```sh
|
178
|
+
curl -X POST http://localhost:9292 \
|
179
|
+
-H "Content-Type: application/json" \
|
180
|
+
-d '{"jsonrpc": "2.0", "method": "climate.fan.on", "params": {}, "id": 7}'
|
181
|
+
```
|
182
|
+
|
183
|
+
Turn off fan:
|
184
|
+
```sh
|
185
|
+
curl -X POST http://localhost:9292 \
|
186
|
+
-H "Content-Type: application/json" \
|
187
|
+
-d '{"jsonrpc": "2.0", "method": "climate.fan.off", "params": {}, "id": 8}'
|
188
|
+
```
|
189
|
+
|
190
|
+
Batch request for evening routine:
|
191
|
+
```sh
|
192
|
+
curl -X POST http://localhost:9292 \
|
193
|
+
-H "Content-Type: application/json" \
|
194
|
+
-d '[
|
195
|
+
{"jsonrpc": "2.0", "method": "off", "params": {}, "id": 9},
|
196
|
+
{"jsonrpc": "2.0", "method": "lights.off", "params": {}, "id": 10},
|
197
|
+
{"jsonrpc": "2.0", "method": "climate.off", "params": {}, "id": 11}
|
198
|
+
]'
|
199
|
+
```
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/inline'
|
4
|
+
|
5
|
+
gemfile(true) do
|
6
|
+
source 'https://rubygems.org'
|
7
|
+
|
8
|
+
gem 'rails', '~> 8.0.2'
|
9
|
+
gem 'puma', '~> 6.6.0'
|
10
|
+
gem 'jsonrpc-middleware', path: '../../', require: 'jsonrpc'
|
11
|
+
end
|
12
|
+
|
13
|
+
require 'rails'
|
14
|
+
require 'action_controller/railtie'
|
15
|
+
|
16
|
+
JSONRPC.configure do |config|
|
17
|
+
config.rescue_internal_errors = true # set to +false+ if you want to raise JSONRPC::InternalError manually
|
18
|
+
|
19
|
+
# Define the allowed JSON-RPC methods. Calls to methods absent from this list will return a method not found error.
|
20
|
+
procedure 'on'
|
21
|
+
procedure 'off'
|
22
|
+
procedure 'lights.on'
|
23
|
+
procedure 'lights.off'
|
24
|
+
procedure 'climate.on'
|
25
|
+
procedure 'climate.off'
|
26
|
+
procedure 'climate.fan.on'
|
27
|
+
procedure 'climate.fan.off'
|
28
|
+
end
|
29
|
+
|
30
|
+
# Define the application
|
31
|
+
class App < Rails::Application
|
32
|
+
config.root = __dir__
|
33
|
+
config.cache_classes = true
|
34
|
+
config.eager_load = true
|
35
|
+
config.active_support.deprecation = :stderr
|
36
|
+
config.consider_all_requests_local = true
|
37
|
+
config.active_support.to_time_preserves_timezone = :zone
|
38
|
+
config.logger = nil
|
39
|
+
config.hosts.clear
|
40
|
+
|
41
|
+
routes.append do
|
42
|
+
jsonrpc '/' do
|
43
|
+
# Handle batch requests with a dedicated controller
|
44
|
+
batch to: 'batch#handle'
|
45
|
+
|
46
|
+
method :on, to: 'main#on'
|
47
|
+
method :off, to: 'main#off'
|
48
|
+
|
49
|
+
namespace 'lights' do
|
50
|
+
method :on, to: 'lights#on' # becomes lights.on
|
51
|
+
method :off, to: 'lights#off' # becomes lights.off
|
52
|
+
end
|
53
|
+
|
54
|
+
namespace 'climate' do
|
55
|
+
method :on, to: 'climate#on' # becomes climate.on
|
56
|
+
method :off, to: 'climate#off' # becomes climate.off
|
57
|
+
|
58
|
+
namespace 'fan' do
|
59
|
+
method :on, to: 'fan#on' # becomes climate.fan.on
|
60
|
+
method :off, to: 'fan#off' # becomes climate.fan.off
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Controller for main system operations
|
68
|
+
class MainController < ActionController::Base
|
69
|
+
def on
|
70
|
+
render jsonrpc: { device: 'main_system', status: 'on' }
|
71
|
+
end
|
72
|
+
|
73
|
+
def off
|
74
|
+
render jsonrpc: { device: 'main_system', status: 'off' }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Controller for lights operations
|
79
|
+
class LightsController < ActionController::Base
|
80
|
+
def on
|
81
|
+
render jsonrpc: { device: 'lights', status: 'on' }
|
82
|
+
end
|
83
|
+
|
84
|
+
def off
|
85
|
+
render jsonrpc: { device: 'lights', status: 'off' }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Controller for climate operations
|
90
|
+
class ClimateController < ActionController::Base
|
91
|
+
def on
|
92
|
+
render jsonrpc: { device: 'climate_system', status: 'on' }
|
93
|
+
end
|
94
|
+
|
95
|
+
def off
|
96
|
+
render jsonrpc: { device: 'climate_system', status: 'off' }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Controller for climate fan operations
|
101
|
+
class FanController < ActionController::Base
|
102
|
+
def on
|
103
|
+
render jsonrpc: { device: 'fan', status: 'on' }
|
104
|
+
end
|
105
|
+
|
106
|
+
def off
|
107
|
+
render jsonrpc: { device: 'fan', status: 'off' }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Controller for batch operations
|
112
|
+
class BatchController < ActionController::Base
|
113
|
+
def handle
|
114
|
+
# Process each request in the batch and collect results
|
115
|
+
results = jsonrpc_batch.process_each do |request_or_notification|
|
116
|
+
result = case request_or_notification.method
|
117
|
+
when 'on'
|
118
|
+
{ device: 'main_system', status: 'on' }
|
119
|
+
when 'off'
|
120
|
+
{ device: 'main_system', status: 'off' }
|
121
|
+
when 'lights.on'
|
122
|
+
{ device: 'lights', status: 'on' }
|
123
|
+
when 'lights.off'
|
124
|
+
{ device: 'lights', status: 'off' }
|
125
|
+
when 'climate.on'
|
126
|
+
{ device: 'climate_system', status: 'on' }
|
127
|
+
when 'climate.off'
|
128
|
+
{ device: 'climate_system', status: 'off' }
|
129
|
+
when 'climate.fan.on'
|
130
|
+
{ device: 'fan', status: 'on' }
|
131
|
+
when 'climate.fan.off'
|
132
|
+
{ device: 'fan', status: 'off' }
|
133
|
+
else
|
134
|
+
{ error: 'Unknown method', method: request_or_notification.method }
|
135
|
+
end
|
136
|
+
|
137
|
+
result
|
138
|
+
end
|
139
|
+
|
140
|
+
render jsonrpc: results
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
App.initialize!
|
145
|
+
|
146
|
+
run App
|
@@ -124,19 +124,6 @@ module JSONRPC
|
|
124
124
|
#
|
125
125
|
alias length size
|
126
126
|
|
127
|
-
# Returns true if the batch contains no requests
|
128
|
-
#
|
129
|
-
# @api public
|
130
|
-
#
|
131
|
-
# @example Check if batch is empty
|
132
|
-
# batch.empty? # => false
|
133
|
-
#
|
134
|
-
# @return [Boolean] true if the batch is empty, false otherwise
|
135
|
-
#
|
136
|
-
def empty?
|
137
|
-
requests.empty?
|
138
|
-
end
|
139
|
-
|
140
127
|
# Handles each request/notification in the batch and returns responses
|
141
128
|
#
|
142
129
|
# @api public
|
@@ -162,15 +162,24 @@ module JSONRPC
|
|
162
162
|
# end
|
163
163
|
# end
|
164
164
|
#
|
165
|
+
# @example Register a procedure without validation
|
166
|
+
# config.procedure('ping')
|
167
|
+
#
|
165
168
|
# @param method_name [String, Symbol] the name of the procedure
|
166
169
|
# @param allow_positional_arguments [Boolean] whether the procedure accepts positional arguments
|
167
170
|
#
|
168
|
-
# @yield A block that defines the validation contract using Dry::Validation DSL
|
171
|
+
# @yield [optional] A block that defines the validation contract using Dry::Validation DSL
|
169
172
|
#
|
170
173
|
# @return [Procedure] the registered procedure
|
171
174
|
#
|
172
|
-
def procedure(method_name, allow_positional_arguments: false, &)
|
173
|
-
contract_class =
|
175
|
+
def procedure(method_name, allow_positional_arguments: false, &block)
|
176
|
+
contract_class = if block
|
177
|
+
Class.new(Dry::Validation::Contract, &block)
|
178
|
+
else
|
179
|
+
Class.new(Dry::Validation::Contract) do
|
180
|
+
params {} # rubocop:disable Lint/EmptyBlock
|
181
|
+
end
|
182
|
+
end
|
174
183
|
contract_class.class_eval { import_predicates_as_macros }
|
175
184
|
contract = contract_class.new
|
176
185
|
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSONRPC
|
4
|
+
# This constraint allows Rails routes to be matched based on JSON-RPC
|
5
|
+
# batch requests, enabling batch-specific routing to dedicated controllers.
|
6
|
+
#
|
7
|
+
# @example Using in Rails routes
|
8
|
+
# post '/', to: 'jsonrpc#handle_batch', constraints: JSONRPC::BatchConstraint.new
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
#
|
12
|
+
class BatchConstraint
|
13
|
+
# Check if the request is a JSON-RPC batch request
|
14
|
+
#
|
15
|
+
# @param request [ActionDispatch::Request] The Rails request object
|
16
|
+
# @return [Boolean] true if the request is a batch request, false otherwise
|
17
|
+
#
|
18
|
+
def matches?(request)
|
19
|
+
jsonrpc_batch = request.env['jsonrpc.batch']
|
20
|
+
|
21
|
+
# Return true if we have a batch request in the environment
|
22
|
+
!jsonrpc_batch.nil?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSONRPC
|
4
|
+
# Extension module for ActionDispatch::Routing::Mapper
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
#
|
8
|
+
module MapperExtension
|
9
|
+
# Define JSON-RPC routes with a DSL
|
10
|
+
#
|
11
|
+
# @param path [String] the path to handle JSON-RPC requests on
|
12
|
+
#
|
13
|
+
# @example Define JSON-RPC routes
|
14
|
+
# jsonrpc '/api/v1' do
|
15
|
+
# # Handle batch requests
|
16
|
+
# batch to: 'batch#handle'
|
17
|
+
#
|
18
|
+
# method 'user.create', to: 'users#create'
|
19
|
+
# method 'user.get', to: 'users#show'
|
20
|
+
#
|
21
|
+
# namespace 'posts' do
|
22
|
+
# method 'create', to: 'posts#create'
|
23
|
+
# method 'list', to: 'posts#index'
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# @return [void]
|
28
|
+
#
|
29
|
+
def jsonrpc(path = '/', &)
|
30
|
+
dsl = JSONRPC::RoutesDsl.new(self, path)
|
31
|
+
dsl.instance_eval(&)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -16,7 +16,7 @@ module JSONRPC
|
|
16
16
|
# @param jsonrpc_method_name [String] The JSON-RPC method name to match against
|
17
17
|
#
|
18
18
|
def initialize(jsonrpc_method_name)
|
19
|
-
@jsonrpc_method_name = jsonrpc_method_name
|
19
|
+
@jsonrpc_method_name = jsonrpc_method_name.to_s
|
20
20
|
end
|
21
21
|
|
22
22
|
# Check if the request matches the configured method name
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSONRPC
|
4
|
+
# DSL context for defining JSON-RPC routes within a jsonrpc block
|
5
|
+
#
|
6
|
+
# @example Simple method routing
|
7
|
+
# jsonrpc '/api/v1' do
|
8
|
+
# method 'ping', to: 'system#ping'
|
9
|
+
# method 'status', to: 'system#status'
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# @example Single-level namespace
|
13
|
+
# jsonrpc '/api/v1' do
|
14
|
+
# namespace 'users' do
|
15
|
+
# method 'create', to: 'users#create' # becomes users.create
|
16
|
+
# method 'list', to: 'users#index' # becomes users.list
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# @example Nested namespaces (smart home control system)
|
21
|
+
# jsonrpc '/' do
|
22
|
+
# # Handle batch requests with dedicated controller
|
23
|
+
# batch to: 'batch#handle'
|
24
|
+
#
|
25
|
+
# method 'on', to: 'main#on'
|
26
|
+
# method 'off', to: 'main#off'
|
27
|
+
#
|
28
|
+
# namespace 'lights' do
|
29
|
+
# method 'on', to: 'lights#on' # becomes lights.on
|
30
|
+
# method 'off', to: 'lights#off' # becomes lights.off
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# namespace 'climate' do
|
34
|
+
# method 'on', to: 'climate#on' # becomes climate.on
|
35
|
+
# method 'off', to: 'climate#off' # becomes climate.off
|
36
|
+
#
|
37
|
+
# namespace 'fan' do
|
38
|
+
# method 'on', to: 'fan#on' # becomes climate.fan.on
|
39
|
+
# method 'off', to: 'fan#off' # becomes climate.fan.off
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# @api private
|
45
|
+
#
|
46
|
+
class RoutesDsl
|
47
|
+
# Initialize a new routes DSL context
|
48
|
+
#
|
49
|
+
# @param mapper [ActionDispatch::Routing::Mapper] the Rails route mapper
|
50
|
+
# @param path_prefix [String] the base path for JSON-RPC requests
|
51
|
+
#
|
52
|
+
def initialize(mapper, path_prefix = '/')
|
53
|
+
@mapper = mapper
|
54
|
+
@path_prefix = path_prefix
|
55
|
+
@namespace_stack = []
|
56
|
+
end
|
57
|
+
|
58
|
+
# Define a JSON-RPC method route
|
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
|
+
# @example Map a JSON-RPC method to controller action
|
64
|
+
# method 'user.create', to: 'users#create'
|
65
|
+
#
|
66
|
+
# @example Method within a namespace
|
67
|
+
# namespace 'posts' do
|
68
|
+
# method 'create', to: 'posts#create' # becomes posts.create
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# @return [void]
|
72
|
+
#
|
73
|
+
def method(jsonrpc_method, to:)
|
74
|
+
full_method_name = build_full_method_name(jsonrpc_method)
|
75
|
+
constraint = JSONRPC::MethodConstraint.new(full_method_name)
|
76
|
+
|
77
|
+
@mapper.post @path_prefix, {
|
78
|
+
to: to,
|
79
|
+
constraints: constraint
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
# Define a route for handling JSON-RPC batch requests
|
84
|
+
#
|
85
|
+
# @param to [String] the Rails controller action (e.g., 'batches#handle')
|
86
|
+
#
|
87
|
+
# @example Map batch requests to a controller action
|
88
|
+
# batch to: 'batches#handle'
|
89
|
+
#
|
90
|
+
# @return [void]
|
91
|
+
def batch(to:)
|
92
|
+
constraint = JSONRPC::BatchConstraint.new
|
93
|
+
|
94
|
+
@mapper.post @path_prefix, {
|
95
|
+
to: to,
|
96
|
+
constraints: constraint
|
97
|
+
}
|
98
|
+
end
|
99
|
+
|
100
|
+
# Create a namespace for grouping related JSON-RPC methods
|
101
|
+
#
|
102
|
+
# Namespaces can be nested to create hierarchical method names.
|
103
|
+
# Each level of nesting adds a dot-separated prefix to the method names.
|
104
|
+
#
|
105
|
+
# @param name [String] the namespace name
|
106
|
+
#
|
107
|
+
# @example Single-level namespace
|
108
|
+
# namespace 'posts' do
|
109
|
+
# method 'create', to: 'posts#create' # becomes posts.create
|
110
|
+
# method 'delete', to: 'posts#delete' # becomes posts.list
|
111
|
+
# end
|
112
|
+
#
|
113
|
+
# @example Nested namespaces
|
114
|
+
# namespace 'climate' do
|
115
|
+
# method 'on', to: 'climate#on' # becomes climate.on
|
116
|
+
# method 'off', to: 'climate#off' # becomes climate.off
|
117
|
+
#
|
118
|
+
# namespace 'fan' do
|
119
|
+
# method 'on', to: 'fan#on' # becomes climate.fan.on
|
120
|
+
# method 'off', to: 'fan#off' # becomes climate.fan.off
|
121
|
+
# end
|
122
|
+
# end
|
123
|
+
#
|
124
|
+
# @return [void]
|
125
|
+
#
|
126
|
+
def namespace(name, &)
|
127
|
+
@namespace_stack.push(name)
|
128
|
+
instance_eval(&)
|
129
|
+
@namespace_stack.pop
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
# Build the full method name including namespaces
|
135
|
+
#
|
136
|
+
# @param method_name [String] the base method name
|
137
|
+
# @return [String] the full method name with namespace prefixes
|
138
|
+
#
|
139
|
+
def build_full_method_name(method_name)
|
140
|
+
if @namespace_stack.any?
|
141
|
+
"#{@namespace_stack.join(".")}.#{method_name}"
|
142
|
+
else
|
143
|
+
method_name
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
data/lib/jsonrpc/railtie.rb
CHANGED
@@ -8,6 +8,13 @@ module JSONRPC
|
|
8
8
|
app.middleware.use JSONRPC::Middleware
|
9
9
|
end
|
10
10
|
|
11
|
+
# Register the JSON-RPC routes DSL extension
|
12
|
+
initializer 'jsonrpc.routes_dsl' do
|
13
|
+
ActiveSupport.on_load(:action_controller) do
|
14
|
+
ActionDispatch::Routing::Mapper.include(JSONRPC::MapperExtension)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
11
18
|
initializer 'jsonrpc.renderer' do
|
12
19
|
ActiveSupport.on_load(:action_controller) do
|
13
20
|
Mime::Type.register 'application/json', :jsonrpc
|
data/lib/jsonrpc/version.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.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Wilson Silva
|
@@ -45,7 +45,7 @@ executables: []
|
|
45
45
|
extensions: []
|
46
46
|
extra_rdoc_files: []
|
47
47
|
files:
|
48
|
-
- ".
|
48
|
+
- ".aiignore"
|
49
49
|
- ".claude/commands/document.md"
|
50
50
|
- ".claude/commands/test.md"
|
51
51
|
- ".claude/docs/yard.md"
|
@@ -80,6 +80,8 @@ files:
|
|
80
80
|
- examples/rack/README.md
|
81
81
|
- examples/rack/app.rb
|
82
82
|
- examples/rack/config.ru
|
83
|
+
- examples/rails-routing-dsl/README.md
|
84
|
+
- examples/rails-routing-dsl/config.ru
|
83
85
|
- examples/rails-single-file-routing/README.md
|
84
86
|
- examples/rails-single-file-routing/config.ru
|
85
87
|
- examples/rails-single-file/README.md
|
@@ -136,7 +138,10 @@ files:
|
|
136
138
|
- lib/jsonrpc/notification.rb
|
137
139
|
- lib/jsonrpc/parser.rb
|
138
140
|
- lib/jsonrpc/railtie.rb
|
141
|
+
- lib/jsonrpc/railtie/batch_constraint.rb
|
142
|
+
- lib/jsonrpc/railtie/mapper_extension.rb
|
139
143
|
- lib/jsonrpc/railtie/method_constraint.rb
|
144
|
+
- lib/jsonrpc/railtie/routes_dsl.rb
|
140
145
|
- lib/jsonrpc/request.rb
|
141
146
|
- lib/jsonrpc/response.rb
|
142
147
|
- lib/jsonrpc/validator.rb
|
/data/{.aiexclude → .aiignore}
RENAMED
File without changes
|