clamo 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +77 -0
- data/README.md +124 -11
- data/lib/clamo/jsonrpc.rb +34 -32
- data/lib/clamo/server.rb +85 -75
- data/lib/clamo/version.rb +1 -1
- data/lib/clamo.rb +1 -1
- metadata +10 -11
- data/lib/clamo/client.rb +0 -5
- data/lib/clamo/gpt_json_rpc_server.rb +0 -186
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1c91f68aa4a88440e10a75a4ae3f138df400c10fd698b2d2b8c72990dc7e25f1
|
|
4
|
+
data.tar.gz: bef5052025e0714e0297b20d95d1b90dfed7fce4e30a2fecd24cff9e8a84913c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 96fa52c9f35c408c60a6827fa7886ee35fcea05af69fc6dde54aca497f0347c8de76bfd163fdb6bb4595f498fe9b55e9059e32d7212f997ec43cf5e77fafafef
|
|
7
|
+
data.tar.gz: a7519eeba5bfd4074ada39cea72a03afa03cfc22809b53034eaf06c48d8a94a1122e136a6cf7b9452a27fb2f0ca54d9507db91a7bf498073b5f25e76cee74f45
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
|
|
7
|
+
## [0.6.0] - 2026-03-14
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `Clamo::Server.handle` — JSON string in, JSON string out entry point for HTTP/socket integrations
|
|
12
|
+
- `Clamo::Server.on_error` callback for notification failure reporting
|
|
13
|
+
- `Clamo::Error` base exception class
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Notifications now dispatch synchronously instead of spawning a background thread per call; callers control their own concurrency
|
|
18
|
+
- `build_error_response_from` accepts explicit `descriptor:` and `id:` keyword arguments instead of `**opts`
|
|
19
|
+
- `build_error_response_parse_error` takes no arguments (always returns `id: nil`)
|
|
20
|
+
- `parallel` dependency relaxed from `~> 1.27.0` to `~> 1.27`
|
|
21
|
+
- Minimum Ruby version raised from 3.0 to 3.3
|
|
22
|
+
|
|
23
|
+
### Removed
|
|
24
|
+
|
|
25
|
+
- `JSONRPC.valid_params?` (use `JSONRPC.proper_params_if_any?` directly)
|
|
26
|
+
- `JSONRPC::PROTOCOL_VERSION_PRAGMA` constant (unused)
|
|
27
|
+
- `JSONRPC::ProtocolErrors::SERVER_ERROR_CODE_RANGE` constant (unused)
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **Security:** replaced `send` with `public_send` to prevent remote invocation of private methods
|
|
32
|
+
- **Security:** `method_known?` now uses `public_methods(false)` to expose only explicitly defined methods
|
|
33
|
+
- Notifications now validate method existence before dispatch (previously skipped validation)
|
|
34
|
+
- Empty batch requests correctly return Invalid Request error per spec
|
|
35
|
+
- All-notification batches return `nil` instead of empty array per spec
|
|
36
|
+
- `build_error_response_from` no longer leaks the `:descriptor` key into the error response builder
|
|
37
|
+
|
|
38
|
+
### Internal
|
|
39
|
+
|
|
40
|
+
- `method_known?`, `dispatch_to_ruby`, `response_for` moved from public to private API
|
|
41
|
+
- `response_for_single_request` extracted into focused private helpers
|
|
42
|
+
- Test suite expanded from scaffold to 62 tests / 84 assertions covering validation, dispatch, security, error handling, batching, notifications, and argument edge cases
|
|
43
|
+
- CI matrix set to Ruby 3.3, 3.4, 4.0
|
|
44
|
+
|
|
45
|
+
## [0.5.0] - 2025-02-07
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- Updated README with detailed usage examples, error table, and batch/notification documentation
|
|
50
|
+
|
|
51
|
+
## [0.4.0] - 2025-02-07
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- Corrected gem metadata URLs
|
|
56
|
+
|
|
57
|
+
## [0.3.0] - 2025-02-06
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
- Batch request support via `parallel` gem
|
|
62
|
+
- Named parameter (Hash) dispatch
|
|
63
|
+
- JSON-RPC 2.0 validation (pragma, method, id, params)
|
|
64
|
+
- Protocol error constants (`PARSE_ERROR`, `INVALID_REQUEST`, etc.)
|
|
65
|
+
- RuboCop configuration
|
|
66
|
+
|
|
67
|
+
### Changed
|
|
68
|
+
|
|
69
|
+
- Multiple version bumps during initial development
|
|
70
|
+
|
|
71
|
+
## [0.1.0] - 2025-02-06
|
|
72
|
+
|
|
73
|
+
### Added
|
|
74
|
+
|
|
75
|
+
- Initial release
|
|
76
|
+
- Basic JSON-RPC 2.0 server with positional parameter dispatch
|
|
77
|
+
- `Clamo::JSONRPC` request/response builders
|
data/README.md
CHANGED
|
@@ -1,29 +1,142 @@
|
|
|
1
1
|
# Clamo
|
|
2
2
|
|
|
3
|
-
JSON-RPC
|
|
3
|
+
A Ruby implementation of [JSON-RPC 2.0](https://www.jsonrpc.org/specification) designed for simplicity and compliance with the specification.
|
|
4
4
|
|
|
5
|
-
Consume, Serve or test JSON-RPC endpoints with Clamo.
|
|
6
5
|
|
|
7
|
-
##
|
|
6
|
+
## Usage
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
### Basic Usage
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
```ruby
|
|
11
|
+
require 'clamo'
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
# Define a service object with methods you want to expose
|
|
14
|
+
module MyService
|
|
15
|
+
def self.add(a, b)
|
|
16
|
+
a + b
|
|
17
|
+
end
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
def self.subtract(a:, b:)
|
|
20
|
+
a - b
|
|
21
|
+
end
|
|
16
22
|
|
|
17
|
-
|
|
23
|
+
# Private methods won't be accessible via JSON-RPC
|
|
24
|
+
private_class_method def self.internal_method
|
|
25
|
+
# This won't be exposed
|
|
26
|
+
end
|
|
27
|
+
end
|
|
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,
|
|
33
|
+
object: MyService
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
puts response
|
|
37
|
+
# => {jsonrpc: "2.0", result: 3, id: 1}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Handling Different Parameter Types
|
|
41
|
+
|
|
42
|
+
Clamo supports both positional (array) and named (object/hash) parameters:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
# Positional parameters
|
|
46
|
+
request = '{"jsonrpc": "2.0", "method": "add", "params": [1, 2], "id": 1}'
|
|
47
|
+
|
|
48
|
+
# Named parameters
|
|
49
|
+
request = '{"jsonrpc": "2.0", "method": "subtract", "params": {"a": 5, "b": 3}, "id": 2}'
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Batch Requests
|
|
53
|
+
|
|
54
|
+
Clamo handles batch requests automatically:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
batch_request = <<~JSON
|
|
58
|
+
[
|
|
59
|
+
{"jsonrpc": "2.0", "method": "add", "params": [1, 2], "id": 1},
|
|
60
|
+
{"jsonrpc": "2.0", "method": "subtract", "params": {"a": 5, "b": 3}, "id": 2}
|
|
61
|
+
]
|
|
62
|
+
JSON
|
|
63
|
+
|
|
64
|
+
batch_response = Clamo::Server.unparsed_dispatch_to_object(
|
|
65
|
+
request: batch_request,
|
|
66
|
+
object: MyService
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
puts batch_response
|
|
70
|
+
# => [{jsonrpc: "2.0", result: 3, id: 1}, {jsonrpc: "2.0", result: 2, id: 2}]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Notifications
|
|
74
|
+
|
|
75
|
+
Notifications are requests without an ID field. They don't produce a response:
|
|
18
76
|
|
|
19
|
-
|
|
77
|
+
```ruby
|
|
78
|
+
notification = '{"jsonrpc": "2.0", "method": "add", "params": [1, 2]}'
|
|
79
|
+
response = Clamo::Server.unparsed_dispatch_to_object(
|
|
80
|
+
request: notification,
|
|
81
|
+
object: MyService
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
puts response
|
|
85
|
+
# => nil
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Building JSON-RPC Requests
|
|
89
|
+
|
|
90
|
+
Clamo provides utilities for building JSON-RPC requests:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
request = Clamo::JSONRPC.build_request(
|
|
94
|
+
method: "add",
|
|
95
|
+
params: [1, 2],
|
|
96
|
+
id: 1
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
puts request
|
|
100
|
+
# => {jsonrpc: "2.0", method: "add", params: [1, 2], id: 1}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Error Handling
|
|
104
|
+
|
|
105
|
+
Clamo follows the JSON-RPC 2.0 specification for error handling:
|
|
106
|
+
|
|
107
|
+
| Error Code | Message | Description |
|
|
108
|
+
|------------|------------------|------------------------------------------------------|
|
|
109
|
+
| -32700 | Parse error | Invalid JSON was received |
|
|
110
|
+
| -32600 | Invalid request | The JSON sent is not a valid Request object |
|
|
111
|
+
| -32601 | Method not found | The method does not exist / is not available |
|
|
112
|
+
| -32602 | Invalid params | Invalid method parameter(s) |
|
|
113
|
+
| -32603 | Internal error | Internal JSON-RPC error |
|
|
114
|
+
| -32000 | Server error | Reserved for implementation-defined server errors |
|
|
115
|
+
|
|
116
|
+
## Advanced Features
|
|
117
|
+
|
|
118
|
+
### Parallel Processing
|
|
119
|
+
|
|
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:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
Clamo::Server.parsed_dispatch_to_object(
|
|
124
|
+
request: batch_request,
|
|
125
|
+
object: MyService,
|
|
126
|
+
in_processes: 4 # Parallel processing option
|
|
127
|
+
)
|
|
128
|
+
```
|
|
20
129
|
|
|
21
130
|
## Development
|
|
22
131
|
|
|
23
132
|
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.
|
|
24
133
|
|
|
25
|
-
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
134
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
26
135
|
|
|
27
136
|
## Contributing
|
|
28
137
|
|
|
29
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/rubakas/clamo
|
|
138
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/rubakas/clamo.
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/lib/clamo/jsonrpc.rb
CHANGED
|
@@ -2,11 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Clamo
|
|
4
4
|
module JSONRPC
|
|
5
|
-
PROTOCOL_VERSION_PRAGMA = { jsonrpc: "2.0" }.freeze
|
|
6
|
-
|
|
7
5
|
module ProtocolErrors
|
|
8
6
|
ErrorDescriptor = Data.define(:code, :message)
|
|
9
|
-
SERVER_ERROR_CODE_RANGE = ((-32_099)..(-32_000))
|
|
10
7
|
|
|
11
8
|
PARSE_ERROR = ErrorDescriptor.new(code: -32_700, message: "Parse error")
|
|
12
9
|
INVALID_REQUEST = ErrorDescriptor.new(code: -32_600, message: "Invalid request")
|
|
@@ -50,28 +47,24 @@ module Clamo
|
|
|
50
47
|
proper_id_if_any?(request)
|
|
51
48
|
end
|
|
52
49
|
|
|
53
|
-
def
|
|
54
|
-
|
|
55
|
-
end
|
|
50
|
+
def build_request(**opts)
|
|
51
|
+
raise ArgumentError, "method is required" unless opts.key?(:method)
|
|
56
52
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
.merge({ params: opts[:params] })
|
|
63
|
-
.merge(opts.key?(:id) ? { id: opts[:id] } : {})
|
|
53
|
+
validate_params_type!(opts[:params]) if opts.key?(:params)
|
|
54
|
+
|
|
55
|
+
{ jsonrpc: "2.0", method: opts[:method] }
|
|
56
|
+
.then { |r| opts.key?(:params) ? r.merge(params: opts[:params]) : r }
|
|
57
|
+
.then { |r| opts.key?(:id) ? r.merge(id: opts[:id]) : r }
|
|
64
58
|
end
|
|
65
59
|
|
|
66
60
|
def build_result_response(id:, result:)
|
|
67
|
-
{}
|
|
68
|
-
.merge({ result: result })
|
|
69
|
-
.merge({ id: id })
|
|
61
|
+
{ jsonrpc: "2.0", result: result, id: id }
|
|
70
62
|
end
|
|
71
63
|
|
|
72
|
-
def build_error_response
|
|
73
|
-
|
|
74
|
-
|
|
64
|
+
def build_error_response(**opts)
|
|
65
|
+
raise ArgumentError, "error code is required" unless opts.dig(:error, :code)
|
|
66
|
+
raise ArgumentError, "error message is required" unless opts.dig(:error, :message)
|
|
67
|
+
|
|
75
68
|
{ jsonrpc: "2.0",
|
|
76
69
|
id: opts[:id],
|
|
77
70
|
error: {
|
|
@@ -81,23 +74,32 @@ module Clamo
|
|
|
81
74
|
}.reject { |k, _| k == :data && !opts[:error].key?(:data) } }
|
|
82
75
|
end
|
|
83
76
|
|
|
84
|
-
def build_error_response_from
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
77
|
+
def build_error_response_from(descriptor:, id: nil)
|
|
78
|
+
build_error_response(
|
|
79
|
+
id: id,
|
|
80
|
+
error: {
|
|
81
|
+
code: descriptor.code,
|
|
82
|
+
message: descriptor.message
|
|
83
|
+
}
|
|
90
84
|
)
|
|
91
|
-
.then { |hash| build_error_response(**hash) }
|
|
92
85
|
end
|
|
93
86
|
|
|
94
|
-
def build_error_response_parse_error
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
87
|
+
def build_error_response_parse_error
|
|
88
|
+
build_error_response(
|
|
89
|
+
id: nil,
|
|
90
|
+
error: {
|
|
91
|
+
code: ProtocolErrors::PARSE_ERROR.code,
|
|
92
|
+
message: ProtocolErrors::PARSE_ERROR.message
|
|
93
|
+
}
|
|
99
94
|
)
|
|
100
|
-
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def validate_params_type!(params)
|
|
100
|
+
return if params.is_a?(Array) || params.is_a?(Hash)
|
|
101
|
+
|
|
102
|
+
raise ArgumentError, "params must be an Array or Hash"
|
|
101
103
|
end
|
|
102
104
|
end
|
|
103
105
|
end
|
data/lib/clamo/server.rb
CHANGED
|
@@ -1,128 +1,138 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require "parallel"
|
|
4
5
|
|
|
5
6
|
module Clamo
|
|
6
7
|
module Server
|
|
7
8
|
class << self
|
|
9
|
+
# Global error callback for notification failures.
|
|
10
|
+
# This is module-level state shared across all callers.
|
|
11
|
+
# Set to any callable (lambda, method, proc) that accepts (exception, method, params).
|
|
12
|
+
#
|
|
13
|
+
# Clamo::Server.on_error = ->(e, method, params) { Rails.logger.error(e) }
|
|
14
|
+
#
|
|
15
|
+
attr_accessor :on_error
|
|
16
|
+
|
|
17
|
+
# JSON string in, JSON string out. Full round-trip for HTTP/socket integrations.
|
|
18
|
+
#
|
|
19
|
+
# Clamo::Server.handle(request: body, object: MyService)
|
|
20
|
+
#
|
|
21
|
+
def handle(request:, object:, **)
|
|
22
|
+
response = unparsed_dispatch_to_object(request: request, object: object, **)
|
|
23
|
+
response&.to_json
|
|
24
|
+
end
|
|
25
|
+
|
|
8
26
|
# Clamo::Server.unparsed_dispatch_to_object(
|
|
9
27
|
# request: request_body,
|
|
10
28
|
# object: MyModule
|
|
11
29
|
# )
|
|
12
|
-
def unparsed_dispatch_to_object(request:, object:, **
|
|
13
|
-
|
|
30
|
+
def unparsed_dispatch_to_object(request:, object:, **)
|
|
31
|
+
raise ArgumentError, "object is required" unless object
|
|
32
|
+
|
|
14
33
|
begin
|
|
15
34
|
parsed = JSON.parse(request)
|
|
16
|
-
rescue JSON::JSONError
|
|
35
|
+
rescue JSON::JSONError
|
|
17
36
|
return JSONRPC.build_error_response_parse_error
|
|
18
37
|
end
|
|
19
38
|
|
|
20
|
-
parsed_dispatch_to_object(request: parsed, object: object, **
|
|
39
|
+
parsed_dispatch_to_object(request: parsed, object: object, **)
|
|
21
40
|
end
|
|
22
41
|
|
|
23
42
|
def parsed_dispatch_to_object(request:, object:, **opts)
|
|
24
43
|
response_for(request: request, object: object, **opts) do |method, params|
|
|
25
|
-
dispatch_to_ruby(
|
|
26
|
-
object: object,
|
|
27
|
-
method: method,
|
|
28
|
-
params: params # consider splating
|
|
29
|
-
)
|
|
44
|
+
dispatch_to_ruby(object: object, method: method, params: params)
|
|
30
45
|
end
|
|
31
46
|
end
|
|
32
47
|
|
|
48
|
+
private
|
|
49
|
+
|
|
33
50
|
def method_known?(object:, method:)
|
|
34
|
-
|
|
35
|
-
.map(&:to_sym)
|
|
36
|
-
.include?(method.to_sym)
|
|
51
|
+
object.public_methods(false).map(&:to_sym).include?(method.to_sym)
|
|
37
52
|
end
|
|
38
53
|
|
|
39
54
|
def dispatch_to_ruby(object:, method:, params:)
|
|
40
55
|
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}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def response_for(request:, object:, **, &block)
|
|
64
|
+
case request
|
|
41
65
|
when Array
|
|
42
|
-
object
|
|
66
|
+
response_for_batch(request: request, object: object, block: block, **)
|
|
43
67
|
when Hash
|
|
44
|
-
object
|
|
45
|
-
when NilClass
|
|
46
|
-
object.send method.to_sym
|
|
68
|
+
response_for_single_request(request: request, object: object, block: block)
|
|
47
69
|
else
|
|
48
|
-
|
|
49
|
-
raise "WTF"
|
|
70
|
+
JSONRPC.build_error_response_from(id: nil, descriptor: JSONRPC::ProtocolErrors::INVALID_REQUEST)
|
|
50
71
|
end
|
|
51
72
|
end
|
|
52
73
|
|
|
53
|
-
def
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
object: object,
|
|
60
|
-
block: block
|
|
61
|
-
)
|
|
62
|
-
end.compact
|
|
63
|
-
when Hash # single request
|
|
64
|
-
response_for_single_request(
|
|
65
|
-
request: request,
|
|
66
|
-
object: object,
|
|
67
|
-
block: block
|
|
68
|
-
)
|
|
69
|
-
else
|
|
70
|
-
JSONRPC.build_error_response_from(
|
|
71
|
-
id: nil,
|
|
72
|
-
descriptor: JSONRPC::ProtocolErrors::INVALID_REQUEST
|
|
73
|
-
)
|
|
74
|
+
def response_for_single_request(request:, object:, block:)
|
|
75
|
+
error = validate_request_structure(request)
|
|
76
|
+
return error if error
|
|
77
|
+
|
|
78
|
+
unless method_known?(object: object, method: request["method"])
|
|
79
|
+
return request.key?("id") ? method_not_found_error(request) : nil
|
|
74
80
|
end
|
|
81
|
+
|
|
82
|
+
return dispatch_notification(request, block) unless request.key?("id")
|
|
83
|
+
|
|
84
|
+
dispatch_request(request, block)
|
|
75
85
|
end
|
|
76
86
|
|
|
77
|
-
def
|
|
78
|
-
|
|
87
|
+
def response_for_batch(request:, object:, block:, **opts)
|
|
88
|
+
if request.empty?
|
|
89
|
+
return JSONRPC.build_error_response_from(id: nil, descriptor: JSONRPC::ProtocolErrors::INVALID_REQUEST)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
result = Parallel.map(request, **opts) do |item|
|
|
93
|
+
response_for_single_request(request: item, object: object, block: block)
|
|
94
|
+
end.compact
|
|
95
|
+
result.empty? ? nil : result
|
|
79
96
|
end
|
|
80
97
|
|
|
81
|
-
def
|
|
98
|
+
def validate_request_structure(request)
|
|
82
99
|
unless JSONRPC.valid_request?(request)
|
|
83
100
|
return JSONRPC.build_error_response_from(
|
|
84
|
-
id: request["id"],
|
|
101
|
+
id: request.is_a?(Hash) ? request["id"] : nil,
|
|
85
102
|
descriptor: JSONRPC::ProtocolErrors::INVALID_REQUEST
|
|
86
103
|
)
|
|
87
104
|
end
|
|
88
105
|
|
|
89
|
-
|
|
90
|
-
return JSONRPC.build_error_response_from(
|
|
91
|
-
id: request["id"],
|
|
92
|
-
descriptor: JSONRPC::ProtocolErrors::INVALID_PARAMS
|
|
93
|
-
)
|
|
94
|
-
end
|
|
106
|
+
return if JSONRPC.proper_params_if_any?(request)
|
|
95
107
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
Thread.new do
|
|
99
|
-
yield_to_execution(
|
|
100
|
-
block: block,
|
|
101
|
-
method: request["method"],
|
|
102
|
-
params: request["params"]
|
|
103
|
-
)
|
|
104
|
-
rescue StandardError
|
|
105
|
-
# TODO: add exception handler
|
|
106
|
-
nil
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
return nil
|
|
110
|
-
end
|
|
108
|
+
JSONRPC.build_error_response_from(id: request["id"], descriptor: JSONRPC::ProtocolErrors::INVALID_PARAMS)
|
|
109
|
+
end
|
|
111
110
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
descriptor: JSONRPC::ProtocolErrors::METHOD_NOT_FOUND
|
|
116
|
-
)
|
|
117
|
-
end
|
|
111
|
+
def method_not_found_error(request)
|
|
112
|
+
JSONRPC.build_error_response_from(id: request["id"], descriptor: JSONRPC::ProtocolErrors::METHOD_NOT_FOUND)
|
|
113
|
+
end
|
|
118
114
|
|
|
115
|
+
def dispatch_notification(request, block)
|
|
116
|
+
block.yield request["method"], request["params"]
|
|
117
|
+
nil
|
|
118
|
+
rescue StandardError => e
|
|
119
|
+
on_error&.call(e, request["method"], request["params"])
|
|
120
|
+
nil
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def dispatch_request(request, block)
|
|
119
124
|
JSONRPC.build_result_response(
|
|
120
125
|
id: request["id"],
|
|
121
|
-
result:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
+
result: block.yield(request["method"], request["params"])
|
|
127
|
+
)
|
|
128
|
+
rescue StandardError => e
|
|
129
|
+
JSONRPC.build_error_response(
|
|
130
|
+
id: request["id"],
|
|
131
|
+
error: {
|
|
132
|
+
code: JSONRPC::ProtocolErrors::INTERNAL_ERROR.code,
|
|
133
|
+
message: JSONRPC::ProtocolErrors::INTERNAL_ERROR.message,
|
|
134
|
+
data: e.message
|
|
135
|
+
}
|
|
126
136
|
)
|
|
127
137
|
end
|
|
128
138
|
end
|
data/lib/clamo/version.rb
CHANGED
data/lib/clamo.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: clamo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andriy Tyurnikov
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: parallel
|
|
@@ -15,15 +15,15 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 1.27
|
|
18
|
+
version: '1.27'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 1.27
|
|
26
|
-
description: JSON-RPC
|
|
25
|
+
version: '1.27'
|
|
26
|
+
description: JSON-RPC 2.0 server toolkit for Ruby
|
|
27
27
|
email:
|
|
28
28
|
- Andriy.Tyurnikov@gmail.com
|
|
29
29
|
executables: []
|
|
@@ -31,11 +31,10 @@ extensions: []
|
|
|
31
31
|
extra_rdoc_files: []
|
|
32
32
|
files:
|
|
33
33
|
- ".rubocop.yml"
|
|
34
|
+
- CHANGELOG.md
|
|
34
35
|
- README.md
|
|
35
36
|
- Rakefile
|
|
36
37
|
- lib/clamo.rb
|
|
37
|
-
- lib/clamo/client.rb
|
|
38
|
-
- lib/clamo/gpt_json_rpc_server.rb
|
|
39
38
|
- lib/clamo/jsonrpc.rb
|
|
40
39
|
- lib/clamo/server.rb
|
|
41
40
|
- lib/clamo/version.rb
|
|
@@ -45,7 +44,7 @@ licenses: []
|
|
|
45
44
|
metadata:
|
|
46
45
|
homepage_uri: https://github.com/rubakas/clamo
|
|
47
46
|
source_code_uri: https://github.com/rubakas/clamo
|
|
48
|
-
changelog_uri: https://github.com/rubakas/clamo
|
|
47
|
+
changelog_uri: https://github.com/rubakas/clamo/blob/main/CHANGELOG.md
|
|
49
48
|
rubygems_mfa_required: 'true'
|
|
50
49
|
rdoc_options: []
|
|
51
50
|
require_paths:
|
|
@@ -54,14 +53,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
54
53
|
requirements:
|
|
55
54
|
- - ">="
|
|
56
55
|
- !ruby/object:Gem::Version
|
|
57
|
-
version: 3.
|
|
56
|
+
version: 3.3.0
|
|
58
57
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
59
58
|
requirements:
|
|
60
59
|
- - ">="
|
|
61
60
|
- !ruby/object:Gem::Version
|
|
62
61
|
version: '0'
|
|
63
62
|
requirements: []
|
|
64
|
-
rubygems_version:
|
|
63
|
+
rubygems_version: 4.0.3
|
|
65
64
|
specification_version: 4
|
|
66
|
-
summary: JSON-RPC
|
|
65
|
+
summary: JSON-RPC 2.0 server toolkit for Ruby
|
|
67
66
|
test_files: []
|
data/lib/clamo/client.rb
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
|
|
5
|
-
module GPTJsonRpcServer
|
|
6
|
-
PROTOCOL_VERSION = "2.0"
|
|
7
|
-
|
|
8
|
-
def self.handle_request(object:, request_json:)
|
|
9
|
-
requests = JSON.parse(request_json)
|
|
10
|
-
if requests.is_a?(Array)
|
|
11
|
-
responses = requests.map do |request|
|
|
12
|
-
Thread.new { process_request(object, request) }
|
|
13
|
-
end.map(&:value).compact
|
|
14
|
-
responses.empty? ? nil : responses.to_json
|
|
15
|
-
else
|
|
16
|
-
response = process_request(object, requests)
|
|
17
|
-
response&.to_json
|
|
18
|
-
end
|
|
19
|
-
rescue JSON::ParserError
|
|
20
|
-
{ jsonrpc: PROTOCOL_VERSION, error: { code: -32_700, message: "Parse error" }, id: nil }.to_json
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def self.process_request(object, request)
|
|
24
|
-
method = request["method"]
|
|
25
|
-
params = request["params"]
|
|
26
|
-
id = request["id"]
|
|
27
|
-
|
|
28
|
-
unless valid_id?(id)
|
|
29
|
-
return { jsonrpc: PROTOCOL_VERSION, error: { code: -32_600, message: "Invalid Request" }, id: nil }
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
unless method.is_a?(String) && object.public_methods(false).include?(method.to_sym)
|
|
33
|
-
return { jsonrpc: PROTOCOL_VERSION, error: { code: -32_601, message: "Method not found" }, id: id }
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
method_object = object.method(method)
|
|
37
|
-
param_list = method_object.parameters
|
|
38
|
-
valid_params = case params
|
|
39
|
-
when Array
|
|
40
|
-
param_list.none? { |type, _| type == :rest } && params.size == param_list.count do |type, _|
|
|
41
|
-
%i[req opt rest].include?(type)
|
|
42
|
-
end
|
|
43
|
-
when Hash
|
|
44
|
-
required_params = param_list.slice(:keyreq).map(&:last)
|
|
45
|
-
required_params.all? { |key| params.key?(key) } && param_list.all? do |type, name|
|
|
46
|
-
type != :keyreq || params.key?(name)
|
|
47
|
-
end
|
|
48
|
-
when NilClass
|
|
49
|
-
param_list.empty?
|
|
50
|
-
else
|
|
51
|
-
false
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
unless valid_params
|
|
55
|
-
return { jsonrpc: PROTOCOL_VERSION, error: { code: -32_602, message: "Invalid params" },
|
|
56
|
-
id: id }
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
if id.nil?
|
|
60
|
-
# Treat id: null as a notification
|
|
61
|
-
Thread.new { safe_call_method(method_object, params) }
|
|
62
|
-
return nil
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
begin
|
|
66
|
-
result = safe_call_method(method_object, params)
|
|
67
|
-
{ jsonrpc: PROTOCOL_VERSION, result: result, id: id }
|
|
68
|
-
rescue StandardError => e
|
|
69
|
-
{ jsonrpc: PROTOCOL_VERSION, error: { code: -32_603, message: "Internal error", data: e.message }, id: id }
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def self.safe_call_method(method_object, params)
|
|
74
|
-
if params.is_a?(Array)
|
|
75
|
-
method_object.call(*params)
|
|
76
|
-
elsif params.is_a?(Hash)
|
|
77
|
-
method_object.call(**params)
|
|
78
|
-
else
|
|
79
|
-
method_object.call
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def self.valid_id?(id)
|
|
84
|
-
return false unless id.is_a?(String) || id.is_a?(Numeric) || id.nil?
|
|
85
|
-
return false if id.is_a?(Numeric) && id != id.to_i
|
|
86
|
-
|
|
87
|
-
true
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# Example usage
|
|
92
|
-
|
|
93
|
-
class MyService
|
|
94
|
-
def add(a, b)
|
|
95
|
-
a + b
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def subtract(a:, b:)
|
|
99
|
-
a - b
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
private
|
|
103
|
-
|
|
104
|
-
def private_method
|
|
105
|
-
"This should not be exposed"
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
service = MyService.new
|
|
110
|
-
|
|
111
|
-
# Example JSON-RPC requests
|
|
112
|
-
single_request_positional = {
|
|
113
|
-
jsonrpc: "2.0",
|
|
114
|
-
method: "add",
|
|
115
|
-
params: [1, 2],
|
|
116
|
-
id: 1
|
|
117
|
-
}.to_json
|
|
118
|
-
|
|
119
|
-
single_request_keyword = {
|
|
120
|
-
jsonrpc: "2.0",
|
|
121
|
-
method: "subtract",
|
|
122
|
-
params: { a: 5, b: 3 },
|
|
123
|
-
id: 2
|
|
124
|
-
}.to_json
|
|
125
|
-
|
|
126
|
-
batch_request = [
|
|
127
|
-
{ jsonrpc: "2.0", method: "add", params: [1, 2], id: 1 },
|
|
128
|
-
{ jsonrpc: "2.0", method: "subtract", params: { a: 5, b: 3 }, id: 2 },
|
|
129
|
-
{ jsonrpc: "2.0", method: "add", params: [7, 3], id: 3 }
|
|
130
|
-
].to_json
|
|
131
|
-
|
|
132
|
-
notification_request = {
|
|
133
|
-
jsonrpc: "2.0",
|
|
134
|
-
method: "add",
|
|
135
|
-
params: [1, 2]
|
|
136
|
-
}.to_json
|
|
137
|
-
|
|
138
|
-
invalid_id_null_request = {
|
|
139
|
-
jsonrpc: "2.0",
|
|
140
|
-
method: "add",
|
|
141
|
-
params: [1, 2],
|
|
142
|
-
id: nil
|
|
143
|
-
}.to_json
|
|
144
|
-
|
|
145
|
-
invalid_id_object_request = {
|
|
146
|
-
jsonrpc: "2.0",
|
|
147
|
-
method: "add",
|
|
148
|
-
params: [1, 2],
|
|
149
|
-
id: {}
|
|
150
|
-
}.to_json
|
|
151
|
-
|
|
152
|
-
invalid_method_request = {
|
|
153
|
-
jsonrpc: "2.0",
|
|
154
|
-
method: {},
|
|
155
|
-
params: [1, 2],
|
|
156
|
-
id: 1
|
|
157
|
-
}.to_json
|
|
158
|
-
|
|
159
|
-
# Handling single request with positional parameters
|
|
160
|
-
response_json_positional = GPTJsonRpcServer.handle_request(object: service, request_json: single_request_positional)
|
|
161
|
-
puts response_json_positional # Output: {"jsonrpc":"2.0","result":3,"id":1}
|
|
162
|
-
|
|
163
|
-
# Handling single request with keyword parameters
|
|
164
|
-
response_json_keyword = GPTJsonRpcServer.handle_request(object: service, request_json: single_request_keyword)
|
|
165
|
-
puts response_json_keyword # Output: {"jsonrpc":"2.0","result":2,"id":2}
|
|
166
|
-
|
|
167
|
-
# Handling batch request
|
|
168
|
-
batch_response_json = GPTJsonRpcServer.handle_request(object: service, request_json: batch_request)
|
|
169
|
-
puts batch_response_json # Output: [{"jsonrpc":"2.0","result":3,"id":1},{"jsonrpc":"2.0","result":2,"id":2},{"jsonrpc":"2.0","result":10,"id":3}]
|
|
170
|
-
|
|
171
|
-
# Handling notification request (no response expected)
|
|
172
|
-
notification_response_json = GPTJsonRpcServer.handle_request(object: service, request_json: notification_request)
|
|
173
|
-
puts notification_response_json.nil? # Output: true
|
|
174
|
-
|
|
175
|
-
# Handling request with id = null (treated as a notification)
|
|
176
|
-
invalid_id_null_response_json = GPTJsonRpcServer.handle_request(object: service, request_json: invalid_id_null_request)
|
|
177
|
-
puts invalid_id_null_response_json.nil? # Output: true
|
|
178
|
-
|
|
179
|
-
# Handling request with id as an object (should return an error)
|
|
180
|
-
invalid_id_object_response_json = GPTJsonRpcServer.handle_request(object: service,
|
|
181
|
-
request_json: invalid_id_object_request)
|
|
182
|
-
puts invalid_id_object_response_json # Output: {"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request"},"id":null}
|
|
183
|
-
|
|
184
|
-
# Handling request with method as an object (should return an error)
|
|
185
|
-
invalid_method_response_json = GPTJsonRpcServer.handle_request(object: service, request_json: invalid_method_request)
|
|
186
|
-
puts invalid_method_response_json # Output: {"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request"},"id":1}
|