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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e1a61a818aab987fe6d621d2f3bf642fd7993044933b0b258f2a515d896bb7a5
4
- data.tar.gz: b68b313c737400cda3d739902d1d2d498a2d28344cf9cc0afc5c8b481850bcb5
3
+ metadata.gz: fb802891a70c42e2bb89446fd258fc7e9114ff29f64c4cc6051f819332172307
4
+ data.tar.gz: 10af0f3deb9d61fd068ebd9a1c6bdbc9f93aca02b8491b8c1856032338c5a56d
5
5
  SHA512:
6
- metadata.gz: b2a1ac30f5f40c035d683d54a36c0164ea24dc56b8ca9af694de499862732faf22cc572029e6bfac1f05413c4d4cf56a737b9b29af96b870fcf23eee5305e756
7
- data.tar.gz: ccb5a26f16ab0492e3d77e27eeed648e26b6fdcb9bda59c37a264cc3e419ba45a77bede078863e9ea18dface1f9f5c0be13a1629e6b37d4a8fb2da2f8cc291b4
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
- A Ruby implementation of [JSON-RPC 2.0](https://www.jsonrpc.org/specification) designed for simplicity and compliance with the specification.
3
+ [![CI](https://github.com/rubakas/clamo/actions/workflows/main.yml/badge.svg)](https://github.com/rubakas/clamo/actions/workflows/main.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/clamo.svg)](https://badge.fury.io/rb/clamo)
5
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.3-ruby.svg)](https://www.ruby-lang.org)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
+ [![JSON-RPC 2.0](https://img.shields.io/badge/JSON--RPC-2.0-orange.svg)](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.handle(
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.unparsed_dispatch_to_object(
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.parsed_dispatch_to_object(
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
- ### Handling Different Parameter Types
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
- ```ruby
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.unparsed_dispatch_to_object(
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.unparsed_dispatch_to_object(
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 | Reserved for implementation-defined server errors |
130
-
131
- ## Configuration
132
-
133
- ### Timeout
146
+ | -32000 | Server error | Exception raised by dispatched method |
134
147
 
135
- Every method dispatch is wrapped in a timeout. The default is 30 seconds. Timed-out requests return a `-32000 Server error` response.
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
- ```ruby
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`. 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:
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
- All configuration options can be overridden per-call. Module-level settings serve as defaults:
164
+ Configuration can be overridden per-call. Module-level settings serve as defaults:
169
165
 
170
166
  ```ruby
171
- Clamo::Server.handle(
167
+ Clamo::Server.handle_json(
172
168
  request: body,
173
169
  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) }
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 using the [parallel](https://github.com/grosser/parallel) gem. You can pass options to `Parallel.map`:
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.parsed_dispatch_to_object(
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
- 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
-
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(:timeout, :on_error, :before_dispatch, :after_dispatch)
22
+ Config = Data.define(:on_error)
10
23
 
11
24
  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
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
- def timeout
19
- return @timeout if defined?(@timeout)
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.handle(request: body, object: MyService)
32
+ # Clamo::Server.handle_json(request: body, object: MyService)
27
33
  #
28
- def handle(request:, object:, **)
29
- response = unparsed_dispatch_to_object(request: request, object: object, **)
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
- # Clamo::Server.unparsed_dispatch_to_object(
34
- # request: request_body,
35
- # object: MyModule
36
- # )
37
- def unparsed_dispatch_to_object(request:, object:, **)
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
- parsed_dispatch_to_object(request: parsed, object: object, **)
58
+ dispatch(request: parsed, object: object, **)
47
59
  end
48
60
 
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)
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(timeout: timeout, on_error: on_error,
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.key?("id") ? method_not_found_error(request) : nil
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 = Parallel.map(request, **opts) do |item|
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 validate_request_structure(request)
138
- unless JSONRPC.valid_request?(request)
139
- return JSONRPC.build_error_response_from(
140
- id: request.is_a?(Hash) ? request["id"] : nil,
141
- descriptor: JSONRPC::ProtocolErrors::INVALID_REQUEST
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
- return if JSONRPC.proper_params_if_any?(request)
172
+ def error_for(request, descriptor)
173
+ return unless request.key?("id")
146
174
 
147
- # Notifications must never produce a response, even for invalid params
148
- return nil unless request.key?("id")
175
+ JSONRPC.build_error_response_from(id: request["id"], descriptor: descriptor)
176
+ end
149
177
 
150
- JSONRPC.build_error_response_from(id: request["id"], descriptor: JSONRPC::ProtocolErrors::INVALID_PARAMS)
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 method_not_found_error(request)
154
- JSONRPC.build_error_response_from(id: request["id"], descriptor: JSONRPC::ProtocolErrors::METHOD_NOT_FOUND)
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
- config.before_dispatch&.call(method, params)
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
- config.before_dispatch&.call(method, params)
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::INTERNAL_ERROR)
210
+ JSONRPC.build_error_response_from(id: request["id"], descriptor: JSONRPC::ProtocolErrors::SERVER_ERROR)
181
211
  end
182
212
 
183
- def timeout_error(request)
184
- JSONRPC.build_error_response(
185
- id: request["id"],
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
- def with_timeout(seconds, &block)
195
- if seconds
196
- Timeout.timeout(seconds, &block)
197
- else
198
- block.call
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clamo
4
- VERSION = "0.9.0"
4
+ VERSION = "1.0.0"
5
5
  end
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.9.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
- - !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'
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: []