clamo 0.9.0 → 1.0.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 +41 -0
- data/README.md +91 -66
- data/lib/clamo/jsonrpc.rb +33 -16
- data/lib/clamo/server.rb +129 -71
- data/lib/clamo/version.rb +1 -1
- data/lib/clamo.rb +5 -0
- metadata +5 -18
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fb802891a70c42e2bb89446fd258fc7e9114ff29f64c4cc6051f819332172307
|
|
4
|
+
data.tar.gz: 10af0f3deb9d61fd068ebd9a1c6bdbc9f93aca02b8491b8c1856032338c5a56d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bdbb35a70e40adad88ef1c19b0247e11512d6d51a50fcba039cb74699e60ad7088b42f0ca8d7297c19d5e075100602c5ee79d1dc28f0604a6f0160c440a0db6f
|
|
7
|
+
data.tar.gz: b8a2d1c0002003468c4d4e3bbc03348d36b56e9ed13396c07653640c00cd3e924a6df45a3951a1d8c5a235bd189fd38d66418cf405f2786db3338570e8bf790a
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,47 @@ 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
|
+
|
|
7
48
|
## [0.9.0] - 2026-03-14
|
|
8
49
|
|
|
9
50
|
### Changed
|
data/README.md
CHANGED
|
@@ -1,7 +1,47 @@
|
|
|
1
1
|
# Clamo
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/rubakas/clamo/actions/workflows/main.yml)
|
|
4
|
+
[](https://badge.fury.io/rb/clamo)
|
|
5
|
+
[](https://www.ruby-lang.org)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://www.jsonrpc.org/specification)
|
|
8
|
+
|
|
9
|
+
A Ruby implementation of [JSON-RPC 2.0](https://www.jsonrpc.org/specification) designed for simplicity and compliance with the specification. Expose any Ruby module or class as a JSON-RPC service with minimal effort — just point Clamo at your object and its public methods become callable.
|
|
10
|
+
|
|
11
|
+
JSON-RPC 2.0 is the transport protocol behind [MCP](https://modelcontextprotocol.io/) and [LSP](https://microsoft.github.io/language-server-protocol/) — Clamo makes it easy to build spec-compliant services in Ruby.
|
|
12
|
+
|
|
13
|
+
## Table of Contents
|
|
14
|
+
|
|
15
|
+
- [Installation](#installation)
|
|
16
|
+
- [Usage](#usage)
|
|
17
|
+
- [Basic Usage](#basic-usage)
|
|
18
|
+
- [Batch Requests](#batch-requests)
|
|
19
|
+
- [Notifications](#notifications)
|
|
20
|
+
- [Error Handling](#error-handling)
|
|
21
|
+
- [Configuration](#configuration)
|
|
22
|
+
- [Error Callback](#error-callback)
|
|
23
|
+
- [Per-Call Configuration](#per-call-configuration)
|
|
24
|
+
- [Advanced Features](#advanced-features)
|
|
25
|
+
- [Parallel Processing](#parallel-processing)
|
|
26
|
+
- [Building JSON-RPC Requests](#building-json-rpc-requests)
|
|
27
|
+
- [Roadmap](#roadmap)
|
|
28
|
+
- [Development](#development)
|
|
29
|
+
- [Contributing](#contributing)
|
|
30
|
+
- [License](#license)
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
Add to your Gemfile:
|
|
4
35
|
|
|
36
|
+
```ruby
|
|
37
|
+
gem "clamo"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or install directly:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
gem install clamo
|
|
44
|
+
```
|
|
5
45
|
|
|
6
46
|
## Usage
|
|
7
47
|
|
|
@@ -28,41 +68,33 @@ end
|
|
|
28
68
|
|
|
29
69
|
# JSON string in, JSON string out — the primary entry point for HTTP/socket integrations.
|
|
30
70
|
# Returns nil for notifications (no response expected).
|
|
31
|
-
json_response = Clamo::Server.
|
|
71
|
+
json_response = Clamo::Server.handle_json(
|
|
32
72
|
request: '{"jsonrpc": "2.0", "method": "add", "params": [1, 2], "id": 1}',
|
|
33
73
|
object: MyService
|
|
34
74
|
)
|
|
35
75
|
# => '{"jsonrpc":"2.0","result":3,"id":1}'
|
|
36
76
|
```
|
|
37
77
|
|
|
38
|
-
If you need the parsed hash instead of a JSON string, use the lower-level methods:
|
|
78
|
+
If you need the parsed hash instead of a JSON string, use the lower-level methods directly or via their shorter aliases:
|
|
39
79
|
|
|
40
80
|
```ruby
|
|
41
81
|
# From a JSON string
|
|
42
|
-
response = Clamo::Server.
|
|
82
|
+
response = Clamo::Server.dispatch_json(
|
|
43
83
|
request: '{"jsonrpc": "2.0", "method": "add", "params": [1, 2], "id": 1}',
|
|
44
84
|
object: MyService
|
|
45
85
|
)
|
|
46
86
|
# => {"jsonrpc" => "2.0", "result" => 3, "id" => 1}
|
|
47
87
|
|
|
48
88
|
# From a pre-parsed hash
|
|
49
|
-
response = Clamo::Server.
|
|
89
|
+
response = Clamo::Server.dispatch(
|
|
50
90
|
request: { "jsonrpc" => "2.0", "method" => "add", "params" => [1, 2], "id" => 1 },
|
|
51
91
|
object: MyService
|
|
52
92
|
)
|
|
53
93
|
```
|
|
54
94
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
Clamo supports both positional (array) and named (object/hash) parameters:
|
|
95
|
+
The longer names `parsed_dispatch_to_object`, `unparsed_dispatch_to_object`, and `handle` still work as deprecated aliases.
|
|
58
96
|
|
|
59
|
-
|
|
60
|
-
# Positional parameters
|
|
61
|
-
request = '{"jsonrpc": "2.0", "method": "add", "params": [1, 2], "id": 1}'
|
|
62
|
-
|
|
63
|
-
# Named parameters
|
|
64
|
-
request = '{"jsonrpc": "2.0", "method": "subtract", "params": {"a": 5, "b": 3}, "id": 2}'
|
|
65
|
-
```
|
|
97
|
+
Both positional (`[1, 2]`) and named (`{"a": 5, "b": 3}`) parameters are supported — they map to positional and keyword arguments on the Ruby method respectively.
|
|
66
98
|
|
|
67
99
|
### Batch Requests
|
|
68
100
|
|
|
@@ -76,7 +108,7 @@ batch_request = <<~JSON
|
|
|
76
108
|
]
|
|
77
109
|
JSON
|
|
78
110
|
|
|
79
|
-
batch_response = Clamo::Server.
|
|
111
|
+
batch_response = Clamo::Server.dispatch_json(
|
|
80
112
|
request: batch_request,
|
|
81
113
|
object: MyService
|
|
82
114
|
)
|
|
@@ -91,7 +123,7 @@ Notifications are requests without an ID field. They don't produce a response:
|
|
|
91
123
|
|
|
92
124
|
```ruby
|
|
93
125
|
notification = '{"jsonrpc": "2.0", "method": "add", "params": [1, 2]}'
|
|
94
|
-
response = Clamo::Server.
|
|
126
|
+
response = Clamo::Server.dispatch_json(
|
|
95
127
|
request: notification,
|
|
96
128
|
object: MyService
|
|
97
129
|
)
|
|
@@ -100,21 +132,6 @@ puts response
|
|
|
100
132
|
# => nil
|
|
101
133
|
```
|
|
102
134
|
|
|
103
|
-
### Building JSON-RPC Requests
|
|
104
|
-
|
|
105
|
-
Clamo provides utilities for building JSON-RPC requests:
|
|
106
|
-
|
|
107
|
-
```ruby
|
|
108
|
-
request = Clamo::JSONRPC.build_request(
|
|
109
|
-
method: "add",
|
|
110
|
-
params: [1, 2],
|
|
111
|
-
id: 1
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
puts request
|
|
115
|
-
# => {"jsonrpc" => "2.0", "method" => "add", "params" => [1, 2], "id" => 1}
|
|
116
|
-
```
|
|
117
|
-
|
|
118
135
|
## Error Handling
|
|
119
136
|
|
|
120
137
|
Clamo follows the JSON-RPC 2.0 specification for error handling:
|
|
@@ -124,24 +141,17 @@ Clamo follows the JSON-RPC 2.0 specification for error handling:
|
|
|
124
141
|
| -32700 | Parse error | Invalid JSON was received |
|
|
125
142
|
| -32600 | Invalid request | The JSON sent is not a valid Request object |
|
|
126
143
|
| -32601 | Method not found | The method does not exist / is not available |
|
|
127
|
-
| -32602 | Invalid params | Invalid method parameter(s)
|
|
144
|
+
| -32602 | Invalid params | Invalid method parameter(s) or arity mismatch |
|
|
128
145
|
| -32603 | Internal error | Internal JSON-RPC error |
|
|
129
|
-
| -32000 | Server error |
|
|
130
|
-
|
|
131
|
-
## Configuration
|
|
132
|
-
|
|
133
|
-
### Timeout
|
|
146
|
+
| -32000 | Server error | Exception raised by dispatched method |
|
|
134
147
|
|
|
135
|
-
|
|
148
|
+
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
149
|
|
|
137
|
-
|
|
138
|
-
Clamo::Server.timeout = 10 # seconds
|
|
139
|
-
Clamo::Server.timeout = nil # disable timeout
|
|
140
|
-
```
|
|
150
|
+
## Configuration
|
|
141
151
|
|
|
142
152
|
### Error Callback
|
|
143
153
|
|
|
144
|
-
Errors during dispatch are reported through `on_error
|
|
154
|
+
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
155
|
|
|
146
156
|
```ruby
|
|
147
157
|
Clamo::Server.on_error = ->(exception, method, params) {
|
|
@@ -149,32 +159,15 @@ Clamo::Server.on_error = ->(exception, method, params) {
|
|
|
149
159
|
}
|
|
150
160
|
```
|
|
151
161
|
|
|
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
162
|
### Per-Call Configuration
|
|
167
163
|
|
|
168
|
-
|
|
164
|
+
Configuration can be overridden per-call. Module-level settings serve as defaults:
|
|
169
165
|
|
|
170
166
|
```ruby
|
|
171
|
-
Clamo::Server.
|
|
167
|
+
Clamo::Server.handle_json(
|
|
172
168
|
request: body,
|
|
173
169
|
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) }
|
|
170
|
+
on_error: ->(e, method, params) { MyLogger.error(e) }
|
|
178
171
|
)
|
|
179
172
|
```
|
|
180
173
|
|
|
@@ -184,16 +177,48 @@ Per-call config is snapshotted at the start of each dispatch, so concurrent muta
|
|
|
184
177
|
|
|
185
178
|
### Parallel Processing
|
|
186
179
|
|
|
187
|
-
Batch requests are processed in parallel
|
|
180
|
+
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
181
|
|
|
189
182
|
```ruby
|
|
190
|
-
Clamo::Server.
|
|
183
|
+
Clamo::Server.dispatch(
|
|
191
184
|
request: batch_request,
|
|
192
185
|
object: MyService,
|
|
193
186
|
in_processes: 4 # Parallel processing option
|
|
194
187
|
)
|
|
195
188
|
```
|
|
196
189
|
|
|
190
|
+
### Building JSON-RPC Requests
|
|
191
|
+
|
|
192
|
+
Clamo provides utilities for building JSON-RPC requests:
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
request = Clamo::JSONRPC.build_request(
|
|
196
|
+
method: "add",
|
|
197
|
+
params: [1, 2],
|
|
198
|
+
id: 1
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
puts request
|
|
202
|
+
# => {"jsonrpc" => "2.0", "method" => "add", "params" => [1, 2], "id" => 1}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Roadmap
|
|
206
|
+
|
|
207
|
+
- [x] Per-call configuration
|
|
208
|
+
- [ ] Method metadata caching
|
|
209
|
+
- [ ] Method allowlists/denylists
|
|
210
|
+
- [ ] Observability and logging
|
|
211
|
+
- [ ] Profiling
|
|
212
|
+
- [ ] Hooks
|
|
213
|
+
- [ ] Rack helpers and framework helpers (Rails)
|
|
214
|
+
- [ ] Autodoc (Markdown?)
|
|
215
|
+
- [ ] Error data builder
|
|
216
|
+
- [ ] Schemas
|
|
217
|
+
- [ ] Method auto-prefixing (namespace to prefix)
|
|
218
|
+
- [ ] Multiple objects (namespace fusion)
|
|
219
|
+
- [ ] Context injection
|
|
220
|
+
- [ ] More examples
|
|
221
|
+
|
|
197
222
|
## Development
|
|
198
223
|
|
|
199
224
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/lib/clamo/jsonrpc.rb
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Clamo
|
|
4
|
+
# Utilities for building and validating JSON-RPC 2.0 messages.
|
|
4
5
|
module JSONRPC
|
|
6
|
+
# Standard JSON-RPC 2.0 error codes and messages.
|
|
5
7
|
module ProtocolErrors
|
|
8
|
+
# Immutable descriptor pairing an error code with its message.
|
|
6
9
|
ErrorDescriptor = Data.define(:code, :message)
|
|
7
10
|
|
|
8
11
|
PARSE_ERROR = ErrorDescriptor.new(code: -32_700, message: "Parse error")
|
|
@@ -14,22 +17,13 @@ module Clamo
|
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
class << self
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
20
|
+
# Builds a JSON-RPC 2.0 request Hash.
|
|
21
|
+
#
|
|
22
|
+
# Clamo::JSONRPC.build_request(method: "add", params: [1, 2], id: 1)
|
|
23
|
+
# # => {"jsonrpc"=>"2.0", "method"=>"add", "params"=>[1, 2], "id"=>1}
|
|
24
|
+
#
|
|
25
|
+
# +method+ is required. +params+ (Array or Hash) and +id+ are optional.
|
|
26
|
+
# Omitting +id+ produces a notification.
|
|
33
27
|
def build_request(**opts)
|
|
34
28
|
raise ArgumentError, "method is required" unless opts.key?(:method)
|
|
35
29
|
|
|
@@ -40,10 +34,13 @@ module Clamo
|
|
|
40
34
|
.then { |r| opts.key?(:id) ? r.merge("id" => opts[:id]) : r }
|
|
41
35
|
end
|
|
42
36
|
|
|
37
|
+
# Builds a successful JSON-RPC 2.0 response Hash.
|
|
43
38
|
def build_result_response(id:, result:)
|
|
44
39
|
{ "jsonrpc" => "2.0", "result" => result, "id" => id }
|
|
45
40
|
end
|
|
46
41
|
|
|
42
|
+
# Builds a JSON-RPC 2.0 error response Hash.
|
|
43
|
+
# Requires +error:+ with +:code+ and +:message+ keys. +:data+ is optional.
|
|
47
44
|
def build_error_response(**opts)
|
|
48
45
|
raise ArgumentError, "error code is required" unless opts.dig(:error, :code)
|
|
49
46
|
raise ArgumentError, "error message is required" unless opts.dig(:error, :message)
|
|
@@ -57,6 +54,7 @@ module Clamo
|
|
|
57
54
|
}.reject { |k, _| k == "data" && !opts[:error].key?(:data) } }
|
|
58
55
|
end
|
|
59
56
|
|
|
57
|
+
# Builds a JSON-RPC 2.0 error response from an ErrorDescriptor.
|
|
60
58
|
def build_error_response_from(descriptor:, id: nil)
|
|
61
59
|
build_error_response(
|
|
62
60
|
id: id,
|
|
@@ -67,6 +65,7 @@ module Clamo
|
|
|
67
65
|
)
|
|
68
66
|
end
|
|
69
67
|
|
|
68
|
+
# Convenience method for a parse error (-32700) response.
|
|
70
69
|
def build_error_response_parse_error
|
|
71
70
|
build_error_response(
|
|
72
71
|
id: nil,
|
|
@@ -77,6 +76,24 @@ module Clamo
|
|
|
77
76
|
)
|
|
78
77
|
end
|
|
79
78
|
|
|
79
|
+
# Used internally by Clamo::Server — not part of the public API.
|
|
80
|
+
def proper_params_if_any?(request)
|
|
81
|
+
if key_indifferent?(request, "params")
|
|
82
|
+
params = fetch_indifferent(request, "params")
|
|
83
|
+
params.is_a?(Array) || params.is_a?(Hash)
|
|
84
|
+
else
|
|
85
|
+
true
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Used internally by Clamo::Server — not part of the public API.
|
|
90
|
+
def valid_request?(request)
|
|
91
|
+
request.is_a?(Hash) &&
|
|
92
|
+
proper_pragma?(request) &&
|
|
93
|
+
proper_method?(request) &&
|
|
94
|
+
proper_id_if_any?(request)
|
|
95
|
+
end
|
|
96
|
+
|
|
80
97
|
private
|
|
81
98
|
|
|
82
99
|
def proper_pragma?(request)
|
data/lib/clamo/server.rb
CHANGED
|
@@ -1,40 +1,52 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
-
require "parallel"
|
|
5
|
-
require "timeout"
|
|
6
4
|
|
|
7
5
|
module Clamo
|
|
6
|
+
# JSON-RPC 2.0 request dispatcher. All public methods on the target +object+
|
|
7
|
+
# become callable JSON-RPC methods.
|
|
8
|
+
#
|
|
9
|
+
# Three entry points, from highest to lowest level:
|
|
10
|
+
# - handle_json — JSON string in, JSON string out
|
|
11
|
+
# - dispatch_json — JSON string in, parsed Hash out
|
|
12
|
+
# - dispatch — parsed Hash in, parsed Hash out
|
|
13
|
+
#
|
|
14
|
+
# == Example
|
|
15
|
+
#
|
|
16
|
+
# Clamo::Server.handle_json(
|
|
17
|
+
# request: '{"jsonrpc":"2.0","method":"add","params":[1,2],"id":1}',
|
|
18
|
+
# object: MyService
|
|
19
|
+
# )
|
|
20
|
+
# # => '{"jsonrpc":"2.0","result":3,"id":1}'
|
|
8
21
|
module Server
|
|
9
|
-
Config = Data.define(:
|
|
22
|
+
Config = Data.define(:on_error)
|
|
10
23
|
|
|
11
24
|
class << self
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
attr_accessor :on_error, :before_dispatch, :after_dispatch
|
|
16
|
-
attr_writer :timeout
|
|
25
|
+
# Global error callback. Called with +(exception, method, params)+ whenever
|
|
26
|
+
# a dispatched method raises. Fires for both requests and notifications.
|
|
27
|
+
attr_accessor :on_error
|
|
17
28
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
30
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# JSON string in, JSON string out. Full round-trip for HTTP/socket integrations.
|
|
29
|
+
# JSON string in, JSON string out. The primary entry point for HTTP/socket
|
|
30
|
+
# integrations. Returns +nil+ for notifications (no response expected).
|
|
25
31
|
#
|
|
26
|
-
# Clamo::Server.
|
|
32
|
+
# Clamo::Server.handle_json(request: body, object: MyService)
|
|
27
33
|
#
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
# All extra keyword arguments are forwarded to #dispatch.
|
|
35
|
+
def handle_json(request:, object:, **)
|
|
36
|
+
response = dispatch_json(request: request, object: object, **)
|
|
30
37
|
response&.to_json
|
|
31
38
|
end
|
|
32
39
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
#
|
|
36
|
-
# )
|
|
37
|
-
|
|
40
|
+
alias handle handle_json
|
|
41
|
+
|
|
42
|
+
# JSON string in, parsed response Hash out. Parses the JSON and delegates
|
|
43
|
+
# to #dispatch. Returns a Hash (or Array of Hashes for batches), or +nil+
|
|
44
|
+
# for notifications.
|
|
45
|
+
#
|
|
46
|
+
# Clamo::Server.dispatch_json(request: json_string, object: MyService)
|
|
47
|
+
#
|
|
48
|
+
# All extra keyword arguments are forwarded to #dispatch.
|
|
49
|
+
def dispatch_json(request:, object:, **)
|
|
38
50
|
raise ArgumentError, "object is required" unless object
|
|
39
51
|
|
|
40
52
|
begin
|
|
@@ -43,26 +55,35 @@ module Clamo
|
|
|
43
55
|
return JSONRPC.build_error_response_parse_error
|
|
44
56
|
end
|
|
45
57
|
|
|
46
|
-
|
|
58
|
+
dispatch(request: parsed, object: object, **)
|
|
47
59
|
end
|
|
48
60
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
alias unparsed_dispatch_to_object dispatch_json
|
|
62
|
+
|
|
63
|
+
# Parsed Hash (or Array) in, parsed response Hash out. Validates the
|
|
64
|
+
# request, resolves the method on +object+, checks parameter arity,
|
|
65
|
+
# and dispatches. Returns +nil+ for notifications.
|
|
66
|
+
#
|
|
67
|
+
# Clamo::Server.dispatch(request: hash_or_array, object: MyService)
|
|
68
|
+
#
|
|
69
|
+
# ==== Options
|
|
70
|
+
# +on_error+:: Error callback for this call, overrides the global on_error.
|
|
71
|
+
# Extra keyword arguments are forwarded to +Parallel.map+ for batch requests.
|
|
72
|
+
def dispatch(request:, object:,
|
|
73
|
+
on_error: self.on_error,
|
|
74
|
+
**opts)
|
|
55
75
|
raise ArgumentError, "object is required" unless object
|
|
56
76
|
|
|
57
77
|
request = normalize_request_keys(request)
|
|
58
|
-
config = Config.new(
|
|
59
|
-
before_dispatch: before_dispatch, after_dispatch: after_dispatch)
|
|
78
|
+
config = Config.new(on_error: on_error)
|
|
60
79
|
|
|
61
80
|
response_for(request: request, object: object, config: config, **opts) do |method, params|
|
|
62
81
|
dispatch_to_ruby(object: object, method: method, params: params)
|
|
63
82
|
end
|
|
64
83
|
end
|
|
65
84
|
|
|
85
|
+
alias parsed_dispatch_to_object dispatch
|
|
86
|
+
|
|
66
87
|
private
|
|
67
88
|
|
|
68
89
|
def normalize_request_keys(request)
|
|
@@ -109,8 +130,15 @@ module Clamo
|
|
|
109
130
|
error = validate_request_structure(request)
|
|
110
131
|
return error if error
|
|
111
132
|
|
|
133
|
+
error = validate_params_type(request)
|
|
134
|
+
return error if error
|
|
135
|
+
|
|
112
136
|
unless method_known?(object: object, method: request["method"])
|
|
113
|
-
return request
|
|
137
|
+
return error_for(request, JSONRPC::ProtocolErrors::METHOD_NOT_FOUND)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
unless params_match_arity?(object: object, method: request["method"], params: request["params"])
|
|
141
|
+
return error_for(request, JSONRPC::ProtocolErrors::INVALID_PARAMS)
|
|
114
142
|
end
|
|
115
143
|
|
|
116
144
|
return dispatch_notification(request, block, config) unless request.key?("id")
|
|
@@ -128,38 +156,44 @@ module Clamo
|
|
|
128
156
|
return result ? [result] : nil
|
|
129
157
|
end
|
|
130
158
|
|
|
131
|
-
result =
|
|
159
|
+
result = map_batch(request, **opts) do |item|
|
|
132
160
|
response_for_single_request(request: item, object: object, block: block, config: config)
|
|
133
161
|
end.compact
|
|
134
162
|
result.empty? ? nil : result
|
|
135
163
|
end
|
|
136
164
|
|
|
137
|
-
def
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
end
|
|
165
|
+
def map_batch(items, **, &)
|
|
166
|
+
require "parallel"
|
|
167
|
+
Parallel.map(items, **, &)
|
|
168
|
+
rescue LoadError
|
|
169
|
+
items.map(&)
|
|
170
|
+
end
|
|
144
171
|
|
|
145
|
-
|
|
172
|
+
def error_for(request, descriptor)
|
|
173
|
+
return unless request.key?("id")
|
|
146
174
|
|
|
147
|
-
|
|
148
|
-
|
|
175
|
+
JSONRPC.build_error_response_from(id: request["id"], descriptor: descriptor)
|
|
176
|
+
end
|
|
149
177
|
|
|
150
|
-
|
|
178
|
+
def validate_request_structure(request)
|
|
179
|
+
return if JSONRPC.valid_request?(request)
|
|
180
|
+
|
|
181
|
+
JSONRPC.build_error_response_from(
|
|
182
|
+
id: request.is_a?(Hash) ? request["id"] : nil,
|
|
183
|
+
descriptor: JSONRPC::ProtocolErrors::INVALID_REQUEST
|
|
184
|
+
)
|
|
151
185
|
end
|
|
152
186
|
|
|
153
|
-
def
|
|
154
|
-
JSONRPC.
|
|
187
|
+
def validate_params_type(request)
|
|
188
|
+
return if JSONRPC.proper_params_if_any?(request)
|
|
189
|
+
|
|
190
|
+
error_for(request, JSONRPC::ProtocolErrors::INVALID_PARAMS)
|
|
155
191
|
end
|
|
156
192
|
|
|
157
193
|
def dispatch_notification(request, block, config)
|
|
158
194
|
method = request["method"]
|
|
159
195
|
params = request["params"]
|
|
160
|
-
|
|
161
|
-
result = with_timeout(config.timeout) { block.yield(method, params) }
|
|
162
|
-
config.after_dispatch&.call(method, params, result)
|
|
196
|
+
block.yield(method, params)
|
|
163
197
|
nil
|
|
164
198
|
rescue StandardError => e
|
|
165
199
|
config.on_error&.call(e, method, params)
|
|
@@ -169,35 +203,59 @@ module Clamo
|
|
|
169
203
|
def dispatch_request(request, block, config)
|
|
170
204
|
method = request["method"]
|
|
171
205
|
params = request["params"]
|
|
172
|
-
|
|
173
|
-
result = with_timeout(config.timeout) { block.yield(method, params) }
|
|
174
|
-
config.after_dispatch&.call(method, params, result)
|
|
206
|
+
result = block.yield(method, params)
|
|
175
207
|
JSONRPC.build_result_response(id: request["id"], result: result)
|
|
176
|
-
rescue Timeout::Error
|
|
177
|
-
timeout_error(request)
|
|
178
208
|
rescue StandardError => e
|
|
179
209
|
config.on_error&.call(e, method, params)
|
|
180
|
-
JSONRPC.build_error_response_from(id: request["id"], descriptor: JSONRPC::ProtocolErrors::
|
|
210
|
+
JSONRPC.build_error_response_from(id: request["id"], descriptor: JSONRPC::ProtocolErrors::SERVER_ERROR)
|
|
181
211
|
end
|
|
182
212
|
|
|
183
|
-
def
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
error: {
|
|
187
|
-
code: JSONRPC::ProtocolErrors::SERVER_ERROR.code,
|
|
188
|
-
message: JSONRPC::ProtocolErrors::SERVER_ERROR.message,
|
|
189
|
-
data: "Request timed out"
|
|
190
|
-
}
|
|
191
|
-
)
|
|
192
|
-
end
|
|
213
|
+
def params_match_arity?(object:, method:, params:)
|
|
214
|
+
ruby_method = resolve_method(object, method)
|
|
215
|
+
return true unless ruby_method
|
|
193
216
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
217
|
+
parameters = ruby_method.parameters
|
|
218
|
+
|
|
219
|
+
case params
|
|
220
|
+
when Array then array_params_match?(parameters, params.size)
|
|
221
|
+
when Hash then hash_params_match?(parameters, params.keys.map(&:to_sym))
|
|
222
|
+
when nil then nil_params_match?(parameters)
|
|
223
|
+
else true
|
|
199
224
|
end
|
|
200
225
|
end
|
|
226
|
+
|
|
227
|
+
def resolve_method(object, method_name)
|
|
228
|
+
object.method(method_name.to_sym)
|
|
229
|
+
rescue NameError
|
|
230
|
+
nil
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def array_params_match?(parameters, count)
|
|
234
|
+
by_type = parameters.group_by(&:first)
|
|
235
|
+
|
|
236
|
+
return false if by_type.key?(:keyreq)
|
|
237
|
+
return true if by_type.key?(:rest)
|
|
238
|
+
|
|
239
|
+
required = by_type.fetch(:req, []).size
|
|
240
|
+
count.between?(required, required + by_type.fetch(:opt, []).size)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def hash_params_match?(parameters, keys)
|
|
244
|
+
by_type = parameters.group_by(&:first)
|
|
245
|
+
required = by_type.fetch(:keyreq, []).map(&:last)
|
|
246
|
+
allowed = required + by_type.fetch(:key, []).map(&:last)
|
|
247
|
+
|
|
248
|
+
return false if by_type.key?(:req)
|
|
249
|
+
return false unless (required - keys).empty?
|
|
250
|
+
|
|
251
|
+
by_type.key?(:keyrest) || (keys - allowed).empty?
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def nil_params_match?(parameters)
|
|
255
|
+
by_type = parameters.group_by(&:first)
|
|
256
|
+
|
|
257
|
+
by_type.fetch(:req, []).empty? && !by_type.key?(:keyreq)
|
|
258
|
+
end
|
|
201
259
|
end
|
|
202
260
|
end
|
|
203
261
|
end
|
data/lib/clamo/version.rb
CHANGED
data/lib/clamo.rb
CHANGED
|
@@ -4,5 +4,10 @@ require_relative "clamo/version"
|
|
|
4
4
|
require_relative "clamo/jsonrpc"
|
|
5
5
|
require_relative "clamo/server"
|
|
6
6
|
|
|
7
|
+
# Clamo is a minimal, spec-compliant JSON-RPC 2.0 server for Ruby.
|
|
8
|
+
# Expose any module or class as a JSON-RPC service — public methods
|
|
9
|
+
# become callable via JSON-RPC requests.
|
|
10
|
+
#
|
|
11
|
+
# See Clamo::Server for the main entry points.
|
|
7
12
|
module Clamo
|
|
8
13
|
end
|
metadata
CHANGED
|
@@ -1,30 +1,17 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: clamo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.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
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
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'
|
|
26
|
-
description: Minimal, spec-compliant JSON-RPC 2.0 server for Ruby with request validation,
|
|
27
|
-
method dispatch, batch processing, and notification support.
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Minimal, spec-compliant JSON-RPC 2.0 server for Ruby. Expose any module
|
|
13
|
+
or class as a JSON-RPC service with request validation, batch processing, and notification
|
|
14
|
+
support.
|
|
28
15
|
email:
|
|
29
16
|
- Andriy.Tyurnikov@gmail.com
|
|
30
17
|
executables: []
|