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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a45b1d353d7fd1547e7e984edba9c9ed1e02669914c00294d08f259cb5bb76e6
4
- data.tar.gz: 25d2b7802a03378c90e181d5e5b580489299d41a3070817d762b43063a9d68b3
3
+ metadata.gz: 5d8775dfa68b34e4513883b9bdbfb565a708902efe694727bb19d517d4539cd7
4
+ data.tar.gz: 8f087e14952785c53470895871bb175e0e48fd9aab560b9906cd17b509d6f4e2
5
5
  SHA512:
6
- metadata.gz: c3a417f6096c195940c782cb313f9b4e7a5e6ef2c1c6ed994a4d23e226e7a70ba7df4b635b03c56f392983b45b1d0239f0a121c2b2982101c5ad5c47680bd7f8
7
- data.tar.gz: 601bc95a7d965e6721af683765881d73ac34fc5ced0791c398b1ddc4acff054b586d0c74ab0107bf139bb177afcebc760460d96cb86896afec1394972b98b6d9
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
@@ -52,9 +52,5 @@ JSONRPC.configure do |config|
52
52
  end
53
53
 
54
54
  # Used only to test internal server errors
55
- procedure(:explode) do
56
- params do
57
- # No params
58
- end
59
- end
55
+ procedure(:explode)
60
56
  end
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../..
3
3
  specs:
4
- jsonrpc-middleware (0.3.0)
4
+ jsonrpc-middleware (0.4.0)
5
5
  dry-validation (~> 1.11)
6
6
  zeitwerk (~> 2.7)
7
7
 
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../..
3
3
  specs:
4
- jsonrpc-middleware (0.2.0)
4
+ jsonrpc-middleware (0.4.0)
5
5
  dry-validation (~> 1.11)
6
6
  zeitwerk (~> 2.7)
7
7
 
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../..
3
3
  specs:
4
- jsonrpc-middleware (0.2.0)
4
+ jsonrpc-middleware (0.4.0)
5
5
  dry-validation (~> 1.11)
6
6
  zeitwerk (~> 2.7)
7
7
 
@@ -54,9 +54,5 @@ require_relative '../../../procedures'
54
54
  # end
55
55
  #
56
56
  # # Used only to test internal server errors
57
- # procedure(:explode) do
58
- # params do
59
- # # No params
60
- # end
61
- # end
57
+ # procedure(:explode)
62
58
  # end
@@ -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
@@ -17,7 +17,7 @@ end
17
17
 
18
18
  class JsonrpcController < ActionController::Base
19
19
  # POST /
20
- def echoc
20
+ def echo
21
21
  render jsonrpc: jsonrpc_request.params
22
22
  end
23
23
 
@@ -22,11 +22,7 @@ JSONRPC.configure do |config|
22
22
  end
23
23
  end
24
24
 
25
- procedure(:ping) do
26
- params do
27
- # no params
28
- end
29
- end
25
+ procedure(:ping)
30
26
  end
31
27
 
32
28
  # Define the application
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../..
3
3
  specs:
4
- jsonrpc-middleware (0.2.0)
4
+ jsonrpc-middleware (0.4.0)
5
5
  dry-validation (~> 1.11)
6
6
  zeitwerk (~> 2.7)
7
7
 
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../..
3
3
  specs:
4
- jsonrpc-middleware (0.2.0)
4
+ jsonrpc-middleware (0.4.0)
5
5
  dry-validation (~> 1.11)
6
6
  zeitwerk (~> 2.7)
7
7
 
@@ -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 = Class.new(Dry::Validation::Contract, &)
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
@@ -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
@@ -11,5 +11,5 @@ module JSONRPC
11
11
  #
12
12
  # @return [String] The current version number
13
13
  #
14
- VERSION = '0.4.0'
14
+ VERSION = '0.5.0'
15
15
  end
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.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
- - ".aiexclude"
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
File without changes