jsonrpc-middleware 0.3.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.
@@ -45,8 +45,5 @@ def handle_single(request_or_notification)
45
45
  end
46
46
 
47
47
  def handle_batch(batch)
48
- batch.flat_map do |request_or_notification|
49
- result = handle_single(request_or_notification)
50
- JSONRPC::Response.new(id: request_or_notification.id, result:) if request_or_notification.is_a?(JSONRPC::Request)
51
- end.compact
48
+ batch.process_each { |request_or_notification| handle_single(request_or_notification) }
52
49
  end
@@ -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
 
@@ -49,9 +49,6 @@ class App < Sinatra::Base
49
49
  end
50
50
 
51
51
  def handle_batch(batch)
52
- batch.flat_map do |request_or_notification|
53
- result = handle_single(request_or_notification)
54
- JSONRPC::Response.new(id: request_or_notification.id, result:) if request_or_notification.is_a?(JSONRPC::Request)
55
- end.compact
52
+ batch.process_each { |request_or_notification| handle_single(request_or_notification) }
56
53
  end
57
54
  end
@@ -124,17 +124,36 @@ module JSONRPC
124
124
  #
125
125
  alias length size
126
126
 
127
- # Returns true if the batch contains no requests
127
+ # Handles each request/notification in the batch and returns responses
128
128
  #
129
129
  # @api public
130
130
  #
131
- # @example Check if batch is empty
132
- # batch.empty? # => false
131
+ # @example Handle batch with a block
132
+ # batch.process_each do |request_or_notification|
133
+ # # Process the request/notification
134
+ # result = some_processing(request_or_notification.params)
135
+ # result
136
+ # end
133
137
  #
134
- # @return [Boolean] true if the batch is empty, false otherwise
138
+ # @yield [request_or_notification] Yields each request/notification in the batch
135
139
  #
136
- def empty?
137
- requests.empty?
140
+ # @yieldparam request_or_notification [JSONRPC::Request, JSONRPC::Notification] a request or notification
141
+ # in the batch
142
+ #
143
+ # @yieldreturn [Object] the result of processing the request. Notifications yield no results.
144
+ #
145
+ # @return [Array<JSONRPC::Response>] responses for requests only (notifications return no response)
146
+ #
147
+ def process_each
148
+ raise ArgumentError, 'Block required' unless block_given?
149
+
150
+ flat_map do |request_or_notification|
151
+ result = yield(request_or_notification)
152
+
153
+ if request_or_notification.is_a?(JSONRPC::Request)
154
+ JSONRPC::Response.new(id: request_or_notification.id, result:)
155
+ end
156
+ end.compact
138
157
  end
139
158
 
140
159
  private
@@ -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.3.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.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wilson Silva
@@ -37,16 +37,17 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '2.7'
40
- description: Implements the JSON-RPC 2.0 protocol, enabling standardized remote procedure
41
- calls encoded in JSON.
40
+ description: A Rack middleware implementing the JSON-RPC 2.0 protocol that integrates
41
+ easily with all Rack-based applications (Rails, Sinatra, Hanami, etc).
42
42
  email:
43
43
  - wilson.dsigns@gmail.com
44
44
  executables: []
45
45
  extensions: []
46
46
  extra_rdoc_files: []
47
47
  files:
48
- - ".aiexclude"
48
+ - ".aiignore"
49
49
  - ".claude/commands/document.md"
50
+ - ".claude/commands/test.md"
50
51
  - ".claude/docs/yard.md"
51
52
  - ".claude/settings.local.json"
52
53
  - ".editorconfig"
@@ -79,6 +80,8 @@ files:
79
80
  - examples/rack/README.md
80
81
  - examples/rack/app.rb
81
82
  - examples/rack/config.ru
83
+ - examples/rails-routing-dsl/README.md
84
+ - examples/rails-routing-dsl/config.ru
82
85
  - examples/rails-single-file-routing/README.md
83
86
  - examples/rails-single-file-routing/config.ru
84
87
  - examples/rails-single-file/README.md
@@ -135,7 +138,10 @@ files:
135
138
  - lib/jsonrpc/notification.rb
136
139
  - lib/jsonrpc/parser.rb
137
140
  - lib/jsonrpc/railtie.rb
141
+ - lib/jsonrpc/railtie/batch_constraint.rb
142
+ - lib/jsonrpc/railtie/mapper_extension.rb
138
143
  - lib/jsonrpc/railtie/method_constraint.rb
144
+ - lib/jsonrpc/railtie/routes_dsl.rb
139
145
  - lib/jsonrpc/request.rb
140
146
  - lib/jsonrpc/response.rb
141
147
  - lib/jsonrpc/validator.rb
@@ -167,5 +173,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
167
173
  requirements: []
168
174
  rubygems_version: 3.7.0
169
175
  specification_version: 4
170
- summary: Implementation of the JSON-RPC protocol.
176
+ summary: Rack middleware implementing the JSON-RPC 2.0 protocol.
171
177
  test_files: []
File without changes