clamo 0.5.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 +7 -32
- 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,26 +1,5 @@
|
|
|
1
1
|
# Clamo
|
|
2
2
|
|
|
3
|
-
JSON-RPC protocol toolkit for Ruby.
|
|
4
|
-
|
|
5
|
-
Consume, Serve or test JSON-RPC endpoints with Clamo.
|
|
6
|
-
|
|
7
|
-
## Installation
|
|
8
|
-
|
|
9
|
-
Install the gem and add to the application's Gemfile by executing:
|
|
10
|
-
|
|
11
|
-
$ bundle add clamo
|
|
12
|
-
|
|
13
|
-
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
14
|
-
|
|
15
|
-
$ gem install clamo
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
## Development
|
|
19
|
-
|
|
20
|
-
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.
|
|
21
|
-
|
|
22
|
-
To install this gem onto your local machi# Clamo
|
|
23
|
-
|
|
24
3
|
A Ruby implementation of [JSON-RPC 2.0](https://www.jsonrpc.org/specification) designed for simplicity and compliance with the specification.
|
|
25
4
|
|
|
26
5
|
|
|
@@ -36,11 +15,11 @@ module MyService
|
|
|
36
15
|
def self.add(a, b)
|
|
37
16
|
a + b
|
|
38
17
|
end
|
|
39
|
-
|
|
18
|
+
|
|
40
19
|
def self.subtract(a:, b:)
|
|
41
20
|
a - b
|
|
42
21
|
end
|
|
43
|
-
|
|
22
|
+
|
|
44
23
|
# Private methods won't be accessible via JSON-RPC
|
|
45
24
|
private_class_method def self.internal_method
|
|
46
25
|
# This won't be exposed
|
|
@@ -55,7 +34,7 @@ response = Clamo::Server.unparsed_dispatch_to_object(
|
|
|
55
34
|
)
|
|
56
35
|
|
|
57
36
|
puts response
|
|
58
|
-
# => {
|
|
37
|
+
# => {jsonrpc: "2.0", result: 3, id: 1}
|
|
59
38
|
```
|
|
60
39
|
|
|
61
40
|
### Handling Different Parameter Types
|
|
@@ -88,7 +67,7 @@ batch_response = Clamo::Server.unparsed_dispatch_to_object(
|
|
|
88
67
|
)
|
|
89
68
|
|
|
90
69
|
puts batch_response
|
|
91
|
-
# => [{
|
|
70
|
+
# => [{jsonrpc: "2.0", result: 3, id: 1}, {jsonrpc: "2.0", result: 2, id: 2}]
|
|
92
71
|
```
|
|
93
72
|
|
|
94
73
|
### Notifications
|
|
@@ -118,7 +97,7 @@ request = Clamo::JSONRPC.build_request(
|
|
|
118
97
|
)
|
|
119
98
|
|
|
120
99
|
puts request
|
|
121
|
-
# => {:
|
|
100
|
+
# => {jsonrpc: "2.0", method: "add", params: [1, 2], id: 1}
|
|
122
101
|
```
|
|
123
102
|
|
|
124
103
|
## Error Handling
|
|
@@ -156,12 +135,8 @@ To install this gem onto your local machine, run `bundle exec rake install`.
|
|
|
156
135
|
|
|
157
136
|
## Contributing
|
|
158
137
|
|
|
159
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
|
138
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/rubakas/clamo.
|
|
160
139
|
|
|
161
140
|
## License
|
|
162
141
|
|
|
163
|
-
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
164
|
-
|
|
165
|
-
## Contributing
|
|
166
|
-
|
|
167
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/rubakas/clamo
|
|
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}
|