jsonrpc-middleware 0.4.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 +90 -1
- data/README.md +3 -0
- data/Rakefile +57 -0
- data/examples/README.md +9 -0
- data/examples/procedures.rb +1 -5
- 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/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 +39 -4
- data/examples/rails-single-file-routing/config.ru +18 -5
- data/examples/sinatra-classic/Gemfile.lock +3 -2
- data/examples/sinatra-modular/Gemfile.lock +3 -2
- data/lib/jsonrpc/batch_request.rb +1 -14
- data/lib/jsonrpc/batch_response.rb +1 -1
- data/lib/jsonrpc/configuration.rb +25 -3
- 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/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 +9 -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 +38 -3
- /data/{.aiexclude → .aiignore} +0 -0
@@ -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
|
@@ -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
|
@@ -17,7 +21,7 @@ end
|
|
17
21
|
|
18
22
|
class JsonrpcController < ActionController::Base
|
19
23
|
# POST /
|
20
|
-
def
|
24
|
+
def echo
|
21
25
|
render jsonrpc: jsonrpc_request.params
|
22
26
|
end
|
23
27
|
|
@@ -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
|
```
|
@@ -22,11 +22,7 @@ JSONRPC.configure do |config|
|
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
|
-
procedure(:ping)
|
26
|
-
params do
|
27
|
-
# no params
|
28
|
-
end
|
29
|
-
end
|
25
|
+
procedure(:ping)
|
30
26
|
end
|
31
27
|
|
32
28
|
# Define the application
|
@@ -41,6 +37,10 @@ class App < Rails::Application
|
|
41
37
|
config.hosts.clear
|
42
38
|
|
43
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
|
44
44
|
post '/', to: 'jsonrpc#echo', constraints: JSONRPC::MethodConstraint.new('echo')
|
45
45
|
post '/', to: 'jsonrpc#ping', constraints: JSONRPC::MethodConstraint.new('ping')
|
46
46
|
end
|
@@ -55,6 +55,19 @@ class JsonrpcController < ActionController::Base
|
|
55
55
|
def ping
|
56
56
|
render jsonrpc: 'pong'
|
57
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
|
58
71
|
end
|
59
72
|
|
60
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)
|
@@ -75,7 +75,7 @@ module JSONRPC
|
|
75
75
|
# @return [String] the JSON-formatted batch
|
76
76
|
#
|
77
77
|
def to_json(*)
|
78
|
-
|
78
|
+
MultiJson.dump(to_h, *)
|
79
79
|
end
|
80
80
|
|
81
81
|
# Implements the Enumerable contract by yielding each request in the batch
|
@@ -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
|
@@ -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
|
@@ -162,15 +175,24 @@ module JSONRPC
|
|
162
175
|
# end
|
163
176
|
# end
|
164
177
|
#
|
178
|
+
# @example Register a procedure without validation
|
179
|
+
# config.procedure('ping')
|
180
|
+
#
|
165
181
|
# @param method_name [String, Symbol] the name of the procedure
|
166
182
|
# @param allow_positional_arguments [Boolean] whether the procedure accepts positional arguments
|
167
183
|
#
|
168
|
-
# @yield A block that defines the validation contract using Dry::Validation DSL
|
184
|
+
# @yield [optional] A block that defines the validation contract using Dry::Validation DSL
|
169
185
|
#
|
170
186
|
# @return [Procedure] the registered procedure
|
171
187
|
#
|
172
|
-
def procedure(method_name, allow_positional_arguments: false, &)
|
173
|
-
contract_class =
|
188
|
+
def procedure(method_name, allow_positional_arguments: false, &block)
|
189
|
+
contract_class = if block
|
190
|
+
Class.new(Dry::Validation::Contract, &block)
|
191
|
+
else
|
192
|
+
Class.new(Dry::Validation::Contract) do
|
193
|
+
params {} # rubocop:disable Lint/EmptyBlock
|
194
|
+
end
|
195
|
+
end
|
174
196
|
contract_class.class_eval { import_predicates_as_macros }
|
175
197
|
contract = contract_class.new
|
176
198
|
|
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
|
@@ -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
|