acp_sdk_async 0.2.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 +7 -0
- data/LICENSE +24 -0
- data/README.md +147 -0
- data/lib/acp/agent.rb +201 -0
- data/lib/acp/client.rb +213 -0
- data/lib/acp/connection.rb +341 -0
- data/lib/acp/exceptions.rb +32 -0
- data/lib/acp/meta.rb +58 -0
- data/lib/acp/router.rb +119 -0
- data/lib/acp/schema.rb +1365 -0
- data/lib/acp/schema_base.rb +440 -0
- data/lib/acp/stdio.rb +162 -0
- data/lib/acp/transport.rb +134 -0
- data/lib/acp/version.rb +5 -0
- data/lib/acp/wait.rb +129 -0
- data/lib/acp_sdk.rb +3 -0
- data/lib/acp_sdk_async.rb +14 -0
- data/schema/VERSION +1 -0
- data/schema/meta.json +52 -0
- data/schema/schema.json +10295 -0
- metadata +90 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "set"
|
|
5
|
+
require "async/queue"
|
|
6
|
+
require_relative "exceptions"
|
|
7
|
+
require_relative "schema_base"
|
|
8
|
+
require_relative "transport"
|
|
9
|
+
require_relative "wait"
|
|
10
|
+
|
|
11
|
+
module ACP
|
|
12
|
+
StreamEvent = Struct.new(:direction, :message, keyword_init: true) do
|
|
13
|
+
def incoming? = direction == :incoming
|
|
14
|
+
def outgoing? = direction == :outgoing
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class Connection
|
|
18
|
+
attr_reader :transport
|
|
19
|
+
|
|
20
|
+
def initialize(handler, transport, worker_grace: 0.5)
|
|
21
|
+
@handler = handler
|
|
22
|
+
@transport = transport
|
|
23
|
+
@worker_grace = worker_grace
|
|
24
|
+
@next_id = 0
|
|
25
|
+
@pending = {}
|
|
26
|
+
@closed = false
|
|
27
|
+
@mutex = Mutex.new
|
|
28
|
+
@workers = Set.new
|
|
29
|
+
@observers = []
|
|
30
|
+
@listen_worker = nil
|
|
31
|
+
@notification_queue = ::Async::Queue.new
|
|
32
|
+
@notification_worker = nil
|
|
33
|
+
@finished = Wait::Latch.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def closed?
|
|
37
|
+
@closed
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def add_observer(callable = nil, &block)
|
|
41
|
+
observer = callable || block
|
|
42
|
+
raise ArgumentError, "Observer must respond to #call" unless observer.respond_to?(:call)
|
|
43
|
+
|
|
44
|
+
@mutex.synchronize { @observers << observer }
|
|
45
|
+
observer
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def remove_observer(observer)
|
|
49
|
+
@mutex.synchronize { @observers.delete(observer) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def listen
|
|
53
|
+
start_notification_worker
|
|
54
|
+
receive_loop
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
ACP.logger.error("acp: receive loop failed: #{e.class}: #{e.message}")
|
|
57
|
+
raise
|
|
58
|
+
ensure
|
|
59
|
+
reject_all(ConnectionError.new("Connection closed"))
|
|
60
|
+
stop_notification_worker
|
|
61
|
+
@finished.open
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def start
|
|
65
|
+
return @listen_worker if listening?
|
|
66
|
+
|
|
67
|
+
Wait.ensure_reactor!
|
|
68
|
+
@finished = Wait::Latch.new
|
|
69
|
+
@listen_worker = Wait.spawn("acp-listen") { listen }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def join(timeout = nil)
|
|
73
|
+
@finished.wait(timeout)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def send_request(method, params = nil, timeout: nil)
|
|
77
|
+
raise ConnectionError, "Connection closed" if @closed
|
|
78
|
+
|
|
79
|
+
request_id = next_id
|
|
80
|
+
promise = Wait::Promise.new
|
|
81
|
+
@mutex.synchronize { @pending[request_id] = promise }
|
|
82
|
+
|
|
83
|
+
payload = { "jsonrpc" => "2.0", "id" => request_id, "method" => method }
|
|
84
|
+
payload["params"] = serialize(params) unless params.nil?
|
|
85
|
+
|
|
86
|
+
begin
|
|
87
|
+
write(payload)
|
|
88
|
+
rescue StandardError
|
|
89
|
+
@mutex.synchronize { @pending.delete(request_id) }
|
|
90
|
+
raise
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
begin
|
|
94
|
+
promise.wait(timeout)
|
|
95
|
+
rescue TimeoutError
|
|
96
|
+
@mutex.synchronize { @pending.delete(request_id) }
|
|
97
|
+
raise TimeoutError, "Request #{method} (id=#{request_id}) timed out after #{timeout}s"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def send_notification(method, params = nil)
|
|
102
|
+
raise ConnectionError, "Connection closed" if @closed
|
|
103
|
+
|
|
104
|
+
payload = { "jsonrpc" => "2.0", "method" => method }
|
|
105
|
+
payload["params"] = serialize(params) unless params.nil?
|
|
106
|
+
write(payload)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def drain_notifications(timeout: nil)
|
|
110
|
+
return true unless Wait.alive?(@notification_worker)
|
|
111
|
+
|
|
112
|
+
latch = Wait::Latch.new
|
|
113
|
+
@notification_queue.push(latch)
|
|
114
|
+
latch.wait(timeout)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def close
|
|
118
|
+
return if @closed
|
|
119
|
+
|
|
120
|
+
@closed = true
|
|
121
|
+
reject_all(ConnectionError.new("Connection closed"))
|
|
122
|
+
begin
|
|
123
|
+
@transport.close
|
|
124
|
+
rescue StandardError => e
|
|
125
|
+
ACP.logger.debug("acp: transport close failed: #{e.message}")
|
|
126
|
+
end
|
|
127
|
+
stop_notification_worker
|
|
128
|
+
stop_workers
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def next_id
|
|
134
|
+
@mutex.synchronize do
|
|
135
|
+
id = @next_id
|
|
136
|
+
@next_id += 1
|
|
137
|
+
id
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def serialize(params)
|
|
142
|
+
Schema.serialize(params)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def write(payload)
|
|
146
|
+
notify_observers(:outgoing, payload)
|
|
147
|
+
@transport.send_message(payload)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def receive_loop
|
|
151
|
+
loop do
|
|
152
|
+
break if @closed
|
|
153
|
+
|
|
154
|
+
message = begin
|
|
155
|
+
@transport.receive_message
|
|
156
|
+
rescue TimeoutError => e
|
|
157
|
+
ACP.logger.error("acp: #{e.message}")
|
|
158
|
+
break
|
|
159
|
+
rescue ConnectionError => e
|
|
160
|
+
ACP.logger.debug("acp: receive failed: #{e.message}")
|
|
161
|
+
break
|
|
162
|
+
rescue ::Async::Cancel
|
|
163
|
+
break
|
|
164
|
+
end
|
|
165
|
+
break if message.nil?
|
|
166
|
+
|
|
167
|
+
notify_observers(:incoming, message)
|
|
168
|
+
process_message(message)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def process_message(message)
|
|
173
|
+
unless message.is_a?(Hash)
|
|
174
|
+
ACP.logger.warn("acp: ignoring non-object message: #{message.inspect[0, 200]}")
|
|
175
|
+
return
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
method = message["method"]
|
|
179
|
+
has_id = message.key?("id")
|
|
180
|
+
|
|
181
|
+
if method
|
|
182
|
+
unless method.is_a?(String)
|
|
183
|
+
respond_error(message["id"], RequestError.invalid_request("details" => "method must be a string")) if has_id
|
|
184
|
+
return
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
if has_id
|
|
188
|
+
spawn_worker("acp-request:#{method}") { run_request(message) }
|
|
189
|
+
else
|
|
190
|
+
@notification_queue.push(message)
|
|
191
|
+
end
|
|
192
|
+
return
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
if has_id
|
|
196
|
+
handle_response(message)
|
|
197
|
+
else
|
|
198
|
+
ACP.logger.warn("acp: ignoring message without method or id: #{message.inspect[0, 200]}")
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def run_request(message)
|
|
203
|
+
payload = { "jsonrpc" => "2.0", "id" => message["id"] }
|
|
204
|
+
begin
|
|
205
|
+
result = @handler.call(message["method"], message["params"], false)
|
|
206
|
+
payload["result"] = serialize(result)
|
|
207
|
+
rescue RequestError => e
|
|
208
|
+
payload["error"] = e.to_error_obj
|
|
209
|
+
rescue Schema::ValidationError => e
|
|
210
|
+
payload["error"] = RequestError.invalid_params(
|
|
211
|
+
"errors" => [{ "message" => e.message, "loc" => e.path }]
|
|
212
|
+
).to_error_obj
|
|
213
|
+
rescue ::Async::Cancel
|
|
214
|
+
return
|
|
215
|
+
rescue StandardError => e
|
|
216
|
+
ACP.logger.error("acp: handler for #{message['method']} failed: #{e.class}: #{e.message}")
|
|
217
|
+
payload["error"] = RequestError.internal_error("details" => e.message).to_error_obj
|
|
218
|
+
end
|
|
219
|
+
write(payload)
|
|
220
|
+
rescue ConnectionError => e
|
|
221
|
+
ACP.logger.debug("acp: could not send response for #{message['method']}: #{e.message}")
|
|
222
|
+
rescue ::Async::Cancel
|
|
223
|
+
nil
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def respond_error(id, error)
|
|
227
|
+
write({ "jsonrpc" => "2.0", "id" => id, "error" => error.to_error_obj })
|
|
228
|
+
rescue ConnectionError, ::Async::Cancel
|
|
229
|
+
nil
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def run_notification(message)
|
|
233
|
+
@handler.call(message["method"], message["params"], true)
|
|
234
|
+
rescue ::Async::Cancel
|
|
235
|
+
nil
|
|
236
|
+
rescue StandardError => e
|
|
237
|
+
ACP.logger.error("acp: notification handler for #{message['method']} failed: #{e.class}: #{e.message}")
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def handle_response(message)
|
|
241
|
+
promise = @mutex.synchronize { @pending.delete(message["id"]) }
|
|
242
|
+
unless promise
|
|
243
|
+
ACP.logger.debug("acp: response for unknown request id #{message['id'].inspect}")
|
|
244
|
+
return
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
if message.key?("error")
|
|
248
|
+
error = message["error"] || {}
|
|
249
|
+
promise.reject(RequestError.new(error["code"] || -32603, error["message"] || "Error", error["data"]))
|
|
250
|
+
else
|
|
251
|
+
promise.resolve(message["result"])
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def reject_all(error)
|
|
256
|
+
pending = @mutex.synchronize do
|
|
257
|
+
items = @pending.values
|
|
258
|
+
@pending.clear
|
|
259
|
+
items
|
|
260
|
+
end
|
|
261
|
+
pending.each { |promise| promise.reject(error) }
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def notify_observers(direction, message)
|
|
265
|
+
observers = @mutex.synchronize { @observers.dup }
|
|
266
|
+
return if observers.empty?
|
|
267
|
+
|
|
268
|
+
event = StreamEvent.new(direction: direction, message: message)
|
|
269
|
+
observers.each do |observer|
|
|
270
|
+
observer.call(event)
|
|
271
|
+
rescue StandardError => e
|
|
272
|
+
ACP.logger.error("acp: observer failed: #{e.class}: #{e.message}")
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def start_notification_worker
|
|
277
|
+
return if Wait.alive?(@notification_worker)
|
|
278
|
+
|
|
279
|
+
@notification_worker = Wait.spawn("acp-notifications") do
|
|
280
|
+
while (message = @notification_queue.pop)
|
|
281
|
+
if message.is_a?(Wait::Latch)
|
|
282
|
+
message.open
|
|
283
|
+
else
|
|
284
|
+
run_notification(message)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def stop_notification_worker
|
|
291
|
+
worker = @mutex.synchronize do
|
|
292
|
+
current = @notification_worker
|
|
293
|
+
@notification_worker = nil
|
|
294
|
+
current
|
|
295
|
+
end
|
|
296
|
+
return unless worker
|
|
297
|
+
|
|
298
|
+
begin
|
|
299
|
+
@notification_queue.push(nil)
|
|
300
|
+
rescue ::Async::Queue::ClosedError, ClosedQueueError
|
|
301
|
+
nil
|
|
302
|
+
end
|
|
303
|
+
return if Wait.current?(worker)
|
|
304
|
+
|
|
305
|
+
Wait.stop(worker) unless Wait.join(worker, @worker_grace)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def spawn_worker(name, &block)
|
|
309
|
+
Wait.spawn(name) do
|
|
310
|
+
me = Wait.current_worker
|
|
311
|
+
@mutex.synchronize { @workers << me }
|
|
312
|
+
begin
|
|
313
|
+
block.call
|
|
314
|
+
ensure
|
|
315
|
+
@mutex.synchronize { @workers.delete(me) }
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def listening?
|
|
321
|
+
Wait.alive?(@listen_worker)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def stop_workers
|
|
325
|
+
workers = @mutex.synchronize do
|
|
326
|
+
list = @workers.to_a
|
|
327
|
+
@workers.clear
|
|
328
|
+
list
|
|
329
|
+
end
|
|
330
|
+
workers.each do |worker|
|
|
331
|
+
next if Wait.current?(worker)
|
|
332
|
+
|
|
333
|
+
Wait.stop(worker) unless Wait.join(worker, @worker_grace)
|
|
334
|
+
end
|
|
335
|
+
listener = @listen_worker
|
|
336
|
+
return if listener.nil? || Wait.current?(listener)
|
|
337
|
+
|
|
338
|
+
Wait.stop(listener) unless Wait.join(listener, @worker_grace)
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ACP
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class RequestError < Error
|
|
7
|
+
attr_reader :code, :data
|
|
8
|
+
|
|
9
|
+
def initialize(code, message, data = nil)
|
|
10
|
+
super(message)
|
|
11
|
+
@code = code
|
|
12
|
+
@data = data
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.parse_error(data = nil) = new(-32700, "Parse error", data)
|
|
16
|
+
def self.invalid_request(data = nil) = new(-32600, "Invalid request", data)
|
|
17
|
+
def self.method_not_found(method) = new(-32601, "Method not found", { "method" => method })
|
|
18
|
+
def self.invalid_params(data = nil) = new(-32602, "Invalid params", data)
|
|
19
|
+
def self.internal_error(data = nil) = new(-32603, "Internal error", data)
|
|
20
|
+
def self.request_cancelled(data = nil) = new(-32800, "Request cancelled", data)
|
|
21
|
+
def self.auth_required(data = nil) = new(-32000, "Authentication required", data)
|
|
22
|
+
def self.resource_not_found(uri = nil) = new(-32002, "Resource not found", uri ? { "uri" => uri } : nil)
|
|
23
|
+
|
|
24
|
+
def to_error_obj
|
|
25
|
+
{ "code" => @code, "message" => message, "data" => @data }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class ConnectionError < Error; end
|
|
30
|
+
|
|
31
|
+
class TimeoutError < Error; end
|
|
32
|
+
end
|
data/lib/acp/meta.rb
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ACP
|
|
4
|
+
PROTOCOL_VERSION = 1
|
|
5
|
+
SCHEMA_REF = "refs/tags/schema-v1.21.0"
|
|
6
|
+
|
|
7
|
+
AGENT_METHODS = {
|
|
8
|
+
"initialize" => "initialize",
|
|
9
|
+
"authenticate" => "authenticate",
|
|
10
|
+
"providers_list" => "providers/list",
|
|
11
|
+
"providers_set" => "providers/set",
|
|
12
|
+
"providers_disable" => "providers/disable",
|
|
13
|
+
"session_new" => "session/new",
|
|
14
|
+
"session_load" => "session/load",
|
|
15
|
+
"session_set_mode" => "session/set_mode",
|
|
16
|
+
"session_set_config_option" => "session/set_config_option",
|
|
17
|
+
"session_prompt" => "session/prompt",
|
|
18
|
+
"session_cancel" => "session/cancel",
|
|
19
|
+
"mcp_message" => "mcp/message",
|
|
20
|
+
"session_list" => "session/list",
|
|
21
|
+
"session_delete" => "session/delete",
|
|
22
|
+
"session_fork" => "session/fork",
|
|
23
|
+
"session_resume" => "session/resume",
|
|
24
|
+
"session_close" => "session/close",
|
|
25
|
+
"logout" => "logout",
|
|
26
|
+
"nes_start" => "nes/start",
|
|
27
|
+
"nes_suggest" => "nes/suggest",
|
|
28
|
+
"nes_accept" => "nes/accept",
|
|
29
|
+
"nes_reject" => "nes/reject",
|
|
30
|
+
"nes_close" => "nes/close",
|
|
31
|
+
"document_did_open" => "document/didOpen",
|
|
32
|
+
"document_did_change" => "document/didChange",
|
|
33
|
+
"document_did_close" => "document/didClose",
|
|
34
|
+
"document_did_save" => "document/didSave",
|
|
35
|
+
"document_did_focus" => "document/didFocus"
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
CLIENT_METHODS = {
|
|
39
|
+
"session_request_permission" => "session/request_permission",
|
|
40
|
+
"session_update" => "session/update",
|
|
41
|
+
"fs_write_text_file" => "fs/write_text_file",
|
|
42
|
+
"fs_read_text_file" => "fs/read_text_file",
|
|
43
|
+
"terminal_create" => "terminal/create",
|
|
44
|
+
"terminal_output" => "terminal/output",
|
|
45
|
+
"terminal_release" => "terminal/release",
|
|
46
|
+
"terminal_wait_for_exit" => "terminal/wait_for_exit",
|
|
47
|
+
"terminal_kill" => "terminal/kill",
|
|
48
|
+
"mcp_connect" => "mcp/connect",
|
|
49
|
+
"mcp_message" => "mcp/message",
|
|
50
|
+
"mcp_disconnect" => "mcp/disconnect",
|
|
51
|
+
"elicitation_create" => "elicitation/create",
|
|
52
|
+
"elicitation_complete" => "elicitation/complete"
|
|
53
|
+
}.freeze
|
|
54
|
+
|
|
55
|
+
PROTOCOL_METHODS = {
|
|
56
|
+
"cancel_request" => "$/cancel_request"
|
|
57
|
+
}.freeze
|
|
58
|
+
end
|
data/lib/acp/router.rb
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "exceptions"
|
|
4
|
+
require_relative "schema_base"
|
|
5
|
+
|
|
6
|
+
module ACP
|
|
7
|
+
class Route
|
|
8
|
+
attr_reader :method, :kind
|
|
9
|
+
|
|
10
|
+
def initialize(method:, handler:, kind:, model: nil, optional: false, default_result: nil, adapt_result: nil)
|
|
11
|
+
@method = method
|
|
12
|
+
@handler = handler
|
|
13
|
+
@kind = kind
|
|
14
|
+
@model = model
|
|
15
|
+
@optional = optional
|
|
16
|
+
@default_result = default_result
|
|
17
|
+
@adapt_result = adapt_result
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def handle(params)
|
|
21
|
+
unless @handler
|
|
22
|
+
return @default_result if @optional
|
|
23
|
+
|
|
24
|
+
raise RequestError.method_not_found(@method)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
argument = @model ? @model.coerce(params.nil? ? {} : params) : params
|
|
28
|
+
result = @handler.call(argument)
|
|
29
|
+
return result unless @kind == :request && @adapt_result
|
|
30
|
+
|
|
31
|
+
@adapt_result.call(result)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class Router
|
|
36
|
+
NORMALIZE_RESULT = ->(result) { result.nil? ? {} : result }
|
|
37
|
+
|
|
38
|
+
def initialize
|
|
39
|
+
@requests = {}
|
|
40
|
+
@notifications = {}
|
|
41
|
+
@extension_request = nil
|
|
42
|
+
@extension_notification = nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def add_route(route)
|
|
46
|
+
if route.kind == :request
|
|
47
|
+
@requests[route.method] = route
|
|
48
|
+
else
|
|
49
|
+
@notifications[route.method] = route
|
|
50
|
+
end
|
|
51
|
+
route
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def route_request(method, model, target, *names, optional: false, default_result: nil, normalize: false)
|
|
55
|
+
handler = resolve_handler(target, names)
|
|
56
|
+
add_route(Route.new(
|
|
57
|
+
method: method,
|
|
58
|
+
handler: handler,
|
|
59
|
+
kind: :request,
|
|
60
|
+
model: model,
|
|
61
|
+
optional: optional,
|
|
62
|
+
default_result: default_result,
|
|
63
|
+
adapt_result: normalize ? NORMALIZE_RESULT : nil
|
|
64
|
+
))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def route_notification(method, model, target, *names)
|
|
68
|
+
handler = resolve_handler(target, names)
|
|
69
|
+
add_route(Route.new(
|
|
70
|
+
method: method,
|
|
71
|
+
handler: handler,
|
|
72
|
+
kind: :notification,
|
|
73
|
+
model: model,
|
|
74
|
+
optional: true
|
|
75
|
+
))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def on_extension_request(&block)
|
|
79
|
+
@extension_request = block
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def on_extension_notification(&block)
|
|
83
|
+
@extension_notification = block
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def requests
|
|
87
|
+
@requests.keys
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def notifications
|
|
91
|
+
@notifications.keys
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def call(method, params, is_notification)
|
|
95
|
+
if method.start_with?("_")
|
|
96
|
+
handler = is_notification ? @extension_notification : @extension_request
|
|
97
|
+
return nil if is_notification && handler.nil?
|
|
98
|
+
raise RequestError.method_not_found(method) unless handler
|
|
99
|
+
|
|
100
|
+
return handler.call(method[1..], params.is_a?(Hash) ? params : {})
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
routes = is_notification ? @notifications : @requests
|
|
104
|
+
route = routes[method]
|
|
105
|
+
raise RequestError.method_not_found(method) unless route
|
|
106
|
+
|
|
107
|
+
route.handle(params)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def resolve_handler(target, names)
|
|
113
|
+
name = names.flatten.find { |candidate| target.respond_to?(candidate) }
|
|
114
|
+
return nil unless name
|
|
115
|
+
|
|
116
|
+
target.method(name)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|