ultimate_json_rpc 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 57a4e2297d9a88e829782f6ba971a4d358cf342cf608e85cdd45b4532f1d5cc5
4
+ data.tar.gz: 561c9b24aaf6369bc54429592ac5a3d3ef21577eba4df66591badbbca6f4f274
5
+ SHA512:
6
+ metadata.gz: a66e18a36696028d540dfdeffbf3136b2b413fd0c326ffc0ca4878b5c24eb96c19820f765491739ebc0d9a33076d68856e54f9808364e3615a9ed754f2106ca5
7
+ data.tar.gz: 26f3451b0dc98ea82c04051a415efa68cf7d26131718ad051cf1842e898cb9a8b86b00ce3fe6685d3d191c4d5e9d61147727076abf41d3e3bf1a6238f8247b59
data/CHANGELOG.md ADDED
@@ -0,0 +1,88 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [1.0.0] - 2026-03-20
11
+
12
+ ### Added
13
+ - `TCP#port` accessor for discovering the bound port at runtime
14
+
15
+ ### Changed
16
+ - Gem renamed from `reclamo` to `ultimate_json_rpc` (module `Reclamo` → `UltimateJsonRpc`)
17
+ - `InvalidParams` error data is now gated by `expose_errors` (same as `InvalidRequest` and `ArgumentError`). `InvalidParams` is a protocol-level schema check, not an application validation error — use `ApplicationError` for business rule violations.
18
+
19
+ ### Fixed
20
+ - `Server#freeze` is now idempotent — calling freeze on an already-frozen server no longer raises `FrozenError`
21
+ - RBS type signatures now correctly reflect Core/Extras/Transport namespace structure
22
+ - RBS `Server` class no longer declares conditional `include` for extras
23
+ - Recorder `max_exchanges:` and TCP `MAX_LINE_BYTES` added to RBS type signatures
24
+ - `Docs#escape_cell` now escapes newlines in Markdown table cells
25
+ - Flaky profiler test with timing-dependent percentile assertion
26
+ - Batch-too-large error now uses standard `"Invalid Request"` message with reason in `data` field (JSON-RPC 2.0 conformance)
27
+
28
+ ## [0.3.0] - 2026-03-18
29
+
30
+ ### Added
31
+ - Method-level middleware: `server.use(only: ["admin.*"])` and `server.use(except: ["ping"])` to scope middleware to specific methods or namespaces, with glob pattern support
32
+ - `UltimateJsonRpc::Transport::Rack` built-in Rack adapter: Content-Type handling, 200/204/405 responses, `require "ultimate_json_rpc/transport/rack"` to opt in
33
+ - Test assertion helpers: `assert_rpc_success`, `assert_rpc_error`, `assert_rpc_notification` in `ultimate_json_rpc/test_helpers`
34
+ - `UltimateJsonRpc::Transport::Stdio` adapter: newline-delimited JSON-RPC over stdin/stdout with signal handling, `require "ultimate_json_rpc/transport/stdio"`
35
+ - Return type annotations: `returns:` keyword on `expose_method` and `returns:` hash on `expose`, appears in `rpc.discover`
36
+ - OpenRPC 1.3.2 schema: `rpc.discover` now returns a full OpenRPC document with `openrpc` version, `info` object, and `result` contentDescriptors
37
+ - Instrumentation hooks: `server.on(:request)`, `on(:response)`, `on(:error)` for read-only lifecycle observability with duration timing
38
+ - Request timeout: `Server.new(timeout: 5)` wraps dispatch in `Timeout.timeout`, returns `-32001 Request timeout` on expiry
39
+ - `RequestTimeout` error class and `REQUEST_TIMEOUT` (-32001) constant
40
+ - Method deprecation markers: `deprecated: true` or `deprecated: "Use v2"` on `expose_method` and `expose`, appears in `rpc.discover`
41
+ - Parameter validation: `params_schema:` on `expose_method` and `expose` for type and enum validation before dispatch, returns `-32602 Invalid params` on mismatch, schemas appear in `rpc.discover` param descriptors
42
+ - Concurrent batch execution: `Server.new(concurrent_batches: true)` processes batch items in parallel using threads
43
+ - Error catalog: `server.register_error(code, name, description)` registers application error codes that appear in `rpc.discover` under `components.errors`
44
+ - Custom JSON serializer: `Server.new(json: Oj)` to swap JSON encoder/decoder, any object responding to `parse` and `generate`
45
+ - Method-level authorization: `server.authorize("admin.*") { |req| req.context[:role] == :admin }` with glob patterns and custom error codes
46
+ - Structured logging: `require "ultimate_json_rpc/extras/logging"` provides `UltimateJsonRpc::Extras::Logging.new(server, logger)` for logger-agnostic observability
47
+ - Request/response recorder: `require "ultimate_json_rpc/extras/recorder"` provides `UltimateJsonRpc::Extras::Recorder.new(server)` capturing exchanges for replay testing, with optional JSONL file output
48
+ - MCP (Model Context Protocol) adapter: `require "ultimate_json_rpc/extras/mcp"` provides `UltimateJsonRpc::Extras::MCP.new(server)` for AI tool integration over stdio, mapping methods to MCP tools with schema support
49
+ - Rate limiting: `require "ultimate_json_rpc/extras/rate_limit"` provides `UltimateJsonRpc::Extras::RateLimiter.new(server, max:, period:)` with sliding window, per-caller keying, and per-method scoping
50
+ - Per-method profiling: `require "ultimate_json_rpc/extras/profiler"` provides `UltimateJsonRpc::Extras::Profiler.new(server)` collecting count, min/max/avg, and p50/p95/p99 per method
51
+ - TCP server adapter: `require "ultimate_json_rpc/transport/tcp"` provides `UltimateJsonRpc::Transport::TCP.new(server, port: 4000)` for newline-delimited JSON-RPC over TCP with multi-client threading
52
+ - API documentation generation: `require "ultimate_json_rpc/extras/docs"` provides `UltimateJsonRpc::Extras::Docs.new(server).to_markdown` generating Markdown from OpenRPC schema
53
+ - Usage examples: `examples/` directory with runnable patterns for Rack, MCP, multi-namespace/versioning, error handling, and testing
54
+ - WebSocket adapter: `require "ultimate_json_rpc/transport/websocket"` provides `UltimateJsonRpc::Transport::WebSocket` for JSON-RPC over WebSockets with any Rack-compatible library
55
+
56
+ ### Fixed
57
+ - MCP `inputSchema` now always included for zero-parameter tools (MCP spec compliance)
58
+ - `handle_parsed` no longer crashes with `TypeError` on Symbol-keyed hashes
59
+
60
+ ### Changed
61
+ - `rpc.discover` output restructured: service metadata moved to `info` object (`name` → `info.title`), method `returns` → `result` in OpenRPC contentDescriptor format
62
+
63
+ ## [0.2.0] - 2026-03-18
64
+
65
+ ### Added
66
+ - `InvalidParams` error class for explicit -32602 errors from user code
67
+ - `expose_method` accepts callable objects (Method, Proc, lambda) as first argument
68
+ - `Server#empty?` and `Handler#empty?` convenience methods
69
+ - Callable validation: `expose_method` checks that callable responds to `#call`
70
+
71
+ ### Changed
72
+ - `MethodNotFound` now has a descriptive Ruby exception message ("Method not found: name")
73
+ - `ApplicationError` validates that error code is an Integer
74
+ - Request params and id are frozen after construction for immutability
75
+ - `ServerError` uses `between?` for range validation (consistency)
76
+
77
+ ## [0.1.0] - 2025-05-01
78
+
79
+ ### Added
80
+ - Initial release
81
+ - JSON-RPC 2.0 server with `expose`, `expose_method`, and middleware support
82
+ - `rpc.discover` built-in introspection
83
+ - Batch request handling
84
+ - `Server#freeze` for thread-safe immutability
85
+ - `Server#call` alias and `#to_proc` for Rack-like usage
86
+ - `only:`/`except:` method filtering
87
+ - Method descriptions via `descriptions:` option
88
+ - Service metadata via `name:` and `version:` options
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,478 @@
1
+ # UltimateJsonRpc
2
+
3
+ [![CI](https://github.com/rubakas/ultimate_json_rpc/actions/workflows/main.yml/badge.svg)](https://github.com/rubakas/ultimate_json_rpc/actions/workflows/main.yml)
4
+
5
+ Network-agnostic JSON-RPC 2.0 server that exposes Ruby modules, classes, and instances as callable RPC endpoints. UltimateJsonRpc handles JSON-RPC message parsing, method dispatch, and response serialization — transport (HTTP, WebSocket, stdio, TCP, etc.) is the caller's responsibility.
6
+
7
+ ## Installation
8
+
9
+ Add to your Gemfile:
10
+
11
+ ```ruby
12
+ gem "ultimate_json_rpc"
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```ruby
18
+ require "ultimate_json_rpc"
19
+
20
+ # Define your service
21
+ module Calculator
22
+ def self.add(a, b) = a + b
23
+ def self.divide(a, b) = a.to_f / b
24
+ end
25
+
26
+ # Create a server and expose objects
27
+ server = UltimateJsonRpc::Server.new(name: "My API", version: "1.0")
28
+ server.expose(Calculator, descriptions: { add: "Add two numbers" })
29
+ server.expose(some_instance, namespace: "greeter")
30
+
31
+ # Handle a JSON-RPC request string, get a JSON-RPC response string
32
+ request = '{"jsonrpc":"2.0","method":"add","params":[2,3],"id":1}'
33
+ response = server.handle(request)
34
+ # => '{"jsonrpc":"2.0","result":5,"id":1}'
35
+ ```
36
+
37
+ ### Expose individual methods
38
+
39
+ ```ruby
40
+ server.expose_method("double", description: "Double a number") { |n| n * 2 }
41
+ server.expose_method("greet") { |name:, greeting: "Hello"| "#{greeting}, #{name}!" }
42
+ ```
43
+
44
+ ### Chainable API
45
+
46
+ All setup methods return `self`, and the server is callable via `to_proc`:
47
+
48
+ ```ruby
49
+ server = UltimateJsonRpc::Server.new
50
+ .expose(Calculator, namespace: "calc")
51
+ .expose_method("ping") { "pong" }
52
+ .use { |req, next_call| next_call.call }
53
+
54
+ # Use to_proc for mapping over requests
55
+ responses = json_requests.map(&server)
56
+ ```
57
+
58
+ ### Return type annotations
59
+
60
+ Declare return types for discovery metadata (purely informational, no runtime enforcement):
61
+
62
+ ```ruby
63
+ server.expose_method("add", returns: { "type" => "number" }) { |a, b| a + b }
64
+ server.expose(Calculator, returns: { add: { "type" => "number" } })
65
+ ```
66
+
67
+ ### Service discovery
68
+
69
+ Built-in `rpc.discover` returns an [OpenRPC](https://open-rpc.org/) 1.3.2-compatible document:
70
+
71
+ ```ruby
72
+ request = '{"jsonrpc":"2.0","method":"rpc.discover","id":1}'
73
+ server.handle(request)
74
+ # => '{"jsonrpc":"2.0","result":{"openrpc":"1.3.2","info":{"title":"My API","version":"1.0"},"methods":[...]},"id":1}'
75
+ ```
76
+
77
+ ### Middleware
78
+
79
+ Add cross-cutting concerns like logging, auth, or rate limiting:
80
+
81
+ ```ruby
82
+ server.use do |request, next_call|
83
+ puts "Calling #{request.method_name}"
84
+ result = next_call.call
85
+ puts "Done"
86
+ result
87
+ end
88
+
89
+ # Reject unauthorized calls
90
+ server.use do |request, next_call|
91
+ raise UltimateJsonRpc::ApplicationError.new(code: 403, message: "Forbidden") unless authorized?(request)
92
+
93
+ next_call.call
94
+ end
95
+ ```
96
+
97
+ Middleware runs in registration order (first registered = outermost wrapper).
98
+
99
+ #### Scoped middleware
100
+
101
+ Target specific methods or namespaces with `only:` / `except:`, supporting glob patterns:
102
+
103
+ ```ruby
104
+ # Only run for admin namespace methods
105
+ server.use(only: ["admin.*"]) do |request, next_call|
106
+ raise UltimateJsonRpc::ApplicationError.new(code: 403, message: "Forbidden") unless admin?(request)
107
+ next_call.call
108
+ end
109
+
110
+ # Run for everything except health checks
111
+ server.use(except: ["ping", "health"]) do |request, next_call|
112
+ log(request)
113
+ next_call.call
114
+ end
115
+ ```
116
+
117
+ Middleware can pass data to handlers via `request.context`:
118
+
119
+ ```ruby
120
+ server.use do |request, next_call|
121
+ request.context[:user] = authenticate(request)
122
+ next_call.call
123
+ end
124
+ ```
125
+
126
+ ### Instrumentation hooks
127
+
128
+ Read-only lifecycle hooks for observability — they can't alter responses or swallow errors:
129
+
130
+ ```ruby
131
+ server.on(:request) { |request| puts "→ #{request.method_name}" }
132
+ server.on(:response) { |request, result, duration| puts "← #{request.method_name} (#{duration}s)" }
133
+ server.on(:error) { |request, error, duration| log_error(error, method: request.method_name) }
134
+ ```
135
+
136
+ Hook errors are rescued and logged via `Kernel.warn`, never breaking dispatch.
137
+
138
+ ### Custom JSON serializer
139
+
140
+ Swap the JSON encoder/decoder (defaults to stdlib `JSON`):
141
+
142
+ ```ruby
143
+ require "oj"
144
+ Oj.mimic_JSON
145
+ server = UltimateJsonRpc::Server.new(json: Oj)
146
+ ```
147
+
148
+ Any object responding to `parse(string)` and `generate(object)` works.
149
+
150
+ ### Concurrent batches
151
+
152
+ Opt-in parallel processing for batch requests:
153
+
154
+ ```ruby
155
+ server = UltimateJsonRpc::Server.new(concurrent_batches: true)
156
+ # Batch items are processed in parallel using threads
157
+ # Respects max_batch_size and request timeout
158
+ ```
159
+
160
+ ### Request timeout
161
+
162
+ Set a per-server timeout (in seconds) to prevent slow handlers from blocking:
163
+
164
+ ```ruby
165
+ server = UltimateJsonRpc::Server.new(timeout: 5)
166
+ # Handlers exceeding 5 seconds receive a -32001 "Request timeout" error
167
+ ```
168
+
169
+ ### Parameter validation
170
+
171
+ Declare parameter schemas to validate incoming params before dispatch:
172
+
173
+ ```ruby
174
+ server.expose_method("add", params_schema: {
175
+ a: { "type" => "number" },
176
+ b: { "type" => "number" }
177
+ }) { |a, b| a + b }
178
+
179
+ server.expose(Calculator, params_schema: {
180
+ add: { left: { "type" => "number" }, right: { "type" => "number" } }
181
+ })
182
+ ```
183
+
184
+ Supported JSON Schema keywords: `type` (`string`, `number`, `integer`, `boolean`, `array`, `object`, `null`) and `enum`. On mismatch, returns `-32602 Invalid params` with a descriptive message. Schemas also appear in `rpc.discover` output.
185
+
186
+ ### Method deprecation
187
+
188
+ Mark methods as deprecated in discovery metadata (purely informational, no runtime enforcement):
189
+
190
+ ```ruby
191
+ server.expose_method("old_add", deprecated: true) { |a, b| a + b }
192
+ server.expose_method("old_multiply", deprecated: "Use multiply_v2 instead") { |a, b| a * b }
193
+ server.expose(Calculator, deprecated: { add: "Use add_v2" })
194
+ ```
195
+
196
+ Deprecated methods still work normally but appear flagged in `rpc.discover` output.
197
+
198
+ ### Per-method profiling
199
+
200
+ Collect per-method timing statistics:
201
+
202
+ ```ruby
203
+ require "ultimate_json_rpc/extras/profiler"
204
+
205
+ profiler = UltimateJsonRpc::Extras::Profiler.new(server)
206
+ # ... handle requests ...
207
+ profiler["add"] # => {count: 150, min: 0.0001, max: 0.05, avg: 0.002, p50: ..., p95: ..., p99: ...}
208
+ profiler.stats # => all methods
209
+ profiler.reset # clear data
210
+ ```
211
+
212
+ ### Structured logging
213
+
214
+ Logger-agnostic observability:
215
+
216
+ ```ruby
217
+ require "ultimate_json_rpc/extras/logging"
218
+
219
+ UltimateJsonRpc::Extras::Logging.new(server, Logger.new($stdout)) # logs at INFO by default
220
+ UltimateJsonRpc::Extras::Logging.new(server, Rails.logger, level: :debug) # custom level
221
+ ```
222
+
223
+ Successful responses log at the specified level; errors always log at ERROR.
224
+
225
+ ### Rate limiting
226
+
227
+ Sliding-window rate limiting with per-caller keying:
228
+
229
+ ```ruby
230
+ require "ultimate_json_rpc/extras/rate_limit"
231
+
232
+ UltimateJsonRpc::Extras::RateLimiter.new(server, max: 100, period: 60) # 100 req/min global
233
+ UltimateJsonRpc::Extras::RateLimiter.new(server, max: 10, period: 60, only: ["expensive.*"]) # per-method
234
+ UltimateJsonRpc::Extras::RateLimiter.new(server, max: 50, period: 60, key: :api_key) # per-caller via context
235
+ ```
236
+
237
+ ### Authorization
238
+
239
+ Declarative access control with glob patterns:
240
+
241
+ ```ruby
242
+ server.authorize("admin.*") { |req| req.context[:role] == :admin }
243
+ server.authorize("users.delete", code: 1001, message: "Insufficient permissions") do |req|
244
+ req.context[:permissions]&.include?("delete")
245
+ end
246
+ ```
247
+
248
+ Returns `ApplicationError` (code 403 by default) when the block returns falsy.
249
+
250
+ ### Error catalog
251
+
252
+ Register application-specific error codes for discovery:
253
+
254
+ ```ruby
255
+ server.register_error(code: 42, message: "InsufficientFunds", description: "Account balance too low")
256
+ server.register_error(code: 43, message: "AccountLocked")
257
+ ```
258
+
259
+ Registered errors appear in `rpc.discover` under `components.errors`, so consumers know which error codes to expect.
260
+
261
+ ### Error handling
262
+
263
+ Raise `ApplicationError` for custom error codes, or `ServerError` for implementation-defined errors:
264
+
265
+ ```ruby
266
+ raise UltimateJsonRpc::ApplicationError.new(code: 42, message: "Custom error", data: { "detail" => "info" })
267
+ raise UltimateJsonRpc::ServerError.new(code: -32_001, message: "Server shutting down")
268
+ ```
269
+
270
+ Ruby's `ArgumentError` automatically maps to JSON-RPC Invalid params (`-32602`). All other exceptions become Internal error (`-32603`).
271
+
272
+ ### Error visibility
273
+
274
+ By default, internal error details (exception messages) are hidden from clients:
275
+
276
+ ```ruby
277
+ server = UltimateJsonRpc::Server.new # expose_errors: false (default)
278
+ # Internal errors return generic "Internal server error" in the data field
279
+
280
+ server = UltimateJsonRpc::Server.new(expose_errors: true)
281
+ # Internal errors include the actual exception message in the data field
282
+ ```
283
+
284
+ `ApplicationError`, `ServerError`, and `MethodNotFound` always expose their details regardless of this setting, since those are intentionally raised by your code.
285
+
286
+ Protocol-level errors (`InvalidRequest`, `InvalidParams`, `ArgumentError`, and unhandled exceptions) are gated by `expose_errors`. `InvalidParams` is a JSON-RPC schema check (type mismatch, enum violation) — for application-level business validation, raise `ApplicationError` instead.
287
+
288
+ ### Freezing
289
+
290
+ Lock the server after setup to prevent accidental modifications:
291
+
292
+ ```ruby
293
+ server.freeze # expose, expose_method, use will now raise FrozenError
294
+ server.handle(request) # still works
295
+ ```
296
+
297
+ ### Features
298
+
299
+ - **JSON-RPC 2.0** compliant (requests, notifications, batch requests)
300
+ - **Expose modules** — singleton methods become RPC methods
301
+ - **Expose instances** — public methods become RPC methods
302
+ - **Expose blocks** — `server.expose_method("name") { ... }` for standalone methods
303
+ - **Namespacing** — `server.expose(obj, namespace: "ns")` makes methods callable as `ns.method_name`
304
+ - **Method filtering** — `server.expose(obj, only: [:add])` or `except: [:internal]`
305
+ - **Method descriptions** — `descriptions:` hash or `description:` keyword for discovery
306
+ - **Return type annotations** — `returns:` hash or keyword for discovery metadata
307
+ - **Service metadata** — `name:` and `version:` appear in `rpc.discover` responses
308
+ - **Positional and keyword params** — arrays map to positional args, objects map to keyword args
309
+ - **Service discovery** — built-in `rpc.discover` returns OpenRPC 1.3.2-compatible schema
310
+ - **Middleware** — `server.use { |request, next_call| ... }` for cross-cutting concerns
311
+ - **Scoped middleware** — `only:` / `except:` with glob patterns to target specific methods or namespaces
312
+ - **Instrumentation hooks** — `on(:request)`, `on(:response)`, `on(:error)` for read-only observability
313
+ - **Parameter validation** — `params_schema:` with JSON Schema types and enum constraints
314
+ - **Request timeout** — `Server.new(timeout: 5)` prevents slow handlers from blocking
315
+ - **Method deprecation** — mark methods as deprecated in discovery metadata
316
+ - **Error catalog** — `register_error` documents app error codes in `rpc.discover`
317
+ - **Authorization** — `authorize("admin.*") { |req| ... }` for declarative access control
318
+ - **Error handling** — standard JSON-RPC error codes, `ApplicationError`, and `ServerError`
319
+ - **Error visibility** — `expose_errors: true` to include exception messages in error responses
320
+ - **Rate limiting** — sliding-window `rate_limit(max:, period:)` with per-caller keying
321
+ - **Security** — dangerous methods (eval, system, exec, etc.) are automatically blocked
322
+ - **Chainable API** — all setup methods return `self`
323
+ - **Callable** — `to_proc` enables `requests.map(&server)`
324
+ - **Rack adapter** — `UltimateJsonRpc::Transport::Rack.new(server)` for instant HTTP deployment
325
+ - **MCP adapter** — `UltimateJsonRpc::Extras::MCP.new(server).run` for AI tool integration (Claude, Cursor, etc.)
326
+ - **stdio adapter** — `UltimateJsonRpc::Transport::Stdio.new(server).run` for CLI/MCP-style integrations
327
+ - **Test helpers** — `rpc_call`, `assert_rpc_success`, `assert_rpc_error` for cleaner tests
328
+ - **Concurrent batches** — `concurrent_batches: true` processes batch items in parallel
329
+ - **Custom JSON** — `json: Oj` to swap the JSON encoder/decoder
330
+ - **Batch size limit** — `max_batch_size: 100` (default) prevents oversized batch requests
331
+ - **Freezable** — `server.freeze` locks configuration after setup
332
+
333
+ ### Transport examples
334
+
335
+ UltimateJsonRpc is transport-agnostic. Here are common setups:
336
+
337
+ #### Rack (HTTP)
338
+
339
+ Use the built-in Rack adapter:
340
+
341
+ ```ruby
342
+ # config.ru
343
+ require "ultimate_json_rpc"
344
+ require "ultimate_json_rpc/transport/rack"
345
+
346
+ server = UltimateJsonRpc::Server.new(name: "My API")
347
+ server.expose(Calculator)
348
+
349
+ run UltimateJsonRpc::Transport::Rack.new(server)
350
+ ```
351
+
352
+ `UltimateJsonRpc::Transport::Rack` handles Content-Type, returns 200 for responses, 204 for notifications, and 405 for non-POST requests.
353
+
354
+ #### MCP (Model Context Protocol)
355
+
356
+ Expose methods as AI tools via MCP:
357
+
358
+ ```ruby
359
+ require "ultimate_json_rpc/extras/mcp"
360
+
361
+ server = UltimateJsonRpc::Server.new(name: "My Tools", version: "1.0")
362
+ server.expose(Calculator, descriptions: { add: "Add two numbers" })
363
+
364
+ UltimateJsonRpc::Extras::MCP.new(server).run
365
+ ```
366
+
367
+ `UltimateJsonRpc::Extras::MCP` handles the MCP lifecycle (`initialize`, `tools/list`, `tools/call`), maps methods to tool definitions with input schemas, and runs over stdio for integration with Claude, Cursor, and other AI tools.
368
+
369
+ #### WebSocket
370
+
371
+ Use the WebSocket adapter with any Rack-compatible library (e.g., `faye-websocket`):
372
+
373
+ ```ruby
374
+ require "ultimate_json_rpc/transport/websocket"
375
+
376
+ ws_handler = UltimateJsonRpc::Transport::WebSocket.new(server)
377
+
378
+ # In your Rack app:
379
+ ws = Faye::WebSocket.new(env)
380
+ ws_handler.call(env, ws)
381
+ ```
382
+
383
+ See `examples/websocket_server.ru` for a complete runnable example.
384
+
385
+ #### TCP
386
+
387
+ Use the built-in TCP adapter for internal microservices:
388
+
389
+ ```ruby
390
+ require "ultimate_json_rpc/transport/tcp"
391
+
392
+ server = UltimateJsonRpc::Server.new
393
+ server.expose(Calculator)
394
+
395
+ UltimateJsonRpc::Transport::TCP.new(server, port: 4000).run
396
+ ```
397
+
398
+ `UltimateJsonRpc::Transport::TCP` accepts newline-delimited JSON-RPC over TCP, handles multiple concurrent clients via threads, and supports `SIGINT`/`SIGTERM` for graceful shutdown.
399
+
400
+ #### stdio
401
+
402
+ Use the built-in stdio adapter:
403
+
404
+ ```ruby
405
+ require "ultimate_json_rpc"
406
+ require "ultimate_json_rpc/transport/stdio"
407
+
408
+ server = UltimateJsonRpc::Server.new
409
+ server.expose(Calculator)
410
+
411
+ UltimateJsonRpc::Transport::Stdio.new(server).run
412
+ ```
413
+
414
+ `UltimateJsonRpc::Transport::Stdio` reads newline-delimited JSON-RPC from stdin, writes responses to stdout, skips empty lines, and handles `SIGINT`/`SIGTERM` for graceful shutdown.
415
+
416
+ ### Pre-parsed input
417
+
418
+ If you've already parsed the JSON (e.g. from a WebSocket frame), use `handle_parsed` to skip the parse step:
419
+
420
+ ```ruby
421
+ data = JSON.parse(raw_json)
422
+ response = server.handle_parsed(data)
423
+ ```
424
+
425
+ ### Request/response recording
426
+
427
+ Capture exchanges for replay and regression testing:
428
+
429
+ ```ruby
430
+ require "ultimate_json_rpc/extras/recorder"
431
+
432
+ recorder = UltimateJsonRpc::Extras::Recorder.new(server)
433
+ server.handle(request_json)
434
+ recorder.exchanges # => [{"method" => "add", "params" => [2, 3], "result" => 5, "duration" => 0.001}]
435
+
436
+ # Write JSONL to a file
437
+ recorder = UltimateJsonRpc::Extras::Recorder.new(server, output: File.open("exchanges.jsonl", "a"))
438
+ ```
439
+
440
+ ### Test helpers
441
+
442
+ UltimateJsonRpc ships with optional test helpers for cleaner assertions:
443
+
444
+ ```ruby
445
+ require "ultimate_json_rpc/extras/test_helpers"
446
+
447
+ class MyTest < Minitest::Test
448
+ include UltimateJsonRpc::Extras::TestHelpers
449
+
450
+ def test_addition
451
+ response = rpc_call(server, "add", params: [2, 3])
452
+ assert_rpc_success response, 5
453
+ end
454
+
455
+ def test_method_not_found
456
+ response = rpc_call(server, "nonexistent")
457
+ assert_rpc_error response, code: -32_601
458
+ end
459
+
460
+ def test_notification
461
+ assert_rpc_notification server, "add", params: [1, 2]
462
+ end
463
+ end
464
+ ```
465
+
466
+ ## Development
467
+
468
+ ```bash
469
+ bin/setup # Install dependencies
470
+ rake test # Run tests
471
+ rake rubocop # Run linter
472
+ rake # Run both
473
+ bin/console # Interactive console
474
+ ```
475
+
476
+ ## Contributing
477
+
478
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rubakas/ultimate_json_rpc.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Error handling patterns
5
+ #
6
+ # Run: ruby examples/error_handling.rb
7
+
8
+ require "ultimate_json_rpc"
9
+ require "ultimate_json_rpc/extras/logging"
10
+ require "json"
11
+ require "logger"
12
+
13
+ module BankService
14
+ def self.transfer(from:, to:, amount:)
15
+ raise UltimateJsonRpc::Core::ApplicationError.new(code: 1001, message: "Insufficient funds", data: { balance: 50 }) if amount > 100
16
+ raise UltimateJsonRpc::Core::ApplicationError.new(code: 1002, message: "Account frozen") if from == "frozen"
17
+
18
+ { from:, to:, amount:, status: "completed" }
19
+ end
20
+ end
21
+
22
+ server = UltimateJsonRpc::Server.new(name: "Bank API", version: "1.0", expose_errors: true)
23
+ server.expose(BankService, descriptions: { transfer: "Transfer money between accounts" })
24
+ server.register_error(code: 1001, message: "InsufficientFunds", description: "Account balance too low for transfer")
25
+ server.register_error(code: 1002, message: "AccountFrozen", description: "Account is frozen and cannot transact")
26
+ UltimateJsonRpc::Extras::Logging.new(server, Logger.new($stdout, level: :info))
27
+
28
+ # Successful transfer
29
+ puts "=== Successful transfer ==="
30
+ puts server.handle(JSON.generate({
31
+ "jsonrpc" => "2.0", "method" => "transfer",
32
+ "params" => { "from" => "alice", "to" => "bob", "amount" => 50 }, "id" => 1
33
+ }))
34
+
35
+ # Application error — insufficient funds
36
+ puts "\n=== Insufficient funds ==="
37
+ puts server.handle(JSON.generate({
38
+ "jsonrpc" => "2.0", "method" => "transfer",
39
+ "params" => { "from" => "alice", "to" => "bob", "amount" => 200 }, "id" => 2
40
+ }))
41
+
42
+ # Application error — frozen account
43
+ puts "\n=== Frozen account ==="
44
+ puts server.handle(JSON.generate({
45
+ "jsonrpc" => "2.0", "method" => "transfer",
46
+ "params" => { "from" => "frozen", "to" => "bob", "amount" => 10 }, "id" => 3
47
+ }))
48
+
49
+ # Method not found
50
+ puts "\n=== Method not found ==="
51
+ puts server.handle(JSON.generate({ "jsonrpc" => "2.0", "method" => "withdraw", "id" => 4 }))