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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c91f68aa4a88440e10a75a4ae3f138df400c10fd698b2d2b8c72990dc7e25f1
4
- data.tar.gz: bef5052025e0714e0297b20d95d1b90dfed7fce4e30a2fecd24cff9e8a84913c
3
+ metadata.gz: 410f469e07b16f8273149b1eded89c94692a91c8b41cd7c47642e897ca8d97fb
4
+ data.tar.gz: 2d6be4be393cfd70d5bf22109943b7735de5ca0abe93b65a888fe1a372640faa
5
5
  SHA512:
6
- metadata.gz: 96fa52c9f35c408c60a6827fa7886ee35fcea05af69fc6dde54aca497f0347c8de76bfd163fdb6bb4595f498fe9b55e9059e32d7212f997ec43cf5e77fafafef
7
- data.tar.gz: a7519eeba5bfd4074ada39cea72a03afa03cfc22809b53034eaf06c48d8a94a1122e136a6cf7b9452a27fb2f0ca54d9507db91a7bf498073b5f25e76cee74f45
6
+ metadata.gz: 385175b28fea11461b35a98860a6b8099bd9c88bc8dfef4f2603dc911a3d9e1709b51343d2b22ebb524ffc82aa03de7d8886a50b078c258d5fe28ec897a17296
7
+ data.tar.gz: 0c37f0971e24855610a91c8687d8e55ff32a03accf1effc73b6a1090118b46e3d78ffd9f0a7531418aa97a3154969bf7fb4d2dd0da9dc0cfe1b2b7aa94778cda
data/.rubocop.yml CHANGED
@@ -5,6 +5,12 @@ AllCops:
5
5
  Metrics/MethodLength:
6
6
  Enabled: false
7
7
 
8
+ Metrics/ModuleLength:
9
+ Enabled: false
10
+
11
+ Metrics/ClassLength:
12
+ Enabled: false
13
+
8
14
  Style/Documentation:
9
15
  Enabled: false
10
16
 
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
- # Handle a JSON-RPC request
30
- request_body = '{"jsonrpc": "2.0", "method": "add", "params": [1, 2], "id": 1}'
31
- response = Clamo::Server.unparsed_dispatch_to_object(
32
- request: request_body,
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
- puts response
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` via the `parsed_dispatch_to_object` method:
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 request.key?("params")
37
- request["params"].is_a?(Array) || request["params"].is_a?(Hash)
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 then object.public_send(method.to_sym, *params)
57
- when Hash then object.public_send(method.to_sym, **params.transform_keys(&:to_sym))
58
- when NilClass then object.public_send(method.to_sym)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clamo
4
- VERSION = "0.6.0"
4
+ VERSION = "0.7.0"
5
5
  end
data/lib/clamo.rb CHANGED
@@ -5,5 +5,4 @@ require_relative "clamo/jsonrpc"
5
5
  require_relative "clamo/server"
6
6
 
7
7
  module Clamo
8
- class Error < StandardError; end
9
8
  end
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.6.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 toolkit for Ruby
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
@@ -1,4 +0,0 @@
1
- module Clamo
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end