async-matrix 2.1.0-x86_64-linux → 3.0.1-x86_64-linux

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 (98) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +33 -58
  3. data/lib/async/matrix/3.3/async_matrix_e2ee.so +0 -0
  4. data/lib/async/matrix/3.4/async_matrix_e2ee.so +0 -0
  5. data/lib/async/matrix/4.0/async_matrix_e2ee.so +0 -0
  6. data/lib/async/matrix/api/chain.rb +120 -94
  7. data/lib/async/matrix/api/concat.rb +28 -22
  8. data/lib/async/matrix/api/path_tree.rb +71 -44
  9. data/lib/async/matrix/api.rb +6 -1
  10. data/lib/async/matrix/app_service_client.rb +213 -0
  11. data/lib/async/matrix/auth_error.rb +20 -0
  12. data/lib/async/matrix/bad_json_error.rb +20 -0
  13. data/lib/async/matrix/client/encryption.rb +413 -0
  14. data/lib/async/matrix/client/rooms.rb +478 -0
  15. data/lib/async/matrix/client/sync.rb +508 -0
  16. data/lib/async/matrix/client.rb +217 -113
  17. data/lib/async/matrix/config/vivify.rb +107 -0
  18. data/lib/async/matrix/config.rb +184 -0
  19. data/lib/async/matrix/device_store.rb +1621 -0
  20. data/lib/async/matrix/double_puppet_client.rb +1 -1
  21. data/lib/async/matrix/e2ee/pickle_key.rb +182 -0
  22. data/lib/async/matrix/error.rb +9 -36
  23. data/lib/async/matrix/{application_service/error_response.rb → error_response.rb} +10 -10
  24. data/lib/async/matrix/homeserver_error.rb +20 -0
  25. data/lib/async/matrix/invalid_endpoint_error.rb +20 -0
  26. data/lib/async/matrix/media_client.rb +46 -35
  27. data/lib/async/matrix/not_found_error.rb +20 -0
  28. data/lib/async/matrix/notifier.rb +7 -3
  29. data/lib/async/matrix/response_too_large_error.rb +20 -0
  30. data/lib/async/matrix/version.rb +1 -1
  31. data/lib/async/matrix.rb +16 -7
  32. data/lib/protocol/matrix/canonical_json.rb +200 -0
  33. data/lib/protocol/matrix/content.rb +106 -0
  34. data/lib/protocol/matrix/encrypted_message.rb +1018 -0
  35. data/lib/protocol/matrix/error.rb +49 -0
  36. data/lib/protocol/matrix/event.rb +208 -0
  37. data/lib/protocol/matrix/key_backup.rb +347 -0
  38. data/lib/protocol/matrix/keys.rb +381 -0
  39. data/lib/protocol/matrix/message_batch.rb +482 -0
  40. data/lib/protocol/matrix/schema/registry.rb +381 -0
  41. data/lib/protocol/matrix/schema/validation_error.rb +234 -0
  42. data/lib/protocol/matrix/schema.rb +170 -0
  43. data/lib/protocol/matrix/secret_storage.rb +535 -0
  44. data/lib/protocol/matrix/signing.rb +278 -0
  45. data/lib/protocol/matrix.rb +25 -0
  46. metadata +43 -136
  47. data/lib/async/discord/api/path_tree.rb +0 -127
  48. data/lib/async/discord/api.rb +0 -151
  49. data/lib/async/discord/client.rb +0 -283
  50. data/lib/async/discord/error.rb +0 -84
  51. data/lib/async/discord/gateway.rb +0 -359
  52. data/lib/async/discord.rb +0 -15
  53. data/lib/async/matrix/application_service/bot.rb +0 -232
  54. data/lib/async/matrix/application_service/config/schema/analytics.json +0 -21
  55. data/lib/async/matrix/application_service/config/schema/appservice.json +0 -82
  56. data/lib/async/matrix/application_service/config/schema/backfill.json +0 -91
  57. data/lib/async/matrix/application_service/config/schema/bridge.json +0 -209
  58. data/lib/async/matrix/application_service/config/schema/config.json +0 -61
  59. data/lib/async/matrix/application_service/config/schema/database.json +0 -38
  60. data/lib/async/matrix/application_service/config/schema/direct_media.json +0 -35
  61. data/lib/async/matrix/application_service/config/schema/double_puppet.json +0 -24
  62. data/lib/async/matrix/application_service/config/schema/encryption.json +0 -164
  63. data/lib/async/matrix/application_service/config/schema/homeserver.json +0 -58
  64. data/lib/async/matrix/application_service/config/schema/logging.json +0 -50
  65. data/lib/async/matrix/application_service/config/schema/management_room_texts.json +0 -25
  66. data/lib/async/matrix/application_service/config/schema/matrix.json +0 -45
  67. data/lib/async/matrix/application_service/config/schema/permissions.json +0 -54
  68. data/lib/async/matrix/application_service/config/schema/provisioning.json +0 -23
  69. data/lib/async/matrix/application_service/config/schema/public_media.json +0 -39
  70. data/lib/async/matrix/application_service/config/schema/relay.json +0 -43
  71. data/lib/async/matrix/application_service/config/vivify.rb +0 -109
  72. data/lib/async/matrix/application_service/config.rb +0 -225
  73. data/lib/async/matrix/application_service/dispatcher.rb +0 -185
  74. data/lib/async/matrix/application_service/event.rb +0 -285
  75. data/lib/async/matrix/application_service/server.rb +0 -430
  76. data/lib/async/matrix/application_service/transaction.rb +0 -66
  77. data/lib/async/matrix/application_service/transaction_handler.rb +0 -185
  78. data/lib/async/matrix/application_service/transaction_store.rb +0 -80
  79. data/lib/async/matrix/bridge/discord/db/connection.rb +0 -141
  80. data/lib/async/matrix/bridge/discord/db/file.rb +0 -118
  81. data/lib/async/matrix/bridge/discord/db/guild.rb +0 -120
  82. data/lib/async/matrix/bridge/discord/db/message.rb +0 -160
  83. data/lib/async/matrix/bridge/discord/db/migrations/001_create_users.rb +0 -14
  84. data/lib/async/matrix/bridge/discord/db/migrations/002_create_guilds.rb +0 -14
  85. data/lib/async/matrix/bridge/discord/db/migrations/003_create_portals.rb +0 -23
  86. data/lib/async/matrix/bridge/discord/db/migrations/004_create_puppets.rb +0 -19
  87. data/lib/async/matrix/bridge/discord/db/migrations/005_create_messages.rb +0 -20
  88. data/lib/async/matrix/bridge/discord/db/migrations/006_create_reactions.rb +0 -19
  89. data/lib/async/matrix/bridge/discord/db/migrations/007_create_files.rb +0 -18
  90. data/lib/async/matrix/bridge/discord/db/portal.rb +0 -150
  91. data/lib/async/matrix/bridge/discord/db/puppet.rb +0 -128
  92. data/lib/async/matrix/bridge/discord/db/reaction.rb +0 -165
  93. data/lib/async/matrix/bridge/discord/db/schema.rb +0 -18
  94. data/lib/async/matrix/bridge/discord/db/user.rb +0 -112
  95. data/lib/async/matrix/bridge/discord/db.rb +0 -138
  96. data/lib/async/matrix/schema/registry.rb +0 -354
  97. data/lib/async/matrix/schema/validation_error.rb +0 -225
  98. data/lib/async/matrix/schema.rb +0 -170
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ef835466d5f6581a316c297db4e2996d441168c05555a9e01eeb4115ad2c4fad
4
- data.tar.gz: 843e809c2cce911beaed17022fdd7cda2777e6c18bc60106304616bb96abbc1b
3
+ metadata.gz: a53be21853e15499f9b3a290012f4ffc67e18ca8cef41acadfbf2d8b49f3fc1b
4
+ data.tar.gz: 8de73b759f94fc1908d9283c451667793976988f5881f55f5a3db0ce6bedbb92
5
5
  SHA512:
6
- metadata.gz: c3df9aaadeb5a2942ffef4aad59dbd72176dee3268501dd280797b70c3a9cbd29641cffffc35ee772ce981263d4cabbdd0ed1910cec43a5819e7e3272b18c199
7
- data.tar.gz: d92358ac27b0b2ab8db850f0c6fa755d1afe4a8b537b097b68c6a81882bc1a353f8fc88793dc36c6790ed7f82a7669b590f3a46a672eca7c46c686c5077fef75
6
+ metadata.gz: de1d0a8f6737e19ee1f4af41ca2bf575f12b1e345455681b06a77ef22dc92e11099ec25ba9d31793a6d5cc5aa10ac59fdca9b5a26b6ca689c2595746e4e73e14
7
+ data.tar.gz: c4fc3a0cbeb29a6186ca9acefdb7cccade389077c565ec441ea09b63a33cafa407a116bc52b4cdfbea034dbbae73cf19363cb48528a4ad4e9d502efc7359bfcd
data/README.md CHANGED
@@ -5,7 +5,9 @@
5
5
  [![License](https://img.shields.io/github/license/general-intelligence-systems/async-matrix)](https://github.com/general-intelligence-systems/async-matrix/blob/main/LICENSE)
6
6
  [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.3-red)](https://www.ruby-lang.org)
7
7
 
8
- Async-native [Matrix](https://matrix.org) Application Service SDK for Ruby. Built on the [Socketry](https://github.com/socketry) ecosystem (`async`, `async-http`, Falcon). No threads, no callbacks -- just fibers.
8
+ Async-native [Matrix](https://matrix.org) protocol primitives for Ruby. Built on the [Socketry](https://github.com/socketry) ecosystem (`async`, `async-http`, Falcon). No threads, no callbacks -- just fibers.
9
+
10
+ This gem is the protocol layer: a Client-Server API client, schema-validated events, media, and end-to-end encryption. The *server* side of a bridge or bot -- receiving transactions from a homeserver and dispatching events to handlers -- is [async-matrix-bridge](https://github.com/general-intelligence-systems/async-matrix-bridge), which builds on this gem.
9
11
 
10
12
  ## Usage
11
13
 
@@ -20,60 +22,17 @@ gem "async-matrix"
20
22
  ## Quick Start
21
23
 
22
24
  ```ruby
23
- # config.ru
24
25
  require "async/matrix"
25
26
 
26
- config = Async::Matrix::ApplicationService::Config.load("config/appservice.yml")
27
+ config = Async::Matrix::Config.load("config/appservice.yml")
27
28
  client = Async::Matrix::Client.new(config)
28
29
 
29
- bot = Async::Matrix::ApplicationService::Bot.new(client) do
30
- on "m.room.member" do |event|
31
- join_room(event.room_id) if event.content.membership == "invite"
32
- end
33
-
34
- on "m.room.message", msgtype: "m.text", not_from: :self do |event|
35
- send_notice event.room_id, "Echo: #{event.content.body}"
36
- end
37
- end
38
-
39
- app = Async::Matrix::ApplicationService::Server.new(
40
- hs_token: config.appservice.hs_token,
41
- client: client
42
- ) do
43
- dispatch bot
30
+ Async do
31
+ client.join_room("!room:example.org")
32
+ client.send_notice("!room:example.org", "Hello from a fiber")
44
33
  end
45
-
46
- run app
47
- ```
48
-
49
- ```bash
50
- falcon serve --bind http://0.0.0.0:9292
51
34
  ```
52
35
 
53
- A complete working example with Docker Compose and Synapse lives in [`examples/echo_bot/`](https://github.com/general-intelligence-systems/async-matrix/tree/main/examples/echo_bot).
54
-
55
- ## Handlers
56
-
57
- Any object that responds to `#event_types` and `#call(event)` is a handler. Use this when you need more control than the Bot DSL provides.
58
-
59
- ```ruby
60
- class Echo
61
- def initialize(client) = @client = client
62
-
63
- def event_types = ["m.room.message"]
64
-
65
- def call(event)
66
- return unless event.content&.msgtype == "m.text"
67
- return unless event.sender != @client.config.bot_mxid
68
- @client.send_notice(event.room_id, "Echo: #{event.content.body}")
69
- end
70
- end
71
-
72
- app.dispatch(Echo.new(client))
73
- ```
74
-
75
- Dispatch is fault-tolerant -- one handler raising won't take down the rest.
76
-
77
36
  ## Client
78
37
 
79
38
  ```ruby
@@ -93,40 +52,56 @@ client.api.createRoom.post(name: "Pub")
93
52
  client.api.rooms("!room:ex.com").messages.get(dir: "b", limit: 10)
94
53
  ```
95
54
 
96
- All methods are fiber-safe with automatic connection pooling.
55
+ Binary routes (upload, download, thumbnail) are detected and dispatched to a dedicated `MediaClient`, so raw bytes never pass through JSON encoding.
56
+
57
+ All methods are fiber-safe with automatic connection pooling. The client retries 502/503/504 with exponential backoff and full jitter, honours `Retry-After` on 429, and caps response bodies while streaming them.
58
+
59
+ ## Events
60
+
61
+ ```ruby
62
+ event = Async::Matrix::Schema.parse(raw_hash)
63
+
64
+ event.type # => "m.room.message"
65
+ event.sender # => "@alice:example.org"
66
+ event.content.body # => "hello"
67
+ event.valid? # => true
68
+ event.valid! # => true, or raises Schema::ValidationError
69
+ ```
70
+
71
+ Events validate against the official Matrix event JSON schemas, bundled into the gem.
97
72
 
98
73
  ## Configuration
99
74
 
100
- Create `config/appservice.yml` for your bot:
75
+ `Async::Matrix::Config` loads a YAML file (or a plain hash) and exposes it through dot notation. `Client` reads `homeserver.address` and `appservice.as_token`; it ignores the rest.
101
76
 
102
77
  ```yaml
78
+ # config/appservice.yml
103
79
  homeserver:
104
80
  address: "http://synapse:8008"
105
81
  domain: "localhost"
106
82
 
107
83
  appservice:
108
84
  as_token: "your-appservice-token"
109
- hs_token: "your-homeserver-token"
110
85
  bot:
111
86
  username: "bot"
112
87
  ```
113
88
 
114
- You'll also need a [`registration.yml`](https://spec.matrix.org/latest/application-service-api/#registration) registered with your homeserver. See the [echo bot example](https://github.com/general-intelligence-systems/async-matrix/tree/main/examples/echo_bot) for a working template.
115
-
116
- Override the config path at runtime:
117
-
118
- ```bash
119
- APPSERVICE_CONFIG=/etc/bot/appservice.yml falcon serve
89
+ ```ruby
90
+ config = Async::Matrix::Config.load("config/appservice.yml")
91
+ config.homeserver.address # => "http://synapse:8008"
92
+ config.bot_mxid # => "@bot:localhost"
120
93
  ```
121
94
 
95
+ It validates nothing by default. A subclass adds validation by overriding `.validate!` -- which is how async-matrix-bridge layers a mautrix bridgev2 JSON Schema suite on top without reimplementing the loading.
96
+
122
97
  ## Built With
123
98
 
124
99
  - [async](https://github.com/socketry/async) -- fiber-based concurrency framework
125
100
  - [async-http](https://github.com/socketry/async-http) -- HTTP client/server with connection pooling
126
- - [falcon](https://github.com/socketry/falcon) -- async Rack-compatible web server
127
101
  - [json_schemer](https://github.com/davishmcclurg/json_schemer) -- JSON Schema validation
128
102
  - [scampi](https://github.com/general-intelligence-systems/scampi) -- inline co-located test framework
129
103
  - [string_builder](https://github.com/general-intelligence-systems/string_builder) -- method-chain string builder
104
+ - [vodozemac](https://github.com/matrix-org/vodozemac) -- Olm/Megolm, via a native Rust binding
130
105
 
131
106
  ## License
132
107
 
@@ -108,122 +108,148 @@ module Async
108
108
 
109
109
  private
110
110
 
111
- def execute(method, body: nil, query: nil, content_type: nil, max_retries: nil)
112
- segments = _rewrite_version(_build_full_segments)
113
- result = @path_tree.match(segments, method)
114
-
115
- if !result[:valid]
116
- path = "/" + segments.join("/")
117
- available = result[:methods]
118
- if available.empty?
119
- ::Kernel.raise ::Async::Matrix::InvalidEndpointError.new(
120
- "INVALID_ENDPOINT",
121
- "No Matrix API endpoint matches: #{method} #{path}"
122
- )
123
- else
124
- ::Kernel.raise ::Async::Matrix::InvalidEndpointError.new(
125
- "METHOD_NOT_ALLOWED",
126
- "#{method} not allowed for #{path}. Valid methods: #{available.map(&:upcase).join(", ")}"
127
- )
111
+ def execute(method, body: nil, query: nil, content_type: nil, max_retries: nil)
112
+ segments = _rewrite_version(_build_full_segments)
113
+ result = @path_tree.match(segments, method)
114
+
115
+ if !result[:valid]
116
+ path = "/" + segments.join("/")
117
+ available = result[:methods]
118
+ if available.empty?
119
+ ::Kernel.raise ::Async::Matrix::InvalidEndpointError.new(
120
+ "INVALID_ENDPOINT",
121
+ "No Matrix API endpoint matches: #{method} #{path}",
122
+ )
123
+ else
124
+ ::Kernel.raise ::Async::Matrix::InvalidEndpointError.new(
125
+ "METHOD_NOT_ALLOWED",
126
+ "#{method} not allowed for #{path}. Valid methods: #{available.map(&:upcase).join(", ")}",
127
+ )
128
+ end
128
129
  end
129
- end
130
130
 
131
- path = "/" + segments.map { |s| _encode(s) }.join("/")
131
+ path = "/" + segments.map { |s| _encode(s) }.join("/")
132
132
 
133
- if query && !query.empty?
134
- qs = query.map { |k, v| "#{_encode(k.to_s)}=#{_encode(v.to_s)}" }.join("&")
135
- path = "#{path}?#{qs}"
136
- end
133
+ if query && !query.empty?
134
+ qs = query.map { |k, v| "#{_encode(k.to_s)}=#{_encode(v.to_s)}" }.join("&")
135
+ path = "#{path}?#{qs}"
136
+ end
137
137
 
138
- if _binary_route?(segments)
139
- case method
140
- when "GET"
141
- @client.media_client.download(path)
142
- when "POST"
143
- @client.media_client.upload("POST", path, body, content_type || "application/octet-stream")
144
- when "PUT"
145
- @client.media_client.upload("PUT", path, body, content_type || "application/octet-stream")
138
+ if _binary_route?(segments)
139
+ case method
140
+ when "GET"
141
+ @client.media_client.download(path)
142
+ when "POST"
143
+ @client.media_client.upload(
144
+ "POST",
145
+ path,
146
+ body,
147
+ content_type || "application/octet-stream",
148
+ )
149
+ when "PUT"
150
+ @client.media_client.upload(
151
+ "PUT",
152
+ path,
153
+ body,
154
+ content_type || "application/octet-stream",
155
+ )
156
+ end
157
+ else
158
+ if max_retries
159
+ retry_opts = {max_retries: max_retries}
160
+ else
161
+ retry_opts = {}
146
162
  end
147
- else
148
- retry_opts = max_retries ? {max_retries: max_retries} : {}
149
- case method
150
- when "GET"
151
- @client.get(path, **retry_opts)
152
- when "POST"
153
- @client.post(path, body || {}, **retry_opts)
154
- when "PUT"
155
- @client.put(path, body || {}, **retry_opts)
156
- when "DELETE"
157
- @client.request("DELETE", path, nil, **retry_opts)
158
- when "PATCH"
159
- @client.request("PATCH", path, body || {}, **retry_opts)
163
+ case method
164
+ when "GET"
165
+ @client.get(path, **retry_opts)
166
+ when "POST"
167
+ @client.post(path, body || {}, **retry_opts)
168
+ when "PUT"
169
+ @client.put(path, body || {}, **retry_opts)
170
+ when "DELETE"
171
+ @client.request(
172
+ "DELETE",
173
+ path,
174
+ nil,
175
+ **retry_opts,
176
+ )
177
+ when "PATCH"
178
+ @client.request(
179
+ "PATCH",
180
+ path,
181
+ body || {},
182
+ **retry_opts,
183
+ )
184
+ end
160
185
  end
161
186
  end
162
- end
163
187
 
164
188
  # Rewrites version segments for endpoints that only exist at
165
189
  # a different version than the gateway prefix provides.
166
190
  # _matrix/media/v3/create → _matrix/media/v1/create
167
191
  # _matrix/client/v3/media/... → _matrix/client/v1/media/...
168
- def _rewrite_version(segments)
169
- # POST /_matrix/media/v1/create (only exists at v1)
170
- if segments.length == 4 &&
171
- segments[0] == "_matrix" && segments[1] == "media" &&
172
- segments[2] == "v3" && segments[3] == "create"
173
- segments = segments.dup
174
- segments[2] = "v1"
175
- return segments
176
- end
177
-
178
- # Authenticated media endpoints live at /_matrix/client/v1/media/...
179
- if segments.length >= 5 &&
180
- segments[0] == "_matrix" && segments[1] == "client" &&
181
- segments[2] == "v3" && segments[3] == "media"
182
- segments = segments.dup
183
- segments[2] = "v1"
184
- return segments
192
+ def _rewrite_version(segments)
193
+ # POST /_matrix/media/v1/create (only exists at v1)
194
+ media_create = segments.length == 4 &&
195
+ segments[0] == "_matrix" && segments[1] == "media" &&
196
+ segments[2] == "v3" && segments[3] == "create"
197
+
198
+ # Authenticated media endpoints live at /_matrix/client/v1/media/...
199
+ client_media = segments.length >= 5 &&
200
+ segments[0] == "_matrix" && segments[1] == "client" &&
201
+ segments[2] == "v3" && segments[3] == "media"
202
+
203
+ if media_create || client_media
204
+ segments.dup.tap { |rewritten| rewritten[2] = "v1" }
205
+ else
206
+ segments
207
+ end
185
208
  end
186
209
 
187
- segments
188
- end
189
-
190
- def _binary_route?(segments)
191
- BINARY_ROUTES.any? do |pattern|
192
- parts = pattern.split("/")
193
- next false unless parts.length == segments.length + 1 # leading slash adds empty element
194
- parts.shift # remove empty string from leading /
195
- parts.length == segments.length &&
196
- parts.zip(segments).all? { |pat, seg| pat == "*" || pat == seg }
210
+ def _binary_route?(segments)
211
+ BINARY_ROUTES.any? do |pattern|
212
+ parts = pattern.split("/")
213
+ unless parts.length == segments.length + 1
214
+ next false
215
+ end # leading slash adds empty element
216
+ parts.shift # remove empty string from leading /
217
+ parts.length == segments.length &&
218
+ parts.zip(segments).all? { |pat, seg| pat == "*" || pat == seg }
219
+ end
197
220
  end
198
- end
199
221
 
200
- def _extract_query_params(kwargs)
201
- query = {}
202
- kwargs.each_key do |k|
203
- key_s = k.to_s
204
- if key_s.start_with?("?")
205
- query[key_s[1..]] = kwargs.delete(k)
222
+ def _extract_query_params(kwargs)
223
+ query = {}
224
+ kwargs.each_key do |k|
225
+ key_s = k.to_s
226
+ if key_s.start_with?("?")
227
+ query[key_s[1..]] = kwargs.delete(k)
228
+ end
206
229
  end
230
+ query.empty? ? nil : query
207
231
  end
208
- query.empty? ? nil : query
209
- end
210
232
 
211
- def _build_full_segments
212
- chain_segments = []
213
- @buffer.each do |name, args|
214
- chain_segments << name
215
- next if args.nil? || args.empty?
216
- args.each do |arg|
217
- next if arg.is_a?(::Hash)
218
- chain_segments << arg.to_s
233
+ def _build_full_segments
234
+ chain_segments = []
235
+ @buffer.each do |name, args|
236
+ chain_segments << name
237
+ if args.nil? || args.empty?
238
+ next
239
+ end
240
+ args.each do |arg|
241
+ if arg.is_a?(::Hash)
242
+ next
243
+ end
244
+ chain_segments << arg.to_s
245
+ end
219
246
  end
247
+ @prefix + chain_segments
220
248
  end
221
- @prefix + chain_segments
222
- end
223
249
 
224
- def _encode(value)
225
- ::ERB::Util.url_encode(value.to_s)
226
- end
250
+ def _encode(value)
251
+ ::ERB::Util.url_encode(value.to_s)
252
+ end
227
253
  end
228
254
  end
229
255
  end
@@ -24,36 +24,42 @@ module Async
24
24
  end
25
25
 
26
26
  def initialize(buffer)
27
- @buffer = buffer.respond_to?(:to_a) ? buffer.to_a : buffer
27
+ if buffer.respond_to?(:to_a)
28
+ @buffer = buffer.to_a
29
+ else
30
+ @buffer = buffer
31
+ end
28
32
  end
29
33
 
30
34
  def segments
31
- result = []
32
-
33
- @buffer.each do |entry|
34
- case entry
35
- when Symbol
36
- # :slash, :dash -- ignore in URL context
37
- next
38
- when Array
39
- name, args = entry
40
- next unless name.is_a?(String)
35
+ [].tap do |result|
36
+ @buffer.each do |entry|
37
+ case entry
38
+ when Symbol
39
+ # :slash, :dash -- ignore in URL context
40
+ next
41
+ when Array
42
+ name, args = entry
43
+ unless name.is_a?(String)
44
+ next
45
+ end
41
46
 
42
- if args.nil? || args.empty?
43
- # Bare method -> path segment
44
- result << name
45
- else
46
- # Method with args -> method name as segment, then each arg as segment
47
- result << name
48
- args.each do |arg|
49
- next if arg.is_a?(Hash) # kwargs are not path segments
50
- result << arg.to_s
47
+ if args.nil? || args.empty?
48
+ # Bare method -> path segment
49
+ result << name
50
+ else
51
+ # Method with args -> method name as segment, then each arg as segment
52
+ result << name
53
+ args.each do |arg|
54
+ if arg.is_a?(Hash)
55
+ next
56
+ end # kwargs are not path segments
57
+ result << arg.to_s
58
+ end
51
59
  end
52
60
  end
53
61
  end
54
62
  end
55
-
56
- result
57
63
  end
58
64
  end
59
65
  end
@@ -22,7 +22,13 @@ module Async
22
22
  class PathTree
23
23
  SCHEMA_DIR = Pathname.new(File.expand_path("../../../../data/matrix-spec/api/client-server", __dir__))
24
24
 
25
- Node = Struct.new(:children, :wildcard, :methods, :operation_ids, keyword_init: true) do
25
+ Node = Struct.new(
26
+ :children,
27
+ :wildcard,
28
+ :methods,
29
+ :operation_ids,
30
+ keyword_init: true,
31
+ ) do
26
32
  def initialize(**)
27
33
  super
28
34
  self.children ||= {}
@@ -39,34 +45,46 @@ module Async
39
45
 
40
46
  # Load all OpenAPI schemas from data/ and build the tree.
41
47
  def self.load(schema_dir: SCHEMA_DIR)
42
- tree = new
43
- Pathname.glob(schema_dir / "*.yaml").each do |path|
44
- tree.load_schema(path)
48
+ new.tap do |tree|
49
+ Pathname.glob(schema_dir / "*.yaml").each do |path|
50
+ tree.load_schema(path)
51
+ end
45
52
  end
46
- tree
47
53
  end
48
54
 
49
55
  # Parse a single OpenAPI YAML file and insert its paths into the tree.
50
56
  def load_schema(path)
51
57
  doc = YAML.safe_load(File.read(path), permitted_classes: [Symbol], aliases: true)
52
- return unless doc.is_a?(Hash)
53
-
54
- base_path = extract_base_path(doc)
55
- paths = doc["paths"]
56
- return unless paths.is_a?(Hash)
57
-
58
- paths.each do |path_template, methods_hash|
59
- next unless methods_hash.is_a?(Hash)
60
-
61
- # Build full path: basePath + path_template
62
- full_path = "#{base_path}#{path_template.strip}"
63
- segments = full_path.split("/").reject(&:empty?)
64
-
65
- methods_hash.each do |method, operation|
66
- next unless %w[get post put delete patch head].include?(method)
67
- operation_id = operation.is_a?(Hash) ? operation["operationId"] : nil
68
- insert(segments, method, operation_id)
58
+ if doc.is_a?(Hash)
59
+ base_path = extract_base_path(doc)
60
+ paths = doc["paths"]
61
+ if paths.is_a?(Hash)
62
+ paths.each do |path_template, methods_hash|
63
+ unless methods_hash.is_a?(Hash)
64
+ next
65
+ end
66
+
67
+ # Build full path: basePath + path_template
68
+ full_path = "#{base_path}#{path_template.strip}"
69
+ segments = full_path.split("/").reject(&:empty?)
70
+
71
+ methods_hash.each do |method, operation|
72
+ unless %w[get post put delete patch head].include?(method)
73
+ next
74
+ end
75
+ if operation.is_a?(Hash)
76
+ operation_id = operation["operationId"]
77
+ else
78
+ operation_id = nil
79
+ end
80
+ insert(segments, method, operation_id)
81
+ end
82
+ end
83
+ else
84
+ nil
69
85
  end
86
+ else
87
+ nil
70
88
  end
71
89
  end
72
90
 
@@ -83,25 +101,30 @@ module Async
83
101
  node = node.children[segment]
84
102
  end
85
103
  end
86
- node.methods << method.downcase unless node.methods.include?(method.downcase)
87
- node.operation_ids[method.downcase] = operation_id if operation_id
104
+ unless node.methods.include?(method.downcase)
105
+ node.methods << method.downcase
106
+ end
107
+ if operation_id
108
+ node.operation_ids[method.downcase] = operation_id
109
+ end
88
110
  end
89
111
 
90
112
  # Match a concrete path (array of segments) against the trie.
91
113
  # Returns a result hash.
92
114
  def match(segments, method = nil)
93
- node = @root
94
- segments.each do |segment|
95
- if node.children.key?(segment)
96
- node = node.children[segment]
97
- elsif node.wildcard
98
- node = node.wildcard
99
- else
100
- return {valid: false, methods: [], operation_id: nil}
115
+ node = segments.inject(@root) do |current, segment|
116
+ if current.nil?
117
+ nil
118
+ elsif current.children.key?(segment)
119
+ current.children[segment]
120
+ elsif current.wildcard
121
+ current.wildcard
101
122
  end
102
123
  end
103
124
 
104
- if method
125
+ if node.nil?
126
+ {valid: false, methods: [], operation_id: nil}
127
+ elsif method
105
128
  method_down = method.downcase
106
129
  valid = node.methods.include?(method_down)
107
130
  {valid: valid, methods: node.methods, operation_id: node.operation_ids[method_down]}
@@ -112,17 +135,21 @@ module Async
112
135
 
113
136
  private
114
137
 
115
- def extract_base_path(doc)
116
- servers = doc["servers"]
117
- return "" unless servers.is_a?(Array) && servers.first.is_a?(Hash)
118
-
119
- server = servers.first
120
- variables = server["variables"] || {}
121
- base_path_var = variables["basePath"]
122
- return "" unless base_path_var.is_a?(Hash)
123
-
124
- base_path_var["default"] || ""
125
- end
138
+ def extract_base_path(doc)
139
+ servers = doc["servers"]
140
+ if servers.is_a?(Array) && servers.first.is_a?(Hash)
141
+ server = servers.first
142
+ variables = server["variables"] || {}
143
+ base_path_var = variables["basePath"]
144
+ if base_path_var.is_a?(Hash)
145
+ base_path_var["default"] || ""
146
+ else
147
+ ""
148
+ end
149
+ else
150
+ ""
151
+ end
152
+ end
126
153
  end
127
154
  end
128
155
  end
@@ -54,7 +54,12 @@ module Async
54
54
  if name.start_with?("to_")
55
55
  super
56
56
  else
57
- chain.__send__(name, *args, **kwargs, &block)
57
+ chain.__send__(
58
+ name,
59
+ *args,
60
+ **kwargs,
61
+ &block
62
+ )
58
63
  end
59
64
  end
60
65