Skip to content

Commit 0626bba

Browse files
committed
Add Rails routing DSL for JSON-RPC method mapping
Introduces a Rails routing DSL for JSON-RPC, enabling clean mapping of JSON-RPC methods (including namespaced and batch methods) to Rails controller actions. Adds supporting classes for method and batch constraints, a DSL context, and integrates the extension into Rails via the Railtie. Includes an example Rails app demonstrating the new DSL.
1 parent a828f25 commit 0626bba

File tree

8 files changed

+560
-1
lines changed

8 files changed

+560
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ A Rack middleware implementing the JSON-RPC 2.0 protocol that integrates easily
3737
- **Spec-compliant**: Fully implements the [JSON-RPC 2.0 specification](https://www.jsonrpc.org/specification)
3838
- **Rack middleware integration**: Seamlessly integrates with Rack applications (Rails, Sinatra, Hanami, etc)
3939
- **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
4041
- **Error handling**: Comprehensive error handling with standard JSON-RPC error responses
4142
- **Request validation**: Define request parameter specifications and validations
4243
- **Helpers**: Convenient helper methods to simplify request and response processing
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
```
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

lib/jsonrpc/railtie.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ class Railtie < ::Rails::Railtie
88
app.middleware.use JSONRPC::Middleware
99
end
1010

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+
1118
initializer 'jsonrpc.renderer' do
1219
ActiveSupport.on_load(:action_controller) do
1320
Mime::Type.register 'application/json', :jsonrpc
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 commit comments

Comments
 (0)