async-matrix 1.0.0 → 1.1.1

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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/discord-api-spec/openapi.json +40404 -0
  4. data/lib/async/discord/api/path_tree.rb +130 -0
  5. data/lib/async/discord/api.rb +156 -0
  6. data/lib/async/discord/client.rb +286 -0
  7. data/lib/async/discord/error.rb +88 -0
  8. data/lib/async/discord/gateway.rb +362 -0
  9. data/lib/async/discord.rb +16 -0
  10. data/lib/async/matrix/api/chain.rb +14 -8
  11. data/lib/async/matrix/application_service/config/vivify.rb +3 -0
  12. data/lib/async/matrix/application_service/event.rb +9 -20
  13. data/lib/async/matrix/application_service/server.rb +2 -2
  14. data/lib/async/matrix/bridge/discord/db/connection.rb +143 -0
  15. data/lib/async/matrix/bridge/discord/db/file.rb +120 -0
  16. data/lib/async/matrix/bridge/discord/db/guild.rb +122 -0
  17. data/lib/async/matrix/bridge/discord/db/message.rb +162 -0
  18. data/lib/async/matrix/bridge/discord/db/migrations/001_create_users.rb +14 -0
  19. data/lib/async/matrix/bridge/discord/db/migrations/002_create_guilds.rb +14 -0
  20. data/lib/async/matrix/bridge/discord/db/migrations/003_create_portals.rb +23 -0
  21. data/lib/async/matrix/bridge/discord/db/migrations/004_create_puppets.rb +19 -0
  22. data/lib/async/matrix/bridge/discord/db/migrations/005_create_messages.rb +20 -0
  23. data/lib/async/matrix/bridge/discord/db/migrations/006_create_reactions.rb +19 -0
  24. data/lib/async/matrix/bridge/discord/db/migrations/007_create_files.rb +18 -0
  25. data/lib/async/matrix/bridge/discord/db/portal.rb +152 -0
  26. data/lib/async/matrix/bridge/discord/db/puppet.rb +130 -0
  27. data/lib/async/matrix/bridge/discord/db/reaction.rb +167 -0
  28. data/lib/async/matrix/bridge/discord/db/user.rb +114 -0
  29. data/lib/async/matrix/bridge/discord/db.rb +140 -0
  30. data/lib/async/matrix/double_puppet_client.rb +84 -0
  31. data/lib/async/matrix/schema.rb +2 -2
  32. data/lib/async/matrix/server.rb +1 -0
  33. data/lib/async/matrix/version.rb +1 -1
  34. data/lib/async/matrix.rb +2 -0
  35. metadata +67 -1
@@ -0,0 +1,362 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the Apache License, Version 2.0.
4
+ # Copyright, 2026, by General Intelligence Systems.
5
+
6
+ require "bundler/setup"
7
+ require "async"
8
+ require "async/http/endpoint"
9
+ require "async/websocket/client"
10
+ require "json"
11
+ require "console"
12
+ require "async/discord"
13
+
14
+ module Async
15
+ module Discord
16
+ # WebSocket client for the Discord Gateway (v10).
17
+ #
18
+ # Connects to Discord's WebSocket gateway, authenticates with a bot token,
19
+ # manages heartbeating, and dispatches incoming events to registered handlers.
20
+ #
21
+ # Supports session resumption on reconnect.
22
+ #
23
+ # gateway = Async::Discord::Gateway.new(token: "MTk...", intents: 0x30001)
24
+ # gateway.on("MESSAGE_CREATE") { |data| puts data["content"] }
25
+ # gateway.on("READY") { |data| puts "Connected as #{data["user"]["username"]}" }
26
+ # gateway.run # blocks, runs inside Async reactor
27
+ #
28
+ class Gateway
29
+ GATEWAY_URL = "wss://gateway.discord.gg/?v=10&encoding=json"
30
+
31
+ # Discord Gateway opcodes
32
+ DISPATCH = 0
33
+ HEARTBEAT = 1
34
+ IDENTIFY = 2
35
+ PRESENCE_UPDATE = 3
36
+ VOICE_STATE_UPDATE = 4
37
+ RESUME = 6
38
+ RECONNECT = 7
39
+ REQUEST_GUILD_MEMBERS = 8
40
+ INVALID_SESSION = 9
41
+ HELLO = 10
42
+ HEARTBEAT_ACK = 11
43
+
44
+ attr_reader :session_id, :seq
45
+
46
+ def initialize(token:, intents:, gateway_url: GATEWAY_URL)
47
+ @token = token
48
+ @intents = intents
49
+ @gateway_url = gateway_url
50
+ @handlers = Hash.new { |h, k| h[k] = [] }
51
+ @seq = nil
52
+ @session_id = nil
53
+ @resume_url = nil
54
+ @heartbeat_interval = nil
55
+ @heartbeat_acked = true
56
+ @connection = nil
57
+ @running = false
58
+ end
59
+
60
+ # Register a handler for a Discord event type.
61
+ #
62
+ # gateway.on("MESSAGE_CREATE") { |data| ... }
63
+ # gateway.on("READY") { |data| ... }
64
+ #
65
+ def on(event_type, &block)
66
+ @handlers[event_type] << block
67
+ end
68
+
69
+ # Connect to the gateway and run the event loop.
70
+ # Blocks until the connection is closed or an unrecoverable error occurs.
71
+ # Must be called inside an Async reactor.
72
+ def run
73
+ @running = true
74
+
75
+ while @running
76
+ begin
77
+ connect_and_run
78
+ rescue => e
79
+ Console.error(self) { "Gateway error: #{e.class}: #{e.message}" }
80
+ break unless @running
81
+ sleep(5)
82
+ end
83
+ end
84
+ end
85
+
86
+ # Gracefully stop the gateway.
87
+ def stop
88
+ @running = false
89
+ @connection&.close
90
+ end
91
+
92
+ # Send a raw payload to the gateway.
93
+ def send_payload(op:, d: nil)
94
+ return unless @connection
95
+
96
+ payload = {op: op, d: d}
97
+ @connection.write(payload.to_json)
98
+ @connection.flush
99
+ end
100
+
101
+ private
102
+
103
+ def connect_and_run
104
+ url = @resume_url || @gateway_url
105
+ endpoint = Async::HTTP::Endpoint.parse(url)
106
+
107
+ Console.info(self) { "Connecting to #{url}" }
108
+
109
+ Async::WebSocket::Client.connect(endpoint) do |connection|
110
+ @connection = connection
111
+
112
+ # First message should be HELLO
113
+ hello = read_message
114
+ unless hello && hello["op"] == HELLO
115
+ raise GatewayError.new("GATEWAY", "Expected HELLO, got: #{hello&.dig("op")}")
116
+ end
117
+
118
+ @heartbeat_interval = hello["d"]["heartbeat_interval"] / 1000.0
119
+ Console.info(self) { "Heartbeat interval: #{@heartbeat_interval}s" }
120
+
121
+ # Start heartbeat fiber
122
+ heartbeat_task = Async do
123
+ heartbeat_loop
124
+ end
125
+
126
+ # Identify or resume
127
+ if @session_id && @seq
128
+ send_resume
129
+ else
130
+ send_identify
131
+ end
132
+
133
+ # Read events until disconnected
134
+ read_loop
135
+
136
+ ensure
137
+ heartbeat_task&.stop
138
+ @connection = nil
139
+ end
140
+ end
141
+
142
+ def read_message
143
+ message = @connection.read
144
+ return nil unless message
145
+
146
+ JSON.parse(message.to_str)
147
+ rescue => e
148
+ Console.error(self) { "Failed to read/parse gateway message: #{e.message}" }
149
+ nil
150
+ end
151
+
152
+ def read_loop
153
+ loop do
154
+ payload = read_message
155
+ break unless payload
156
+
157
+ handle_payload(payload)
158
+ end
159
+ end
160
+
161
+ def handle_payload(payload)
162
+ op = payload["op"]
163
+ data = payload["d"]
164
+ seq = payload["s"]
165
+ type = payload["t"]
166
+
167
+ @seq = seq if seq
168
+
169
+ case op
170
+ when DISPATCH
171
+ Console.debug(self) { "DISPATCH: #{type}" }
172
+ dispatch(type, data)
173
+
174
+ when HEARTBEAT
175
+ # Server is asking us to heartbeat immediately
176
+ send_heartbeat
177
+
178
+ when RECONNECT
179
+ Console.info(self) { "Server requested reconnect" }
180
+ @connection&.close
181
+
182
+ when INVALID_SESSION
183
+ resumable = data == true
184
+ Console.warn(self) { "Invalid session (resumable=#{resumable})" }
185
+ unless resumable
186
+ @session_id = nil
187
+ @seq = nil
188
+ end
189
+ sleep(rand(1.0..5.0))
190
+ @connection&.close
191
+
192
+ when HEARTBEAT_ACK
193
+ @heartbeat_acked = true
194
+
195
+ else
196
+ Console.debug(self) { "Unknown opcode: #{op}" }
197
+ end
198
+ end
199
+
200
+ def dispatch(event_type, data)
201
+ case event_type
202
+ when "READY"
203
+ @session_id = data["session_id"]
204
+ @resume_url = data["resume_gateway_url"]
205
+ Console.info(self) { "Ready: session=#{@session_id}" }
206
+ when "RESUMED"
207
+ Console.info(self) { "Resumed successfully" }
208
+ end
209
+
210
+ @handlers[event_type].each do |handler|
211
+ begin
212
+ handler.call(data)
213
+ rescue => e
214
+ Console.error(self) { "Handler for #{event_type} raised: #{e.class}: #{e.message}" }
215
+ end
216
+ end
217
+ end
218
+
219
+ def send_identify
220
+ send_payload(
221
+ op: IDENTIFY,
222
+ d: {
223
+ token: @token,
224
+ intents: @intents,
225
+ properties: {
226
+ os: RUBY_PLATFORM,
227
+ browser: "async-discord",
228
+ device: "async-discord"
229
+ }
230
+ }
231
+ )
232
+ end
233
+
234
+ def send_resume
235
+ Console.info(self) { "Resuming session #{@session_id} at seq #{@seq}" }
236
+ send_payload(
237
+ op: RESUME,
238
+ d: {
239
+ token: @token,
240
+ session_id: @session_id,
241
+ seq: @seq
242
+ }
243
+ )
244
+ end
245
+
246
+ def send_heartbeat
247
+ send_payload(op: HEARTBEAT, d: @seq)
248
+ @heartbeat_acked = false
249
+ end
250
+
251
+ def heartbeat_loop
252
+ # Jitter the first heartbeat as per Discord docs
253
+ sleep(@heartbeat_interval * rand)
254
+ send_heartbeat
255
+
256
+ loop do
257
+ sleep(@heartbeat_interval)
258
+
259
+ unless @heartbeat_acked
260
+ Console.warn(self) { "Heartbeat not ACKed, reconnecting" }
261
+ @connection&.close
262
+ break
263
+ end
264
+
265
+ send_heartbeat
266
+ end
267
+ end
268
+ end
269
+ end
270
+ end
271
+
272
+ test do
273
+ describe "Async::Discord::Gateway" do
274
+ it "initializes with token and intents" do
275
+ gw = Async::Discord::Gateway.new(token: "test_token", intents: 0x30001)
276
+ gw.session_id.should.be.nil
277
+ gw.seq.should.be.nil
278
+ end
279
+
280
+ it "registers event handlers" do
281
+ gw = Async::Discord::Gateway.new(token: "test_token", intents: 0)
282
+ received = []
283
+ gw.on("MESSAGE_CREATE") { |data| received << data }
284
+ gw.on("MESSAGE_CREATE") { |data| received << :second }
285
+
286
+ # Dispatch manually via send to test handler registration
287
+ gw.send(:dispatch, "MESSAGE_CREATE", {"content" => "hello"})
288
+ received.length.should == 2
289
+ received[0]["content"].should == "hello"
290
+ received[1].should == :second
291
+ end
292
+
293
+ it "handles READY event and stores session_id" do
294
+ gw = Async::Discord::Gateway.new(token: "test_token", intents: 0)
295
+ ready_data = {
296
+ "session_id" => "abc123",
297
+ "resume_gateway_url" => "wss://resume.discord.gg",
298
+ "user" => {"username" => "testbot"}
299
+ }
300
+
301
+ gw.send(:dispatch, "READY", ready_data)
302
+ gw.session_id.should == "abc123"
303
+ end
304
+
305
+ it "updates seq from dispatch payloads" do
306
+ gw = Async::Discord::Gateway.new(token: "test_token", intents: 0)
307
+ gw.send(:handle_payload, {
308
+ "op" => 0,
309
+ "d" => {"content" => "hi"},
310
+ "s" => 42,
311
+ "t" => "MESSAGE_CREATE"
312
+ })
313
+ gw.seq.should == 42
314
+ end
315
+
316
+ it "clears session on non-resumable INVALID_SESSION" do
317
+ gw = Async::Discord::Gateway.new(token: "test_token", intents: 0)
318
+ # Set a session first
319
+ gw.send(:dispatch, "READY", {
320
+ "session_id" => "sess123",
321
+ "resume_gateway_url" => "wss://resume.discord.gg"
322
+ })
323
+ gw.session_id.should == "sess123"
324
+
325
+ # Simulate non-resumable invalid session (stub connection close)
326
+ gw.instance_variable_set(:@connection, nil)
327
+ gw.send(:handle_payload, {
328
+ "op" => Async::Discord::Gateway::INVALID_SESSION,
329
+ "d" => false,
330
+ "s" => nil,
331
+ "t" => nil
332
+ })
333
+ gw.session_id.should.be.nil
334
+ gw.seq.should.be.nil
335
+ end
336
+
337
+ it "continues dispatching when a handler raises" do
338
+ gw = Async::Discord::Gateway.new(token: "test_token", intents: 0)
339
+ results = []
340
+ gw.on("TEST") { |_| raise "boom" }
341
+ gw.on("TEST") { |data| results << data }
342
+
343
+ gw.send(:dispatch, "TEST", {"value" => 1})
344
+ results.length.should == 1
345
+ results.first["value"].should == 1
346
+ end
347
+
348
+ it "responds to stop" do
349
+ gw = Async::Discord::Gateway.new(token: "test_token", intents: 0)
350
+ gw.should.respond_to :stop
351
+ end
352
+
353
+ it "accepts custom gateway URL" do
354
+ gw = Async::Discord::Gateway.new(
355
+ token: "tok",
356
+ intents: 0,
357
+ gateway_url: "wss://custom.gateway.example.com"
358
+ )
359
+ gw.instance_variable_get(:@gateway_url).should == "wss://custom.gateway.example.com"
360
+ end
361
+ end
362
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the Apache License, Version 2.0.
4
+ # Copyright, 2026, by General Intelligence Systems.
5
+
6
+ require "async/http"
7
+ require "scampi"
8
+
9
+ module Async
10
+ module Discord
11
+ end
12
+ end
13
+
14
+ Dir.glob("#{__dir__}/discord/**/*.rb").sort.each do |path|
15
+ require path
16
+ end
@@ -75,6 +75,13 @@ module Async
75
75
  execute("DELETE", query: kwargs, max_retries: max_retries)
76
76
  end
77
77
 
78
+ def patch(body = nil, **kwargs)
79
+ content_type = kwargs.delete(:content_type)
80
+ max_retries = kwargs.delete(:max_retries)
81
+ query = _extract_query_params(kwargs)
82
+ execute("PATCH", body: body || kwargs, content_type: content_type, max_retries: max_retries, query: query)
83
+ end
84
+
78
85
  # ── Chain inspection ───────────────────────────────────────
79
86
 
80
87
  def to_s
@@ -125,11 +132,12 @@ module Async
125
132
 
126
133
  path = "/" + segments.map { |s| _encode(s) }.join("/")
127
134
 
135
+ if query && !query.empty?
136
+ qs = query.map { |k, v| "#{_encode(k.to_s)}=#{_encode(v.to_s)}" }.join("&")
137
+ path = "#{path}?#{qs}"
138
+ end
139
+
128
140
  if _binary_route?(segments)
129
- if query && !query.empty?
130
- qs = query.map { |k, v| "#{_encode(k.to_s)}=#{_encode(v.to_s)}" }.join("&")
131
- path = "#{path}?#{qs}"
132
- end
133
141
  case method
134
142
  when "GET"
135
143
  @client.media_client.download(path)
@@ -142,10 +150,6 @@ module Async
142
150
  retry_opts = max_retries ? {max_retries: max_retries} : {}
143
151
  case method
144
152
  when "GET"
145
- if query && !query.empty?
146
- qs = query.map { |k, v| "#{_encode(k.to_s)}=#{_encode(v.to_s)}" }.join("&")
147
- path = "#{path}?#{qs}"
148
- end
149
153
  @client.get(path, **retry_opts)
150
154
  when "POST"
151
155
  @client.post(path, body || {}, **retry_opts)
@@ -153,6 +157,8 @@ module Async
153
157
  @client.put(path, body || {}, **retry_opts)
154
158
  when "DELETE"
155
159
  @client.request("DELETE", path, nil, **retry_opts)
160
+ when "PATCH"
161
+ @client.request("PATCH", path, body || {}, **retry_opts)
156
162
  end
157
163
  end
158
164
  end
@@ -3,6 +3,9 @@
3
3
  # Released under the Apache License, Version 2.0.
4
4
  # Copyright, 2026, by General Intelligence Systems.
5
5
 
6
+ require "bundler/setup"
7
+ require "async/matrix"
8
+
6
9
  module Async
7
10
  module Matrix
8
11
  module ApplicationService
@@ -32,15 +32,12 @@ module Async
32
32
  @membership = data["membership"]
33
33
  end
34
34
 
35
- # Direct hash access for any content field.
36
- def [](key)
37
- @data[key.to_s]
38
- end
35
+ def to_h = @data
36
+ def to_s = @body.to_s
37
+ def to_str = to_s
39
38
 
40
- # Returns the raw content hash.
41
- def to_h
42
- @data
43
- end
39
+ # Direct hash access for any content field.
40
+ def [](key) = @data[key.to_s]
44
41
 
45
42
  # Dynamic access to any content field present in the raw data.
46
43
  def method_missing(name, *args)
@@ -86,15 +83,11 @@ module Async
86
83
  end
87
84
 
88
85
  # The JSONSchemer::Schema for this event's type, or nil if unknown.
89
- def schema
90
- Schema[@type]
91
- end
86
+ def schema = Schema[@type]
92
87
 
93
88
  # Validate this event against its schema.
94
89
  # Returns true if valid or if no schema exists (lenient).
95
- def valid?
96
- Schema.valid?(@raw)
97
- end
90
+ def valid? = Schema.valid?(@raw)
98
91
 
99
92
  # Validate this event against its schema.
100
93
  # Raises Schema::ValidationError with detailed errors on failure.
@@ -113,14 +106,10 @@ module Async
113
106
 
114
107
  # Content property names defined by the schema for this event type.
115
108
  # @return [Array<String>]
116
- def content_properties
117
- Schema.content_properties(@type)
118
- end
109
+ def content_properties = Schema.content_properties(@type)
119
110
 
120
111
  # Is this a state event? (has a state_key)
121
- def state_event?
122
- !@state_key.nil?
123
- end
112
+ def state_event? = !@state_key.nil?
124
113
  end
125
114
  end
126
115
  end
@@ -119,8 +119,8 @@ module Async
119
119
  parse_json(request).then do |body|
120
120
  if body
121
121
  Console.info(self) {
122
- event_count = (body["events"] || []).size
123
- "Transaction #{txn_id}: #{event_count} event(s)"
122
+ events = body["events"] || []
123
+ "Transaction #{txn_id}: #{events.size} event(s) — #{JSON.generate(events)}"
124
124
  }
125
125
 
126
126
  @dispatcher.dispatch_transaction(body)
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the Apache License, Version 2.0.
4
+ # Copyright, 2026, by General Intelligence Systems.
5
+
6
+ require "bundler/setup"
7
+ require "sequel"
8
+
9
+ module Async
10
+ module Matrix
11
+ module Bridge
12
+ module Discord
13
+ module DB
14
+ # Establishes a Sequel database connection from the bridge config's
15
+ # database section.
16
+ #
17
+ # Supports both SQLite (with foreign keys and WAL mode) and PostgreSQL,
18
+ # matching the mautrix bridgev2 database config schema.
19
+ #
20
+ # db = Connection.establish(config.database)
21
+ # db[:users].all # => [...]
22
+ #
23
+ module Connection
24
+ SQLITE_TYPE = "sqlite3-fk-wal"
25
+ POSTGRES_TYPE = "postgres"
26
+
27
+ # Establish a database connection from a config.database section.
28
+ #
29
+ # @param database_config [Hash, Vivify] config.database with :type, :uri, :max_open_conns
30
+ # @return [Sequel::Database]
31
+ def self.establish(database_config)
32
+ type = database_config.type || SQLITE_TYPE
33
+ uri = database_config.uri || "sqlite://bridge.db"
34
+
35
+ db = case type
36
+ when SQLITE_TYPE
37
+ connect_sqlite(uri, database_config)
38
+ when POSTGRES_TYPE
39
+ connect_postgres(uri, database_config)
40
+ else
41
+ raise ArgumentError, "Unknown database type: #{type.inspect}. Expected #{SQLITE_TYPE.inspect} or #{POSTGRES_TYPE.inspect}."
42
+ end
43
+
44
+ db
45
+ end
46
+
47
+ class << self
48
+ private
49
+
50
+ def connect_sqlite(uri, config)
51
+ # Normalize mautrix-style URIs: "file:bridge.db?..." -> "sqlite://bridge.db"
52
+ sequel_uri = if uri.start_with?("file:")
53
+ path = uri.sub(%r{\Afile:}, "").split("?").first
54
+ "sqlite://#{path}"
55
+ elsif uri.start_with?("sqlite://")
56
+ uri
57
+ else
58
+ "sqlite://#{uri}"
59
+ end
60
+
61
+ db = Sequel.connect(sequel_uri, max_connections: max_conns(config))
62
+ db.run("PRAGMA foreign_keys = ON")
63
+ db.run("PRAGMA journal_mode = WAL")
64
+ db
65
+ end
66
+
67
+ def connect_postgres(uri, config)
68
+ Sequel.connect(uri, max_connections: max_conns(config))
69
+ end
70
+
71
+ def max_conns(config)
72
+ val = config.respond_to?(:max_open_conns) ? config.max_open_conns : nil
73
+ val && val > 0 ? val : 5
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ test do
84
+ describe "Async::Matrix::Bridge::Discord::DB::Connection" do
85
+ it "connects to an in-memory SQLite database" do
86
+ config = Object.new
87
+ config.define_singleton_method(:type) { "sqlite3-fk-wal" }
88
+ config.define_singleton_method(:uri) { "sqlite:/" }
89
+ config.define_singleton_method(:max_open_conns) { 1 }
90
+
91
+ db = Async::Matrix::Bridge::Discord::DB::Connection.establish(config)
92
+ db.should.be.kind_of Sequel::Database
93
+ db.database_type.should == :sqlite
94
+ db.disconnect
95
+ end
96
+
97
+ it "enables foreign keys and WAL on SQLite" do
98
+ config = Object.new
99
+ config.define_singleton_method(:type) { "sqlite3-fk-wal" }
100
+ config.define_singleton_method(:uri) { "sqlite:/" }
101
+ config.define_singleton_method(:max_open_conns) { 1 }
102
+
103
+ db = Async::Matrix::Bridge::Discord::DB::Connection.establish(config)
104
+ fk = db["PRAGMA foreign_keys"].first
105
+ fk[:foreign_keys].should == 1
106
+ db.disconnect
107
+ end
108
+
109
+ it "normalizes mautrix file: URIs to sequel sqlite:// URIs" do
110
+ config = Object.new
111
+ config.define_singleton_method(:type) { "sqlite3-fk-wal" }
112
+ config.define_singleton_method(:uri) { "file:test.db?_txlock=immediate" }
113
+ config.define_singleton_method(:max_open_conns) { 1 }
114
+
115
+ # Should not raise — the URI is normalized
116
+ db = Async::Matrix::Bridge::Discord::DB::Connection.establish(config)
117
+ db.should.be.kind_of Sequel::Database
118
+ db.disconnect
119
+ end
120
+
121
+ it "defaults to sqlite with 5 connections when config is empty" do
122
+ config = Object.new
123
+ config.define_singleton_method(:type) { nil }
124
+ config.define_singleton_method(:uri) { nil }
125
+ config.define_singleton_method(:max_open_conns) { nil }
126
+
127
+ db = Async::Matrix::Bridge::Discord::DB::Connection.establish(config)
128
+ db.should.be.kind_of Sequel::Database
129
+ db.disconnect
130
+ end
131
+
132
+ it "raises on unknown database type" do
133
+ config = Object.new
134
+ config.define_singleton_method(:type) { "mysql" }
135
+ config.define_singleton_method(:uri) { "mysql://localhost/db" }
136
+ config.define_singleton_method(:max_open_conns) { 1 }
137
+
138
+ lambda {
139
+ Async::Matrix::Bridge::Discord::DB::Connection.establish(config)
140
+ }.should.raise(ArgumentError)
141
+ end
142
+ end
143
+ end