async-matrix 2.0.1-x86_64-linux → 3.0.0-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.
- checksums.yaml +4 -4
- data/README.md +33 -58
- data/lib/async/matrix/3.3/async_matrix_e2ee.so +0 -0
- data/lib/async/matrix/3.4/async_matrix_e2ee.so +0 -0
- data/lib/async/matrix/4.0/async_matrix_e2ee.so +0 -0
- data/lib/async/matrix/api/chain.rb +120 -94
- data/lib/async/matrix/api/concat.rb +28 -22
- data/lib/async/matrix/api/path_tree.rb +71 -44
- data/lib/async/matrix/api.rb +6 -1
- data/lib/async/matrix/auth_error.rb +20 -0
- data/lib/async/matrix/bad_json_error.rb +20 -0
- data/lib/async/matrix/client.rb +142 -110
- data/lib/async/matrix/config/vivify.rb +107 -0
- data/lib/async/matrix/config.rb +184 -0
- data/lib/async/matrix/content.rb +106 -0
- data/lib/async/matrix/double_puppet_client.rb +1 -1
- data/lib/async/matrix/e2ee.rb +37 -0
- data/lib/async/matrix/error.rb +0 -27
- data/lib/async/matrix/{application_service/error_response.rb → error_response.rb} +10 -10
- data/lib/async/matrix/event.rb +197 -0
- data/lib/async/matrix/homeserver_error.rb +20 -0
- data/lib/async/matrix/invalid_endpoint_error.rb +20 -0
- data/lib/async/matrix/media_client.rb +46 -35
- data/lib/async/matrix/not_found_error.rb +20 -0
- data/lib/async/matrix/notifier.rb +7 -3
- data/lib/async/matrix/response_too_large_error.rb +20 -0
- data/lib/async/matrix/schema/registry.rb +367 -340
- data/lib/async/matrix/schema/validation_error.rb +221 -214
- data/lib/async/matrix/schema.rb +161 -161
- data/lib/async/matrix/version.rb +3 -3
- data/lib/async/matrix.rb +0 -7
- metadata +29 -123
- data/lib/async/discord/api/path_tree.rb +0 -127
- data/lib/async/discord/api.rb +0 -151
- data/lib/async/discord/client.rb +0 -283
- data/lib/async/discord/error.rb +0 -84
- data/lib/async/discord/gateway.rb +0 -359
- data/lib/async/discord.rb +0 -15
- data/lib/async/matrix/application_service/bot.rb +0 -232
- data/lib/async/matrix/application_service/config/schema/analytics.json +0 -21
- data/lib/async/matrix/application_service/config/schema/appservice.json +0 -82
- data/lib/async/matrix/application_service/config/schema/backfill.json +0 -91
- data/lib/async/matrix/application_service/config/schema/bridge.json +0 -209
- data/lib/async/matrix/application_service/config/schema/config.json +0 -61
- data/lib/async/matrix/application_service/config/schema/database.json +0 -38
- data/lib/async/matrix/application_service/config/schema/direct_media.json +0 -35
- data/lib/async/matrix/application_service/config/schema/double_puppet.json +0 -24
- data/lib/async/matrix/application_service/config/schema/encryption.json +0 -164
- data/lib/async/matrix/application_service/config/schema/homeserver.json +0 -58
- data/lib/async/matrix/application_service/config/schema/logging.json +0 -50
- data/lib/async/matrix/application_service/config/schema/management_room_texts.json +0 -25
- data/lib/async/matrix/application_service/config/schema/matrix.json +0 -45
- data/lib/async/matrix/application_service/config/schema/permissions.json +0 -54
- data/lib/async/matrix/application_service/config/schema/provisioning.json +0 -23
- data/lib/async/matrix/application_service/config/schema/public_media.json +0 -39
- data/lib/async/matrix/application_service/config/schema/relay.json +0 -43
- data/lib/async/matrix/application_service/config/vivify.rb +0 -109
- data/lib/async/matrix/application_service/config.rb +0 -225
- data/lib/async/matrix/application_service/dispatcher.rb +0 -185
- data/lib/async/matrix/application_service/event.rb +0 -285
- data/lib/async/matrix/application_service/server.rb +0 -430
- data/lib/async/matrix/application_service/transaction.rb +0 -66
- data/lib/async/matrix/application_service/transaction_handler.rb +0 -185
- data/lib/async/matrix/application_service/transaction_store.rb +0 -80
- data/lib/async/matrix/bridge/discord/db/connection.rb +0 -141
- data/lib/async/matrix/bridge/discord/db/file.rb +0 -118
- data/lib/async/matrix/bridge/discord/db/guild.rb +0 -120
- data/lib/async/matrix/bridge/discord/db/message.rb +0 -160
- data/lib/async/matrix/bridge/discord/db/migrations/001_create_users.rb +0 -14
- data/lib/async/matrix/bridge/discord/db/migrations/002_create_guilds.rb +0 -14
- data/lib/async/matrix/bridge/discord/db/migrations/003_create_portals.rb +0 -23
- data/lib/async/matrix/bridge/discord/db/migrations/004_create_puppets.rb +0 -19
- data/lib/async/matrix/bridge/discord/db/migrations/005_create_messages.rb +0 -20
- data/lib/async/matrix/bridge/discord/db/migrations/006_create_reactions.rb +0 -19
- data/lib/async/matrix/bridge/discord/db/migrations/007_create_files.rb +0 -18
- data/lib/async/matrix/bridge/discord/db/portal.rb +0 -150
- data/lib/async/matrix/bridge/discord/db/puppet.rb +0 -128
- data/lib/async/matrix/bridge/discord/db/reaction.rb +0 -165
- data/lib/async/matrix/bridge/discord/db/schema.rb +0 -18
- data/lib/async/matrix/bridge/discord/db/user.rb +0 -112
- data/lib/async/matrix/bridge/discord/db.rb +0 -138
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4fe4d4e0b7fc49cc1bfbe641fed7a4ed1430555973a1db1c08a27bec5765d591
|
|
4
|
+
data.tar.gz: f9d721081238308c948a735039944f61fa0f4878f91efa848179ee7527307559
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1a0b60b1edb4cc6be2e33ac158c727c26ffcfcc2d8083a1b6f96a355112d622c126f26206db064c9ae91ed2c925c65d6e1c518883362219476fd53ebff9e6854
|
|
7
|
+
data.tar.gz: 77dccb7126f368a5d5c0fe713180ad6d213ff5beb7542c26edf2c5ad8b8a993e60c5ff866e9f59b2a23da47937787a9bb26044c3cf2420d6024f2b04779e0e99
|
data/README.md
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
[](https://github.com/general-intelligence-systems/async-matrix/blob/main/LICENSE)
|
|
6
6
|
[](https://www.ruby-lang.org)
|
|
7
7
|
|
|
8
|
-
Async-native [Matrix](https://matrix.org)
|
|
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::
|
|
27
|
+
config = Async::Matrix::Config.load("config/appservice.yml")
|
|
27
28
|
client = Async::Matrix::Client.new(config)
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -108,122 +108,148 @@ module Async
|
|
|
108
108
|
|
|
109
109
|
private
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
131
|
+
path = "/" + segments.map { |s| _encode(s) }.join("/")
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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(
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|