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.
- checksums.yaml +4 -4
- data/.claude/commands/test.md +561 -0
- data/.claude/settings.local.json +2 -1
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +71 -2
- data/README.md +49 -18
- data/Rakefile +57 -3
- data/examples/procedures.rb +1 -5
- data/examples/rack/Gemfile.lock +1 -1
- data/examples/rack/app.rb +1 -4
- data/examples/rack-echo/Gemfile.lock +1 -1
- data/examples/rack-echo/app.rb +1 -4
- data/examples/rack-single-file/config.ru +1 -4
- data/examples/rails/Gemfile.lock +1 -1
- data/examples/rails/app/controllers/jsonrpc_controller.rb +1 -4
- 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/config.ru +1 -4
- 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-classic/app.rb +1 -4
- data/examples/sinatra-modular/Gemfile.lock +1 -1
- data/examples/sinatra-modular/app.rb +1 -4
- data/lib/jsonrpc/batch_request.rb +25 -6
- 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 +11 -5
- /data/{.aiexclude → .aiignore} +0 -0
@@ -45,8 +45,5 @@ def handle_single(request_or_notification)
|
|
45
45
|
end
|
46
46
|
|
47
47
|
def handle_batch(batch)
|
48
|
-
batch.
|
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
|
@@ -49,9 +49,6 @@ class App < Sinatra::Base
|
|
49
49
|
end
|
50
50
|
|
51
51
|
def handle_batch(batch)
|
52
|
-
batch.
|
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
|
-
#
|
127
|
+
# Handles each request/notification in the batch and returns responses
|
128
128
|
#
|
129
129
|
# @api public
|
130
130
|
#
|
131
|
-
# @example
|
132
|
-
# batch.
|
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
|
-
# @
|
138
|
+
# @yield [request_or_notification] Yields each request/notification in the batch
|
135
139
|
#
|
136
|
-
|
137
|
-
|
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 =
|
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
|
@@ -37,16 +37,17 @@ dependencies:
|
|
37
37
|
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
39
|
version: '2.7'
|
40
|
-
description:
|
41
|
-
|
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
|
-
- ".
|
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:
|
176
|
+
summary: Rack middleware implementing the JSON-RPC 2.0 protocol.
|
171
177
|
test_files: []
|
/data/{.aiexclude → .aiignore}
RENAMED
File without changes
|