clamo 0.8.0 → 0.12.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/CHANGELOG.md +56 -0
- data/README.md +21 -43
- data/lib/clamo/jsonrpc.rb +29 -27
- data/lib/clamo/server.rb +108 -71
- data/lib/clamo/version.rb +1 -1
- metadata +2 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7596405122290bf9bce3c04760bbb5e03eef24f8e5e6b3c6e99105c8faa7d14e
|
|
4
|
+
data.tar.gz: 219e2f1f54ffd99c03dcde0c18508d24625904daa2aa65f5c7c1aabbfacee11c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7d6a2c1407df2fc1427a7c95500d07f4873660c2322bcd4ae1a4f642550e89d6a723e7854f2126404f985446432a8e59e2690873d28ff25b218e1f01e260898a
|
|
7
|
+
data.tar.gz: eb941c6f05333536695f9f3d6531de593b592653b17c6fb5a10e18c5cca693c6cb2fe3e922a25f17c5a1bf3d90fae8b518674b14fc78b7b2926deff1f3d7223b
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,62 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
+
## [0.12.0] - 2026-03-14
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Extracted `error_for` helper to centralize notification-aware error responses, replacing scattered `request.key?("id") ? error : nil` pattern.
|
|
12
|
+
- Split `validate_request_structure` into envelope validation and `validate_params_type`, making the single-request pipeline a clear 5-stage chain.
|
|
13
|
+
- Removed `method_not_found_error` and `arity_mismatch_error` one-liner wrappers (replaced by `error_for` calls).
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `server_error_response` test helper, completing the error response helper set.
|
|
18
|
+
- Test for arity-mismatch notification returning nil.
|
|
19
|
+
|
|
20
|
+
## [0.11.0] - 2026-03-14
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **Breaking:** `dispatch` is now the canonical name for `parsed_dispatch_to_object`. The old name remains as a deprecated alias.
|
|
25
|
+
- **Breaking:** `dispatch_json` is now the canonical name for `unparsed_dispatch_to_object`. The old name remains as a deprecated alias.
|
|
26
|
+
- **Breaking:** `handle_json` is now the canonical name for `handle`. The old name remains as a deprecated alias.
|
|
27
|
+
|
|
28
|
+
## [0.10.0] - 2026-03-14
|
|
29
|
+
|
|
30
|
+
### Removed
|
|
31
|
+
|
|
32
|
+
- **Breaking:** `timeout` configuration and `Timeout.timeout` wrapping — Ruby's `Timeout.timeout` is unsafe (can fire during `ensure`, IO, or lock acquisition). Callers who need timeouts should wrap dispatch externally.
|
|
33
|
+
- **Breaking:** `before_dispatch` and `after_dispatch` hooks — callers can wrap `dispatch` calls directly for the same effect with less coupling.
|
|
34
|
+
- `parallel` as a runtime dependency — it is now loaded on demand. Batch requests fall back to sequential `map` when the gem is not installed.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- **Arity validation** — parameter count and keyword names are checked against the Ruby method signature before dispatch. Mismatches return `-32602 Invalid params` instead of the previous `-32603 Internal error`.
|
|
39
|
+
- `Clamo::Server.dispatch` — alias for `parsed_dispatch_to_object`.
|
|
40
|
+
- `Clamo::Server.dispatch_json` — alias for `unparsed_dispatch_to_object`.
|
|
41
|
+
- Concurrency tests for thread-safe dispatch.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- `Config` simplified to `Data.define(:on_error)` (was `Data.define(:timeout, :on_error, :before_dispatch, :after_dispatch)`).
|
|
46
|
+
- `parsed_dispatch_to_object` signature reduced to `(request:, object:, on_error:, **opts)`.
|
|
47
|
+
|
|
48
|
+
## [0.9.0] - 2026-03-14
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
|
|
52
|
+
- **Breaking:** All response and request builder hashes now use string keys (`"jsonrpc"`, `"result"`, `"id"`, `"error"`) instead of symbol keys. This makes the entire pipeline consistent — `JSON.parse` produces string keys, `normalize_request_keys` uses string keys, and now responses match. Callers using the lower-level `parsed_dispatch_to_object` or `unparsed_dispatch_to_object` must update hash access from `response[:result]` to `response["result"]`. The `handle` method (JSON string in/out) is unaffected.
|
|
53
|
+
- `after_dispatch` hook now receives the actual return value for notifications. Previously always passed `nil`; now passes the value returned by the dispatched method.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- **Security:** `method_known?` no longer exposes inherited `Module` methods (`define_method`, `class_eval`, `const_set`, `freeze`, `include`, and 55 others) on module-based service objects. The previous `public_methods(false)` implementation included these; the new `public_method_defined?` check restricts dispatch to methods explicitly defined on the service object.
|
|
58
|
+
|
|
59
|
+
### Internal
|
|
60
|
+
|
|
61
|
+
- `method_known?` replaced array-allocating `public_methods(false).map(&:to_sym).include?` with zero-allocation `public_method_defined?` lookups. Handles both module targets (singleton class) and class instance targets (class + singleton class).
|
|
62
|
+
|
|
7
63
|
## [0.8.0] - 2026-03-14
|
|
8
64
|
|
|
9
65
|
### Added
|
data/README.md
CHANGED
|
@@ -28,30 +28,32 @@ end
|
|
|
28
28
|
|
|
29
29
|
# JSON string in, JSON string out — the primary entry point for HTTP/socket integrations.
|
|
30
30
|
# Returns nil for notifications (no response expected).
|
|
31
|
-
json_response = Clamo::Server.
|
|
31
|
+
json_response = Clamo::Server.handle_json(
|
|
32
32
|
request: '{"jsonrpc": "2.0", "method": "add", "params": [1, 2], "id": 1}',
|
|
33
33
|
object: MyService
|
|
34
34
|
)
|
|
35
35
|
# => '{"jsonrpc":"2.0","result":3,"id":1}'
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
If you need the parsed hash instead of a JSON string, use the lower-level methods:
|
|
38
|
+
If you need the parsed hash instead of a JSON string, use the lower-level methods directly or via their shorter aliases:
|
|
39
39
|
|
|
40
40
|
```ruby
|
|
41
41
|
# From a JSON string
|
|
42
|
-
response = Clamo::Server.
|
|
42
|
+
response = Clamo::Server.dispatch_json(
|
|
43
43
|
request: '{"jsonrpc": "2.0", "method": "add", "params": [1, 2], "id": 1}',
|
|
44
44
|
object: MyService
|
|
45
45
|
)
|
|
46
|
-
# => {jsonrpc
|
|
46
|
+
# => {"jsonrpc" => "2.0", "result" => 3, "id" => 1}
|
|
47
47
|
|
|
48
48
|
# From a pre-parsed hash
|
|
49
|
-
response = Clamo::Server.
|
|
49
|
+
response = Clamo::Server.dispatch(
|
|
50
50
|
request: { "jsonrpc" => "2.0", "method" => "add", "params" => [1, 2], "id" => 1 },
|
|
51
51
|
object: MyService
|
|
52
52
|
)
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
The longer names `parsed_dispatch_to_object`, `unparsed_dispatch_to_object`, and `handle` still work as deprecated aliases.
|
|
56
|
+
|
|
55
57
|
### Handling Different Parameter Types
|
|
56
58
|
|
|
57
59
|
Clamo supports both positional (array) and named (object/hash) parameters:
|
|
@@ -76,13 +78,13 @@ batch_request = <<~JSON
|
|
|
76
78
|
]
|
|
77
79
|
JSON
|
|
78
80
|
|
|
79
|
-
batch_response = Clamo::Server.
|
|
81
|
+
batch_response = Clamo::Server.dispatch_json(
|
|
80
82
|
request: batch_request,
|
|
81
83
|
object: MyService
|
|
82
84
|
)
|
|
83
85
|
|
|
84
86
|
puts batch_response
|
|
85
|
-
# => [{jsonrpc
|
|
87
|
+
# => [{"jsonrpc" => "2.0", "result" => 3, "id" => 1}, {"jsonrpc" => "2.0", "result" => 2, "id" => 2}]
|
|
86
88
|
```
|
|
87
89
|
|
|
88
90
|
### Notifications
|
|
@@ -91,7 +93,7 @@ Notifications are requests without an ID field. They don't produce a response:
|
|
|
91
93
|
|
|
92
94
|
```ruby
|
|
93
95
|
notification = '{"jsonrpc": "2.0", "method": "add", "params": [1, 2]}'
|
|
94
|
-
response = Clamo::Server.
|
|
96
|
+
response = Clamo::Server.dispatch_json(
|
|
95
97
|
request: notification,
|
|
96
98
|
object: MyService
|
|
97
99
|
)
|
|
@@ -112,7 +114,7 @@ request = Clamo::JSONRPC.build_request(
|
|
|
112
114
|
)
|
|
113
115
|
|
|
114
116
|
puts request
|
|
115
|
-
# => {jsonrpc
|
|
117
|
+
# => {"jsonrpc" => "2.0", "method" => "add", "params" => [1, 2], "id" => 1}
|
|
116
118
|
```
|
|
117
119
|
|
|
118
120
|
## Error Handling
|
|
@@ -124,24 +126,17 @@ Clamo follows the JSON-RPC 2.0 specification for error handling:
|
|
|
124
126
|
| -32700 | Parse error | Invalid JSON was received |
|
|
125
127
|
| -32600 | Invalid request | The JSON sent is not a valid Request object |
|
|
126
128
|
| -32601 | Method not found | The method does not exist / is not available |
|
|
127
|
-
| -32602 | Invalid params | Invalid method parameter(s)
|
|
129
|
+
| -32602 | Invalid params | Invalid method parameter(s) or arity mismatch |
|
|
128
130
|
| -32603 | Internal error | Internal JSON-RPC error |
|
|
129
|
-
| -32000 | Server error |
|
|
130
|
-
|
|
131
|
-
## Configuration
|
|
132
|
-
|
|
133
|
-
### Timeout
|
|
131
|
+
| -32000 | Server error | Exception raised by dispatched method |
|
|
134
132
|
|
|
135
|
-
|
|
133
|
+
Parameter arity is validated before dispatch. If the number of positional arguments or keyword arguments doesn't match the Ruby method signature, a `-32602 Invalid params` error is returned.
|
|
136
134
|
|
|
137
|
-
|
|
138
|
-
Clamo::Server.timeout = 10 # seconds
|
|
139
|
-
Clamo::Server.timeout = nil # disable timeout
|
|
140
|
-
```
|
|
135
|
+
## Configuration
|
|
141
136
|
|
|
142
137
|
### Error Callback
|
|
143
138
|
|
|
144
|
-
Errors during dispatch are reported through `on_error
|
|
139
|
+
Errors during dispatch are reported through `on_error`, which is called for both requests and notifications. Notifications are silent by default (no response is sent to the client, but `on_error` still fires); requests return a generic `-32000 Server error` without leaking exception details. Use `on_error` to capture the full exception for logging:
|
|
145
140
|
|
|
146
141
|
```ruby
|
|
147
142
|
Clamo::Server.on_error = ->(exception, method, params) {
|
|
@@ -149,32 +144,15 @@ Clamo::Server.on_error = ->(exception, method, params) {
|
|
|
149
144
|
}
|
|
150
145
|
```
|
|
151
146
|
|
|
152
|
-
### Dispatch Hooks
|
|
153
|
-
|
|
154
|
-
`before_dispatch` and `after_dispatch` run around every method call (requests and notifications). Raise in `before_dispatch` to halt execution:
|
|
155
|
-
|
|
156
|
-
```ruby
|
|
157
|
-
Clamo::Server.before_dispatch = ->(method, params) {
|
|
158
|
-
raise "unauthorized" unless allowed?(method)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
Clamo::Server.after_dispatch = ->(method, params, result) {
|
|
162
|
-
Rails.logger.info("#{method} completed")
|
|
163
|
-
}
|
|
164
|
-
```
|
|
165
|
-
|
|
166
147
|
### Per-Call Configuration
|
|
167
148
|
|
|
168
|
-
|
|
149
|
+
Configuration can be overridden per-call. Module-level settings serve as defaults:
|
|
169
150
|
|
|
170
151
|
```ruby
|
|
171
|
-
Clamo::Server.
|
|
152
|
+
Clamo::Server.handle_json(
|
|
172
153
|
request: body,
|
|
173
154
|
object: MyService,
|
|
174
|
-
|
|
175
|
-
on_error: ->(e, method, params) { MyLogger.error(e) },
|
|
176
|
-
before_dispatch: ->(method, params) { authorize!(method) },
|
|
177
|
-
after_dispatch: ->(method, params, result) { track(method) }
|
|
155
|
+
on_error: ->(e, method, params) { MyLogger.error(e) }
|
|
178
156
|
)
|
|
179
157
|
```
|
|
180
158
|
|
|
@@ -184,10 +162,10 @@ Per-call config is snapshotted at the start of each dispatch, so concurrent muta
|
|
|
184
162
|
|
|
185
163
|
### Parallel Processing
|
|
186
164
|
|
|
187
|
-
Batch requests are processed in parallel
|
|
165
|
+
Batch requests are processed in parallel when the [parallel](https://github.com/grosser/parallel) gem is available. If `parallel` is not installed, batches fall back to sequential processing. You can pass options to `Parallel.map`:
|
|
188
166
|
|
|
189
167
|
```ruby
|
|
190
|
-
Clamo::Server.
|
|
168
|
+
Clamo::Server.dispatch(
|
|
191
169
|
request: batch_request,
|
|
192
170
|
object: MyService,
|
|
193
171
|
in_processes: 4 # Parallel processing option
|
data/lib/clamo/jsonrpc.rb
CHANGED
|
@@ -14,47 +14,31 @@ module Clamo
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
class << self
|
|
17
|
-
def proper_params_if_any?(request)
|
|
18
|
-
if key_indifferent?(request, "params")
|
|
19
|
-
params = fetch_indifferent(request, "params")
|
|
20
|
-
params.is_a?(Array) || params.is_a?(Hash)
|
|
21
|
-
else
|
|
22
|
-
true
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def valid_request?(request)
|
|
27
|
-
request.is_a?(Hash) &&
|
|
28
|
-
proper_pragma?(request) &&
|
|
29
|
-
proper_method?(request) &&
|
|
30
|
-
proper_id_if_any?(request)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
17
|
def build_request(**opts)
|
|
34
18
|
raise ArgumentError, "method is required" unless opts.key?(:method)
|
|
35
19
|
|
|
36
20
|
validate_params_type!(opts[:params]) if opts.key?(:params)
|
|
37
21
|
|
|
38
|
-
{ jsonrpc
|
|
39
|
-
.then { |r| opts.key?(:params) ? r.merge(params
|
|
40
|
-
.then { |r| opts.key?(:id) ? r.merge(id
|
|
22
|
+
{ "jsonrpc" => "2.0", "method" => opts[:method] }
|
|
23
|
+
.then { |r| opts.key?(:params) ? r.merge("params" => opts[:params]) : r }
|
|
24
|
+
.then { |r| opts.key?(:id) ? r.merge("id" => opts[:id]) : r }
|
|
41
25
|
end
|
|
42
26
|
|
|
43
27
|
def build_result_response(id:, result:)
|
|
44
|
-
{ jsonrpc
|
|
28
|
+
{ "jsonrpc" => "2.0", "result" => result, "id" => id }
|
|
45
29
|
end
|
|
46
30
|
|
|
47
31
|
def build_error_response(**opts)
|
|
48
32
|
raise ArgumentError, "error code is required" unless opts.dig(:error, :code)
|
|
49
33
|
raise ArgumentError, "error message is required" unless opts.dig(:error, :message)
|
|
50
34
|
|
|
51
|
-
{ jsonrpc
|
|
52
|
-
id
|
|
53
|
-
error
|
|
54
|
-
code
|
|
55
|
-
message
|
|
56
|
-
data
|
|
57
|
-
}.reject { |k, _| k ==
|
|
35
|
+
{ "jsonrpc" => "2.0",
|
|
36
|
+
"id" => opts[:id],
|
|
37
|
+
"error" => {
|
|
38
|
+
"code" => opts.dig(:error, :code),
|
|
39
|
+
"message" => opts.dig(:error, :message),
|
|
40
|
+
"data" => opts.dig(:error, :data)
|
|
41
|
+
}.reject { |k, _| k == "data" && !opts[:error].key?(:data) } }
|
|
58
42
|
end
|
|
59
43
|
|
|
60
44
|
def build_error_response_from(descriptor:, id: nil)
|
|
@@ -77,6 +61,24 @@ module Clamo
|
|
|
77
61
|
)
|
|
78
62
|
end
|
|
79
63
|
|
|
64
|
+
# Used internally by Clamo::Server — not part of the public API.
|
|
65
|
+
def proper_params_if_any?(request)
|
|
66
|
+
if key_indifferent?(request, "params")
|
|
67
|
+
params = fetch_indifferent(request, "params")
|
|
68
|
+
params.is_a?(Array) || params.is_a?(Hash)
|
|
69
|
+
else
|
|
70
|
+
true
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Used internally by Clamo::Server — not part of the public API.
|
|
75
|
+
def valid_request?(request)
|
|
76
|
+
request.is_a?(Hash) &&
|
|
77
|
+
proper_pragma?(request) &&
|
|
78
|
+
proper_method?(request) &&
|
|
79
|
+
proper_id_if_any?(request)
|
|
80
|
+
end
|
|
81
|
+
|
|
80
82
|
private
|
|
81
83
|
|
|
82
84
|
def proper_pragma?(request)
|
data/lib/clamo/server.rb
CHANGED
|
@@ -1,40 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
-
require "parallel"
|
|
5
|
-
require "timeout"
|
|
6
4
|
|
|
7
5
|
module Clamo
|
|
8
6
|
module Server
|
|
9
|
-
Config = Data.define(:
|
|
7
|
+
Config = Data.define(:on_error)
|
|
10
8
|
|
|
11
9
|
class << self
|
|
12
|
-
|
|
13
|
-
# dispatch call, so mutations mid-request do not affect in-flight work.
|
|
14
|
-
# All four can be overridden per-call via keyword arguments.
|
|
15
|
-
attr_accessor :on_error, :before_dispatch, :after_dispatch
|
|
16
|
-
attr_writer :timeout
|
|
17
|
-
|
|
18
|
-
def timeout
|
|
19
|
-
return @timeout if defined?(@timeout)
|
|
20
|
-
|
|
21
|
-
30
|
|
22
|
-
end
|
|
10
|
+
attr_accessor :on_error
|
|
23
11
|
|
|
24
12
|
# JSON string in, JSON string out. Full round-trip for HTTP/socket integrations.
|
|
25
13
|
#
|
|
26
|
-
# Clamo::Server.
|
|
14
|
+
# Clamo::Server.handle_json(request: body, object: MyService)
|
|
27
15
|
#
|
|
28
|
-
def
|
|
29
|
-
response =
|
|
16
|
+
def handle_json(request:, object:, **)
|
|
17
|
+
response = dispatch_json(request: request, object: object, **)
|
|
30
18
|
response&.to_json
|
|
31
19
|
end
|
|
32
20
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
|
|
21
|
+
alias handle handle_json
|
|
22
|
+
|
|
23
|
+
# JSON string in, parsed response out.
|
|
24
|
+
#
|
|
25
|
+
# Clamo::Server.dispatch_json(request: json_string, object: MyModule)
|
|
26
|
+
#
|
|
27
|
+
def dispatch_json(request:, object:, **)
|
|
38
28
|
raise ArgumentError, "object is required" unless object
|
|
39
29
|
|
|
40
30
|
begin
|
|
@@ -43,26 +33,30 @@ module Clamo
|
|
|
43
33
|
return JSONRPC.build_error_response_parse_error
|
|
44
34
|
end
|
|
45
35
|
|
|
46
|
-
|
|
36
|
+
dispatch(request: parsed, object: object, **)
|
|
47
37
|
end
|
|
48
38
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
39
|
+
alias unparsed_dispatch_to_object dispatch_json
|
|
40
|
+
|
|
41
|
+
# Parsed hash in, parsed response out.
|
|
42
|
+
#
|
|
43
|
+
# Clamo::Server.dispatch(request: hash_or_array, object: MyModule)
|
|
44
|
+
#
|
|
45
|
+
def dispatch(request:, object:,
|
|
46
|
+
on_error: self.on_error,
|
|
47
|
+
**opts)
|
|
55
48
|
raise ArgumentError, "object is required" unless object
|
|
56
49
|
|
|
57
50
|
request = normalize_request_keys(request)
|
|
58
|
-
config = Config.new(
|
|
59
|
-
before_dispatch: before_dispatch, after_dispatch: after_dispatch)
|
|
51
|
+
config = Config.new(on_error: on_error)
|
|
60
52
|
|
|
61
53
|
response_for(request: request, object: object, config: config, **opts) do |method, params|
|
|
62
54
|
dispatch_to_ruby(object: object, method: method, params: params)
|
|
63
55
|
end
|
|
64
56
|
end
|
|
65
57
|
|
|
58
|
+
alias parsed_dispatch_to_object dispatch
|
|
59
|
+
|
|
66
60
|
private
|
|
67
61
|
|
|
68
62
|
def normalize_request_keys(request)
|
|
@@ -74,7 +68,13 @@ module Clamo
|
|
|
74
68
|
end
|
|
75
69
|
|
|
76
70
|
def method_known?(object:, method:)
|
|
77
|
-
|
|
71
|
+
name = method.to_sym
|
|
72
|
+
if object.is_a?(Module)
|
|
73
|
+
object.singleton_class.public_method_defined?(name, false)
|
|
74
|
+
else
|
|
75
|
+
object.class.public_method_defined?(name, false) ||
|
|
76
|
+
object.singleton_class.public_method_defined?(name, false)
|
|
77
|
+
end
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
def dispatch_to_ruby(object:, method:, params:)
|
|
@@ -103,8 +103,15 @@ module Clamo
|
|
|
103
103
|
error = validate_request_structure(request)
|
|
104
104
|
return error if error
|
|
105
105
|
|
|
106
|
+
error = validate_params_type(request)
|
|
107
|
+
return error if error
|
|
108
|
+
|
|
106
109
|
unless method_known?(object: object, method: request["method"])
|
|
107
|
-
return request
|
|
110
|
+
return error_for(request, JSONRPC::ProtocolErrors::METHOD_NOT_FOUND)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
unless params_match_arity?(object: object, method: request["method"], params: request["params"])
|
|
114
|
+
return error_for(request, JSONRPC::ProtocolErrors::INVALID_PARAMS)
|
|
108
115
|
end
|
|
109
116
|
|
|
110
117
|
return dispatch_notification(request, block, config) unless request.key?("id")
|
|
@@ -122,38 +129,44 @@ module Clamo
|
|
|
122
129
|
return result ? [result] : nil
|
|
123
130
|
end
|
|
124
131
|
|
|
125
|
-
result =
|
|
132
|
+
result = map_batch(request, **opts) do |item|
|
|
126
133
|
response_for_single_request(request: item, object: object, block: block, config: config)
|
|
127
134
|
end.compact
|
|
128
135
|
result.empty? ? nil : result
|
|
129
136
|
end
|
|
130
137
|
|
|
131
|
-
def
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
end
|
|
138
|
+
def map_batch(items, **, &)
|
|
139
|
+
require "parallel"
|
|
140
|
+
Parallel.map(items, **, &)
|
|
141
|
+
rescue LoadError
|
|
142
|
+
items.map(&)
|
|
143
|
+
end
|
|
138
144
|
|
|
139
|
-
|
|
145
|
+
def error_for(request, descriptor)
|
|
146
|
+
return unless request.key?("id")
|
|
140
147
|
|
|
141
|
-
|
|
142
|
-
|
|
148
|
+
JSONRPC.build_error_response_from(id: request["id"], descriptor: descriptor)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def validate_request_structure(request)
|
|
152
|
+
return if JSONRPC.valid_request?(request)
|
|
143
153
|
|
|
144
|
-
JSONRPC.build_error_response_from(
|
|
154
|
+
JSONRPC.build_error_response_from(
|
|
155
|
+
id: request.is_a?(Hash) ? request["id"] : nil,
|
|
156
|
+
descriptor: JSONRPC::ProtocolErrors::INVALID_REQUEST
|
|
157
|
+
)
|
|
145
158
|
end
|
|
146
159
|
|
|
147
|
-
def
|
|
148
|
-
JSONRPC.
|
|
160
|
+
def validate_params_type(request)
|
|
161
|
+
return if JSONRPC.proper_params_if_any?(request)
|
|
162
|
+
|
|
163
|
+
error_for(request, JSONRPC::ProtocolErrors::INVALID_PARAMS)
|
|
149
164
|
end
|
|
150
165
|
|
|
151
166
|
def dispatch_notification(request, block, config)
|
|
152
167
|
method = request["method"]
|
|
153
168
|
params = request["params"]
|
|
154
|
-
|
|
155
|
-
with_timeout(config.timeout) { block.yield(method, params) }
|
|
156
|
-
config.after_dispatch&.call(method, params, nil)
|
|
169
|
+
block.yield(method, params)
|
|
157
170
|
nil
|
|
158
171
|
rescue StandardError => e
|
|
159
172
|
config.on_error&.call(e, method, params)
|
|
@@ -163,35 +176,59 @@ module Clamo
|
|
|
163
176
|
def dispatch_request(request, block, config)
|
|
164
177
|
method = request["method"]
|
|
165
178
|
params = request["params"]
|
|
166
|
-
|
|
167
|
-
result = with_timeout(config.timeout) { block.yield(method, params) }
|
|
168
|
-
config.after_dispatch&.call(method, params, result)
|
|
179
|
+
result = block.yield(method, params)
|
|
169
180
|
JSONRPC.build_result_response(id: request["id"], result: result)
|
|
170
|
-
rescue Timeout::Error
|
|
171
|
-
timeout_error(request)
|
|
172
181
|
rescue StandardError => e
|
|
173
182
|
config.on_error&.call(e, method, params)
|
|
174
|
-
JSONRPC.build_error_response_from(id: request["id"], descriptor: JSONRPC::ProtocolErrors::
|
|
183
|
+
JSONRPC.build_error_response_from(id: request["id"], descriptor: JSONRPC::ProtocolErrors::SERVER_ERROR)
|
|
175
184
|
end
|
|
176
185
|
|
|
177
|
-
def
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
error: {
|
|
181
|
-
code: JSONRPC::ProtocolErrors::SERVER_ERROR.code,
|
|
182
|
-
message: JSONRPC::ProtocolErrors::SERVER_ERROR.message,
|
|
183
|
-
data: "Request timed out"
|
|
184
|
-
}
|
|
185
|
-
)
|
|
186
|
-
end
|
|
186
|
+
def params_match_arity?(object:, method:, params:)
|
|
187
|
+
ruby_method = resolve_method(object, method)
|
|
188
|
+
return true unless ruby_method
|
|
187
189
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
190
|
+
parameters = ruby_method.parameters
|
|
191
|
+
|
|
192
|
+
case params
|
|
193
|
+
when Array then array_params_match?(parameters, params.size)
|
|
194
|
+
when Hash then hash_params_match?(parameters, params.keys.map(&:to_sym))
|
|
195
|
+
when nil then nil_params_match?(parameters)
|
|
196
|
+
else true
|
|
193
197
|
end
|
|
194
198
|
end
|
|
199
|
+
|
|
200
|
+
def resolve_method(object, method_name)
|
|
201
|
+
object.method(method_name.to_sym)
|
|
202
|
+
rescue NameError
|
|
203
|
+
nil
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def array_params_match?(parameters, count)
|
|
207
|
+
by_type = parameters.group_by(&:first)
|
|
208
|
+
|
|
209
|
+
return false if by_type.key?(:keyreq)
|
|
210
|
+
return true if by_type.key?(:rest)
|
|
211
|
+
|
|
212
|
+
required = by_type.fetch(:req, []).size
|
|
213
|
+
count.between?(required, required + by_type.fetch(:opt, []).size)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def hash_params_match?(parameters, keys)
|
|
217
|
+
by_type = parameters.group_by(&:first)
|
|
218
|
+
required = by_type.fetch(:keyreq, []).map(&:last)
|
|
219
|
+
allowed = required + by_type.fetch(:key, []).map(&:last)
|
|
220
|
+
|
|
221
|
+
return false if by_type.key?(:req)
|
|
222
|
+
return false unless (required - keys).empty?
|
|
223
|
+
|
|
224
|
+
by_type.key?(:keyrest) || (keys - allowed).empty?
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def nil_params_match?(parameters)
|
|
228
|
+
by_type = parameters.group_by(&:first)
|
|
229
|
+
|
|
230
|
+
by_type.fetch(:req, []).empty? && !by_type.key?(:keyreq)
|
|
231
|
+
end
|
|
195
232
|
end
|
|
196
233
|
end
|
|
197
234
|
end
|
data/lib/clamo/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,28 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: clamo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.12.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andriy Tyurnikov
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
12
|
-
- !ruby/object:Gem::Dependency
|
|
13
|
-
name: parallel
|
|
14
|
-
requirement: !ruby/object:Gem::Requirement
|
|
15
|
-
requirements:
|
|
16
|
-
- - "~>"
|
|
17
|
-
- !ruby/object:Gem::Version
|
|
18
|
-
version: '1.27'
|
|
19
|
-
type: :runtime
|
|
20
|
-
prerelease: false
|
|
21
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
-
requirements:
|
|
23
|
-
- - "~>"
|
|
24
|
-
- !ruby/object:Gem::Version
|
|
25
|
-
version: '1.27'
|
|
11
|
+
dependencies: []
|
|
26
12
|
description: Minimal, spec-compliant JSON-RPC 2.0 server for Ruby with request validation,
|
|
27
13
|
method dispatch, batch processing, and notification support.
|
|
28
14
|
email:
|