clamo 0.7.0 → 0.8.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/.rubocop.yml +3 -0
- data/CHANGELOG.md +15 -0
- data/README.md +32 -1
- data/lib/clamo/server.rb +58 -44
- data/lib/clamo/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 97084b2761bd6af3549fd2444f981da0a23ce6f1ebbc740f4c2fb237e90ee1fe
|
|
4
|
+
data.tar.gz: dde0cc4f7187bc58aeb2d1be4169f3cc6331d4008c7a10539317cb80e2a68480
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 18a87542a9340025ff18f60594ae32e941bca85f4f268c2dd99ca207dc2c4296c74f2809531e7e43034f27219b4d43fec4f380f61def8e37af6477fa979c273e
|
|
7
|
+
data.tar.gz: 976d81d554ff643ddfcbbaa5185094fda802657b5adee586d60e7c31e7aee27b0f6f141263f23dc8ebbc4d7b0a288beb3e1dc78a41b4209c18c266f2c64931a8
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ 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.8.0] - 2026-03-14
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `before_dispatch` and `after_dispatch` hooks — run around every method call (requests and notifications). Raise in `before_dispatch` to halt execution; `after_dispatch` fires only on success with `(method, params, result)`.
|
|
12
|
+
- Per-call configuration — `timeout`, `on_error`, `before_dispatch`, and `after_dispatch` can be passed as keyword arguments to `handle`, `unparsed_dispatch_to_object`, and `parsed_dispatch_to_object`, overriding module-level defaults.
|
|
13
|
+
- `Clamo::Server::Config` — immutable `Data` struct that snapshots configuration at the start of each dispatch, eliminating race conditions from concurrent mutations to module-level settings.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Internal error responses no longer include `e.message` in the `data` field, preventing leakage of internal details to clients. Exceptions are routed through `on_error` instead.
|
|
18
|
+
- `parsed_dispatch_to_object` normalizes request keys to strings at the boundary, fixing silent dispatch failures when callers pass symbol-key hashes.
|
|
19
|
+
- `on_error` is now called for request dispatch errors (previously only for notification errors).
|
|
20
|
+
- Test suite expanded to 95 tests / 138 assertions.
|
|
21
|
+
|
|
7
22
|
## [0.7.0] - 2026-03-14
|
|
8
23
|
|
|
9
24
|
### Added
|
data/README.md
CHANGED
|
@@ -141,7 +141,7 @@ Clamo::Server.timeout = nil # disable timeout
|
|
|
141
141
|
|
|
142
142
|
### Error Callback
|
|
143
143
|
|
|
144
|
-
|
|
144
|
+
Errors during dispatch are reported through `on_error`. Notifications are silent by default (no response is sent); requests return a generic `-32603 Internal error` without leaking exception details. Use `on_error` to capture the full exception for logging:
|
|
145
145
|
|
|
146
146
|
```ruby
|
|
147
147
|
Clamo::Server.on_error = ->(exception, method, params) {
|
|
@@ -149,6 +149,37 @@ Clamo::Server.on_error = ->(exception, method, params) {
|
|
|
149
149
|
}
|
|
150
150
|
```
|
|
151
151
|
|
|
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
|
+
### Per-Call Configuration
|
|
167
|
+
|
|
168
|
+
All configuration options can be overridden per-call. Module-level settings serve as defaults:
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
Clamo::Server.handle(
|
|
172
|
+
request: body,
|
|
173
|
+
object: MyService,
|
|
174
|
+
timeout: 5,
|
|
175
|
+
on_error: ->(e, method, params) { MyLogger.error(e) },
|
|
176
|
+
before_dispatch: ->(method, params) { authorize!(method) },
|
|
177
|
+
after_dispatch: ->(method, params, result) { track(method) }
|
|
178
|
+
)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Per-call config is snapshotted at the start of each dispatch, so concurrent mutations to module-level settings cannot affect in-flight requests.
|
|
182
|
+
|
|
152
183
|
## Advanced Features
|
|
153
184
|
|
|
154
185
|
### Parallel Processing
|
data/lib/clamo/server.rb
CHANGED
|
@@ -6,20 +6,13 @@ require "timeout"
|
|
|
6
6
|
|
|
7
7
|
module Clamo
|
|
8
8
|
module Server
|
|
9
|
-
|
|
10
|
-
# Global error callback for notification failures.
|
|
11
|
-
# This is module-level state shared across all callers.
|
|
12
|
-
# Set to any callable (lambda, method, proc) that accepts (exception, method, params).
|
|
13
|
-
#
|
|
14
|
-
# Clamo::Server.on_error = ->(e, method, params) { Rails.logger.error(e) }
|
|
15
|
-
#
|
|
16
|
-
attr_accessor :on_error
|
|
9
|
+
Config = Data.define(:timeout, :on_error, :before_dispatch, :after_dispatch)
|
|
17
10
|
|
|
18
|
-
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
|
|
11
|
+
class << self
|
|
12
|
+
# Module-level defaults. These are snapshotted at the start of each
|
|
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
|
|
23
16
|
attr_writer :timeout
|
|
24
17
|
|
|
25
18
|
def timeout
|
|
@@ -53,16 +46,33 @@ module Clamo
|
|
|
53
46
|
parsed_dispatch_to_object(request: parsed, object: object, **)
|
|
54
47
|
end
|
|
55
48
|
|
|
56
|
-
def parsed_dispatch_to_object(request:, object:,
|
|
49
|
+
def parsed_dispatch_to_object(request:, object:,
|
|
50
|
+
timeout: self.timeout,
|
|
51
|
+
on_error: self.on_error,
|
|
52
|
+
before_dispatch: self.before_dispatch,
|
|
53
|
+
after_dispatch: self.after_dispatch,
|
|
54
|
+
**opts)
|
|
57
55
|
raise ArgumentError, "object is required" unless object
|
|
58
56
|
|
|
59
|
-
|
|
57
|
+
request = normalize_request_keys(request)
|
|
58
|
+
config = Config.new(timeout: timeout, on_error: on_error,
|
|
59
|
+
before_dispatch: before_dispatch, after_dispatch: after_dispatch)
|
|
60
|
+
|
|
61
|
+
response_for(request: request, object: object, config: config, **opts) do |method, params|
|
|
60
62
|
dispatch_to_ruby(object: object, method: method, params: params)
|
|
61
63
|
end
|
|
62
64
|
end
|
|
63
65
|
|
|
64
66
|
private
|
|
65
67
|
|
|
68
|
+
def normalize_request_keys(request)
|
|
69
|
+
case request
|
|
70
|
+
when Hash then request.transform_keys(&:to_s)
|
|
71
|
+
when Array then request.map { |r| r.is_a?(Hash) ? r.transform_keys(&:to_s) : r }
|
|
72
|
+
else request
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
66
76
|
def method_known?(object:, method:)
|
|
67
77
|
object.public_methods(false).map(&:to_sym).include?(method.to_sym)
|
|
68
78
|
end
|
|
@@ -78,18 +88,18 @@ module Clamo
|
|
|
78
88
|
# Extra keyword arguments (**) are forwarded to response_for_batch only,
|
|
79
89
|
# where they become options for Parallel.map (e.g., in_processes: 4).
|
|
80
90
|
# For single requests they are silently ignored.
|
|
81
|
-
def response_for(request:, object:, **, &block)
|
|
91
|
+
def response_for(request:, object:, config:, **, &block)
|
|
82
92
|
case request
|
|
83
93
|
when Array
|
|
84
|
-
response_for_batch(request: request, object: object, block: block, **)
|
|
94
|
+
response_for_batch(request: request, object: object, block: block, config: config, **)
|
|
85
95
|
when Hash
|
|
86
|
-
response_for_single_request(request: request, object: object, block: block)
|
|
96
|
+
response_for_single_request(request: request, object: object, block: block, config: config)
|
|
87
97
|
else
|
|
88
98
|
JSONRPC.build_error_response_from(id: nil, descriptor: JSONRPC::ProtocolErrors::INVALID_REQUEST)
|
|
89
99
|
end
|
|
90
100
|
end
|
|
91
101
|
|
|
92
|
-
def response_for_single_request(request:, object:, block:)
|
|
102
|
+
def response_for_single_request(request:, object:, block:, config:)
|
|
93
103
|
error = validate_request_structure(request)
|
|
94
104
|
return error if error
|
|
95
105
|
|
|
@@ -97,23 +107,23 @@ module Clamo
|
|
|
97
107
|
return request.key?("id") ? method_not_found_error(request) : nil
|
|
98
108
|
end
|
|
99
109
|
|
|
100
|
-
return dispatch_notification(request, block) unless request.key?("id")
|
|
110
|
+
return dispatch_notification(request, block, config) unless request.key?("id")
|
|
101
111
|
|
|
102
|
-
dispatch_request(request, block)
|
|
112
|
+
dispatch_request(request, block, config)
|
|
103
113
|
end
|
|
104
114
|
|
|
105
|
-
def response_for_batch(request:, object:, block:, **opts)
|
|
115
|
+
def response_for_batch(request:, object:, block:, config:, **opts)
|
|
106
116
|
if request.empty?
|
|
107
117
|
return JSONRPC.build_error_response_from(id: nil, descriptor: JSONRPC::ProtocolErrors::INVALID_REQUEST)
|
|
108
118
|
end
|
|
109
119
|
|
|
110
120
|
if request.size == 1
|
|
111
|
-
result = response_for_single_request(request: request.first, object: object, block: block)
|
|
121
|
+
result = response_for_single_request(request: request.first, object: object, block: block, config: config)
|
|
112
122
|
return result ? [result] : nil
|
|
113
123
|
end
|
|
114
124
|
|
|
115
125
|
result = Parallel.map(request, **opts) do |item|
|
|
116
|
-
response_for_single_request(request: item, object: object, block: block)
|
|
126
|
+
response_for_single_request(request: item, object: object, block: block, config: config)
|
|
117
127
|
end.compact
|
|
118
128
|
result.empty? ? nil : result
|
|
119
129
|
end
|
|
@@ -138,20 +148,33 @@ module Clamo
|
|
|
138
148
|
JSONRPC.build_error_response_from(id: request["id"], descriptor: JSONRPC::ProtocolErrors::METHOD_NOT_FOUND)
|
|
139
149
|
end
|
|
140
150
|
|
|
141
|
-
def dispatch_notification(request, block)
|
|
142
|
-
|
|
151
|
+
def dispatch_notification(request, block, config)
|
|
152
|
+
method = request["method"]
|
|
153
|
+
params = request["params"]
|
|
154
|
+
config.before_dispatch&.call(method, params)
|
|
155
|
+
with_timeout(config.timeout) { block.yield(method, params) }
|
|
156
|
+
config.after_dispatch&.call(method, params, nil)
|
|
143
157
|
nil
|
|
144
158
|
rescue StandardError => e
|
|
145
|
-
on_error&.call(e,
|
|
159
|
+
config.on_error&.call(e, method, params)
|
|
146
160
|
nil
|
|
147
161
|
end
|
|
148
162
|
|
|
149
|
-
def dispatch_request(request, block)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
)
|
|
163
|
+
def dispatch_request(request, block, config)
|
|
164
|
+
method = request["method"]
|
|
165
|
+
params = request["params"]
|
|
166
|
+
config.before_dispatch&.call(method, params)
|
|
167
|
+
result = with_timeout(config.timeout) { block.yield(method, params) }
|
|
168
|
+
config.after_dispatch&.call(method, params, result)
|
|
169
|
+
JSONRPC.build_result_response(id: request["id"], result: result)
|
|
154
170
|
rescue Timeout::Error
|
|
171
|
+
timeout_error(request)
|
|
172
|
+
rescue StandardError => e
|
|
173
|
+
config.on_error&.call(e, method, params)
|
|
174
|
+
JSONRPC.build_error_response_from(id: request["id"], descriptor: JSONRPC::ProtocolErrors::INTERNAL_ERROR)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def timeout_error(request)
|
|
155
178
|
JSONRPC.build_error_response(
|
|
156
179
|
id: request["id"],
|
|
157
180
|
error: {
|
|
@@ -160,20 +183,11 @@ module Clamo
|
|
|
160
183
|
data: "Request timed out"
|
|
161
184
|
}
|
|
162
185
|
)
|
|
163
|
-
rescue StandardError => e
|
|
164
|
-
JSONRPC.build_error_response(
|
|
165
|
-
id: request["id"],
|
|
166
|
-
error: {
|
|
167
|
-
code: JSONRPC::ProtocolErrors::INTERNAL_ERROR.code,
|
|
168
|
-
message: JSONRPC::ProtocolErrors::INTERNAL_ERROR.message,
|
|
169
|
-
data: e.message
|
|
170
|
-
}
|
|
171
|
-
)
|
|
172
186
|
end
|
|
173
187
|
|
|
174
|
-
def with_timeout(&block)
|
|
175
|
-
if
|
|
176
|
-
Timeout.timeout(
|
|
188
|
+
def with_timeout(seconds, &block)
|
|
189
|
+
if seconds
|
|
190
|
+
Timeout.timeout(seconds, &block)
|
|
177
191
|
else
|
|
178
192
|
block.call
|
|
179
193
|
end
|
data/lib/clamo/version.rb
CHANGED