clamo 0.6.0 → 0.7.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 +6 -0
- data/CHANGELOG.md +24 -0
- data/LICENSE +21 -0
- data/README.md +42 -6
- data/lib/clamo/jsonrpc.rb +28 -20
- data/lib/clamo/server.rb +49 -6
- data/lib/clamo/version.rb +1 -1
- data/lib/clamo.rb +0 -1
- metadata +6 -4
- data/sig/clamo.rbs +0 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 410f469e07b16f8273149b1eded89c94692a91c8b41cd7c47642e897ca8d97fb
|
|
4
|
+
data.tar.gz: 2d6be4be393cfd70d5bf22109943b7735de5ca0abe93b65a888fe1a372640faa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 385175b28fea11461b35a98860a6b8099bd9c88bc8dfef4f2603dc911a3d9e1709b51343d2b22ebb524ffc82aa03de7d8886a50b078c258d5fe28ec897a17296
|
|
7
|
+
data.tar.gz: 0c37f0971e24855610a91c8687d8e55ff32a03accf1effc73b6a1090118b46e3d78ffd9f0a7531418aa97a3154969bf7fb4d2dd0da9dc0cfe1b2b7aa94778cda
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ 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.7.0] - 2026-03-14
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `Clamo::Server.timeout` — per-dispatch timeout with 30-second default; returns `-32000 Server error` on timeout for requests, calls `on_error` for notifications. Set to `nil` to disable.
|
|
12
|
+
- MIT LICENSE file and `spec.license` in gemspec
|
|
13
|
+
- Indifferent key access in JSONRPC validators (symbol and string keys both accepted)
|
|
14
|
+
- Tests for string ids, arity mismatch, single-item batches, and handle+timeout (77 tests / 105 assertions)
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- `proper_pragma?`, `proper_method?`, `proper_id_if_any?` moved from public to private API on `Clamo::JSONRPC`
|
|
19
|
+
- Notifications with invalid params type now return `nil` instead of an error response (spec compliance)
|
|
20
|
+
- `parsed_dispatch_to_object` now validates `object:` argument (raises `ArgumentError` if nil)
|
|
21
|
+
- Single-item batches skip `Parallel.map` overhead
|
|
22
|
+
- Gemspec description expanded (no longer identical to summary)
|
|
23
|
+
- README updated with `Server.handle`, `timeout`, and `on_error` documentation
|
|
24
|
+
|
|
25
|
+
### Removed
|
|
26
|
+
|
|
27
|
+
- `Clamo::Error` base exception class (unused)
|
|
28
|
+
- `sig/clamo.rbs` type signatures (misleadingly incomplete)
|
|
29
|
+
- Dead `else` branch in `dispatch_to_ruby`
|
|
30
|
+
|
|
7
31
|
## [0.6.0] - 2026-03-14
|
|
8
32
|
|
|
9
33
|
### Added
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Andriy Tyurnikov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
CHANGED
|
@@ -26,15 +26,30 @@ module MyService
|
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
request:
|
|
29
|
+
# JSON string in, JSON string out — the primary entry point for HTTP/socket integrations.
|
|
30
|
+
# Returns nil for notifications (no response expected).
|
|
31
|
+
json_response = Clamo::Server.handle(
|
|
32
|
+
request: '{"jsonrpc": "2.0", "method": "add", "params": [1, 2], "id": 1}',
|
|
33
33
|
object: MyService
|
|
34
34
|
)
|
|
35
|
+
# => '{"jsonrpc":"2.0","result":3,"id":1}'
|
|
36
|
+
```
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
If you need the parsed hash instead of a JSON string, use the lower-level methods:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
# From a JSON string
|
|
42
|
+
response = Clamo::Server.unparsed_dispatch_to_object(
|
|
43
|
+
request: '{"jsonrpc": "2.0", "method": "add", "params": [1, 2], "id": 1}',
|
|
44
|
+
object: MyService
|
|
45
|
+
)
|
|
37
46
|
# => {jsonrpc: "2.0", result: 3, id: 1}
|
|
47
|
+
|
|
48
|
+
# From a pre-parsed hash
|
|
49
|
+
response = Clamo::Server.parsed_dispatch_to_object(
|
|
50
|
+
request: { "jsonrpc" => "2.0", "method" => "add", "params" => [1, 2], "id" => 1 },
|
|
51
|
+
object: MyService
|
|
52
|
+
)
|
|
38
53
|
```
|
|
39
54
|
|
|
40
55
|
### Handling Different Parameter Types
|
|
@@ -113,11 +128,32 @@ Clamo follows the JSON-RPC 2.0 specification for error handling:
|
|
|
113
128
|
| -32603 | Internal error | Internal JSON-RPC error |
|
|
114
129
|
| -32000 | Server error | Reserved for implementation-defined server errors |
|
|
115
130
|
|
|
131
|
+
## Configuration
|
|
132
|
+
|
|
133
|
+
### Timeout
|
|
134
|
+
|
|
135
|
+
Every method dispatch is wrapped in a timeout. The default is 30 seconds. Timed-out requests return a `-32000 Server error` response.
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
Clamo::Server.timeout = 10 # seconds
|
|
139
|
+
Clamo::Server.timeout = nil # disable timeout
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Error Callback
|
|
143
|
+
|
|
144
|
+
Notifications don't return responses, so errors during notification dispatch are silent by default. Use `on_error` to capture them:
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
Clamo::Server.on_error = ->(exception, method, params) {
|
|
148
|
+
Rails.logger.error("#{method} failed: #{exception.message}")
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
116
152
|
## Advanced Features
|
|
117
153
|
|
|
118
154
|
### Parallel Processing
|
|
119
155
|
|
|
120
|
-
Batch requests are processed in parallel using the [parallel](https://github.com/grosser/parallel) gem. You can pass options to `Parallel.map
|
|
156
|
+
Batch requests are processed in parallel using the [parallel](https://github.com/grosser/parallel) gem. You can pass options to `Parallel.map`:
|
|
121
157
|
|
|
122
158
|
```ruby
|
|
123
159
|
Clamo::Server.parsed_dispatch_to_object(
|
data/lib/clamo/jsonrpc.rb
CHANGED
|
@@ -14,27 +14,10 @@ module Clamo
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
class << self
|
|
17
|
-
def proper_pragma?(request)
|
|
18
|
-
request["jsonrpc"] == "2.0"
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def proper_method?(request)
|
|
22
|
-
request["method"].is_a?(String)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def proper_id_if_any?(request)
|
|
26
|
-
if request.key?("id")
|
|
27
|
-
request["id"].is_a?(String) ||
|
|
28
|
-
request["id"].is_a?(Integer) ||
|
|
29
|
-
request["id"].is_a?(NilClass)
|
|
30
|
-
else
|
|
31
|
-
true
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
17
|
def proper_params_if_any?(request)
|
|
36
|
-
if
|
|
37
|
-
|
|
18
|
+
if key_indifferent?(request, "params")
|
|
19
|
+
params = fetch_indifferent(request, "params")
|
|
20
|
+
params.is_a?(Array) || params.is_a?(Hash)
|
|
38
21
|
else
|
|
39
22
|
true
|
|
40
23
|
end
|
|
@@ -96,11 +79,36 @@ module Clamo
|
|
|
96
79
|
|
|
97
80
|
private
|
|
98
81
|
|
|
82
|
+
def proper_pragma?(request)
|
|
83
|
+
fetch_indifferent(request, "jsonrpc") == "2.0"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def proper_method?(request)
|
|
87
|
+
fetch_indifferent(request, "method").is_a?(String)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def proper_id_if_any?(request)
|
|
91
|
+
if key_indifferent?(request, "id")
|
|
92
|
+
id = fetch_indifferent(request, "id")
|
|
93
|
+
id.is_a?(String) || id.is_a?(Integer) || id.is_a?(NilClass)
|
|
94
|
+
else
|
|
95
|
+
true
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
99
|
def validate_params_type!(params)
|
|
100
100
|
return if params.is_a?(Array) || params.is_a?(Hash)
|
|
101
101
|
|
|
102
102
|
raise ArgumentError, "params must be an Array or Hash"
|
|
103
103
|
end
|
|
104
|
+
|
|
105
|
+
def fetch_indifferent(hash, key)
|
|
106
|
+
hash.fetch(key.to_s) { hash.fetch(key.to_sym, nil) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def key_indifferent?(hash, key)
|
|
110
|
+
hash.key?(key.to_s) || hash.key?(key.to_sym)
|
|
111
|
+
end
|
|
104
112
|
end
|
|
105
113
|
end
|
|
106
114
|
end
|
data/lib/clamo/server.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "parallel"
|
|
5
|
+
require "timeout"
|
|
5
6
|
|
|
6
7
|
module Clamo
|
|
7
8
|
module Server
|
|
@@ -14,6 +15,19 @@ module Clamo
|
|
|
14
15
|
#
|
|
15
16
|
attr_accessor :on_error
|
|
16
17
|
|
|
18
|
+
# Maximum seconds allowed for a single method dispatch. Defaults to 30.
|
|
19
|
+
# Set to nil to disable.
|
|
20
|
+
#
|
|
21
|
+
# Clamo::Server.timeout = 10
|
|
22
|
+
#
|
|
23
|
+
attr_writer :timeout
|
|
24
|
+
|
|
25
|
+
def timeout
|
|
26
|
+
return @timeout if defined?(@timeout)
|
|
27
|
+
|
|
28
|
+
30
|
|
29
|
+
end
|
|
30
|
+
|
|
17
31
|
# JSON string in, JSON string out. Full round-trip for HTTP/socket integrations.
|
|
18
32
|
#
|
|
19
33
|
# Clamo::Server.handle(request: body, object: MyService)
|
|
@@ -40,6 +54,8 @@ module Clamo
|
|
|
40
54
|
end
|
|
41
55
|
|
|
42
56
|
def parsed_dispatch_to_object(request:, object:, **opts)
|
|
57
|
+
raise ArgumentError, "object is required" unless object
|
|
58
|
+
|
|
43
59
|
response_for(request: request, object: object, **opts) do |method, params|
|
|
44
60
|
dispatch_to_ruby(object: object, method: method, params: params)
|
|
45
61
|
end
|
|
@@ -53,13 +69,15 @@ module Clamo
|
|
|
53
69
|
|
|
54
70
|
def dispatch_to_ruby(object:, method:, params:)
|
|
55
71
|
case params
|
|
56
|
-
when Array
|
|
57
|
-
when Hash
|
|
58
|
-
when NilClass
|
|
59
|
-
else raise ArgumentError, "Unsupported params type: #{params.class}"
|
|
72
|
+
when Array then object.public_send(method.to_sym, *params)
|
|
73
|
+
when Hash then object.public_send(method.to_sym, **params.transform_keys(&:to_sym))
|
|
74
|
+
when NilClass then object.public_send(method.to_sym)
|
|
60
75
|
end
|
|
61
76
|
end
|
|
62
77
|
|
|
78
|
+
# Extra keyword arguments (**) are forwarded to response_for_batch only,
|
|
79
|
+
# where they become options for Parallel.map (e.g., in_processes: 4).
|
|
80
|
+
# For single requests they are silently ignored.
|
|
63
81
|
def response_for(request:, object:, **, &block)
|
|
64
82
|
case request
|
|
65
83
|
when Array
|
|
@@ -89,6 +107,11 @@ module Clamo
|
|
|
89
107
|
return JSONRPC.build_error_response_from(id: nil, descriptor: JSONRPC::ProtocolErrors::INVALID_REQUEST)
|
|
90
108
|
end
|
|
91
109
|
|
|
110
|
+
if request.size == 1
|
|
111
|
+
result = response_for_single_request(request: request.first, object: object, block: block)
|
|
112
|
+
return result ? [result] : nil
|
|
113
|
+
end
|
|
114
|
+
|
|
92
115
|
result = Parallel.map(request, **opts) do |item|
|
|
93
116
|
response_for_single_request(request: item, object: object, block: block)
|
|
94
117
|
end.compact
|
|
@@ -105,6 +128,9 @@ module Clamo
|
|
|
105
128
|
|
|
106
129
|
return if JSONRPC.proper_params_if_any?(request)
|
|
107
130
|
|
|
131
|
+
# Notifications must never produce a response, even for invalid params
|
|
132
|
+
return nil unless request.key?("id")
|
|
133
|
+
|
|
108
134
|
JSONRPC.build_error_response_from(id: request["id"], descriptor: JSONRPC::ProtocolErrors::INVALID_PARAMS)
|
|
109
135
|
end
|
|
110
136
|
|
|
@@ -113,7 +139,7 @@ module Clamo
|
|
|
113
139
|
end
|
|
114
140
|
|
|
115
141
|
def dispatch_notification(request, block)
|
|
116
|
-
block.yield request["method"], request["params"]
|
|
142
|
+
with_timeout { block.yield request["method"], request["params"] }
|
|
117
143
|
nil
|
|
118
144
|
rescue StandardError => e
|
|
119
145
|
on_error&.call(e, request["method"], request["params"])
|
|
@@ -123,7 +149,16 @@ module Clamo
|
|
|
123
149
|
def dispatch_request(request, block)
|
|
124
150
|
JSONRPC.build_result_response(
|
|
125
151
|
id: request["id"],
|
|
126
|
-
result: block.yield(request["method"], request["params"])
|
|
152
|
+
result: with_timeout { block.yield(request["method"], request["params"]) }
|
|
153
|
+
)
|
|
154
|
+
rescue Timeout::Error
|
|
155
|
+
JSONRPC.build_error_response(
|
|
156
|
+
id: request["id"],
|
|
157
|
+
error: {
|
|
158
|
+
code: JSONRPC::ProtocolErrors::SERVER_ERROR.code,
|
|
159
|
+
message: JSONRPC::ProtocolErrors::SERVER_ERROR.message,
|
|
160
|
+
data: "Request timed out"
|
|
161
|
+
}
|
|
127
162
|
)
|
|
128
163
|
rescue StandardError => e
|
|
129
164
|
JSONRPC.build_error_response(
|
|
@@ -135,6 +170,14 @@ module Clamo
|
|
|
135
170
|
}
|
|
136
171
|
)
|
|
137
172
|
end
|
|
173
|
+
|
|
174
|
+
def with_timeout(&block)
|
|
175
|
+
if timeout
|
|
176
|
+
Timeout.timeout(timeout, &block)
|
|
177
|
+
else
|
|
178
|
+
block.call
|
|
179
|
+
end
|
|
180
|
+
end
|
|
138
181
|
end
|
|
139
182
|
end
|
|
140
183
|
end
|
data/lib/clamo/version.rb
CHANGED
data/lib/clamo.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: clamo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andriy Tyurnikov
|
|
@@ -23,7 +23,8 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '1.27'
|
|
26
|
-
description: JSON-RPC 2.0 server
|
|
26
|
+
description: Minimal, spec-compliant JSON-RPC 2.0 server for Ruby with request validation,
|
|
27
|
+
method dispatch, batch processing, and notification support.
|
|
27
28
|
email:
|
|
28
29
|
- Andriy.Tyurnikov@gmail.com
|
|
29
30
|
executables: []
|
|
@@ -32,15 +33,16 @@ extra_rdoc_files: []
|
|
|
32
33
|
files:
|
|
33
34
|
- ".rubocop.yml"
|
|
34
35
|
- CHANGELOG.md
|
|
36
|
+
- LICENSE
|
|
35
37
|
- README.md
|
|
36
38
|
- Rakefile
|
|
37
39
|
- lib/clamo.rb
|
|
38
40
|
- lib/clamo/jsonrpc.rb
|
|
39
41
|
- lib/clamo/server.rb
|
|
40
42
|
- lib/clamo/version.rb
|
|
41
|
-
- sig/clamo.rbs
|
|
42
43
|
homepage: https://github.com/rubakas/clamo
|
|
43
|
-
licenses:
|
|
44
|
+
licenses:
|
|
45
|
+
- MIT
|
|
44
46
|
metadata:
|
|
45
47
|
homepage_uri: https://github.com/rubakas/clamo
|
|
46
48
|
source_code_uri: https://github.com/rubakas/clamo
|
data/sig/clamo.rbs
DELETED