async-matrix 2.1.0 → 3.0.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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +8 -8
  3. data/README.md +33 -58
  4. data/ext/async_matrix_e2ee/Cargo.toml +7 -1
  5. data/lib/async/matrix/api/chain.rb +120 -94
  6. data/lib/async/matrix/api/concat.rb +28 -22
  7. data/lib/async/matrix/api/path_tree.rb +71 -44
  8. data/lib/async/matrix/api.rb +6 -1
  9. data/lib/async/matrix/auth_error.rb +20 -0
  10. data/lib/async/matrix/bad_json_error.rb +20 -0
  11. data/lib/async/matrix/client.rb +142 -110
  12. data/lib/async/matrix/config/vivify.rb +107 -0
  13. data/lib/async/matrix/config.rb +184 -0
  14. data/lib/async/matrix/content.rb +106 -0
  15. data/lib/async/matrix/double_puppet_client.rb +1 -1
  16. data/lib/async/matrix/error.rb +0 -27
  17. data/lib/async/matrix/{application_service/error_response.rb → error_response.rb} +10 -10
  18. data/lib/async/matrix/event.rb +197 -0
  19. data/lib/async/matrix/homeserver_error.rb +20 -0
  20. data/lib/async/matrix/invalid_endpoint_error.rb +20 -0
  21. data/lib/async/matrix/media_client.rb +46 -35
  22. data/lib/async/matrix/not_found_error.rb +20 -0
  23. data/lib/async/matrix/notifier.rb +7 -3
  24. data/lib/async/matrix/response_too_large_error.rb +20 -0
  25. data/lib/async/matrix/schema/registry.rb +367 -340
  26. data/lib/async/matrix/schema/validation_error.rb +221 -214
  27. data/lib/async/matrix/schema.rb +161 -161
  28. data/lib/async/matrix/version.rb +3 -3
  29. data/lib/async/matrix.rb +0 -7
  30. metadata +24 -132
  31. data/lib/async/discord/api/path_tree.rb +0 -127
  32. data/lib/async/discord/api.rb +0 -151
  33. data/lib/async/discord/client.rb +0 -283
  34. data/lib/async/discord/error.rb +0 -84
  35. data/lib/async/discord/gateway.rb +0 -359
  36. data/lib/async/discord.rb +0 -15
  37. data/lib/async/matrix/application_service/bot.rb +0 -232
  38. data/lib/async/matrix/application_service/config/schema/analytics.json +0 -21
  39. data/lib/async/matrix/application_service/config/schema/appservice.json +0 -82
  40. data/lib/async/matrix/application_service/config/schema/backfill.json +0 -91
  41. data/lib/async/matrix/application_service/config/schema/bridge.json +0 -209
  42. data/lib/async/matrix/application_service/config/schema/config.json +0 -61
  43. data/lib/async/matrix/application_service/config/schema/database.json +0 -38
  44. data/lib/async/matrix/application_service/config/schema/direct_media.json +0 -35
  45. data/lib/async/matrix/application_service/config/schema/double_puppet.json +0 -24
  46. data/lib/async/matrix/application_service/config/schema/encryption.json +0 -164
  47. data/lib/async/matrix/application_service/config/schema/homeserver.json +0 -58
  48. data/lib/async/matrix/application_service/config/schema/logging.json +0 -50
  49. data/lib/async/matrix/application_service/config/schema/management_room_texts.json +0 -25
  50. data/lib/async/matrix/application_service/config/schema/matrix.json +0 -45
  51. data/lib/async/matrix/application_service/config/schema/permissions.json +0 -54
  52. data/lib/async/matrix/application_service/config/schema/provisioning.json +0 -23
  53. data/lib/async/matrix/application_service/config/schema/public_media.json +0 -39
  54. data/lib/async/matrix/application_service/config/schema/relay.json +0 -43
  55. data/lib/async/matrix/application_service/config/vivify.rb +0 -109
  56. data/lib/async/matrix/application_service/config.rb +0 -225
  57. data/lib/async/matrix/application_service/dispatcher.rb +0 -185
  58. data/lib/async/matrix/application_service/event.rb +0 -285
  59. data/lib/async/matrix/application_service/server.rb +0 -430
  60. data/lib/async/matrix/application_service/transaction.rb +0 -66
  61. data/lib/async/matrix/application_service/transaction_handler.rb +0 -185
  62. data/lib/async/matrix/application_service/transaction_store.rb +0 -80
  63. data/lib/async/matrix/bridge/discord/db/connection.rb +0 -141
  64. data/lib/async/matrix/bridge/discord/db/file.rb +0 -118
  65. data/lib/async/matrix/bridge/discord/db/guild.rb +0 -120
  66. data/lib/async/matrix/bridge/discord/db/message.rb +0 -160
  67. data/lib/async/matrix/bridge/discord/db/migrations/001_create_users.rb +0 -14
  68. data/lib/async/matrix/bridge/discord/db/migrations/002_create_guilds.rb +0 -14
  69. data/lib/async/matrix/bridge/discord/db/migrations/003_create_portals.rb +0 -23
  70. data/lib/async/matrix/bridge/discord/db/migrations/004_create_puppets.rb +0 -19
  71. data/lib/async/matrix/bridge/discord/db/migrations/005_create_messages.rb +0 -20
  72. data/lib/async/matrix/bridge/discord/db/migrations/006_create_reactions.rb +0 -19
  73. data/lib/async/matrix/bridge/discord/db/migrations/007_create_files.rb +0 -18
  74. data/lib/async/matrix/bridge/discord/db/portal.rb +0 -150
  75. data/lib/async/matrix/bridge/discord/db/puppet.rb +0 -128
  76. data/lib/async/matrix/bridge/discord/db/reaction.rb +0 -165
  77. data/lib/async/matrix/bridge/discord/db/schema.rb +0 -18
  78. data/lib/async/matrix/bridge/discord/db/user.rb +0 -112
  79. 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: fbcd762ae4584a018455d875317aed89d4a40751b3c5ded892451411d95cf89c
4
- data.tar.gz: 49f07effb6009235cab4ac84d727a4b34cde54c7406463dedd625f037eac7a81
3
+ metadata.gz: 65f191895135eb9d4f0580991c9beca1a319ed524c12b29d4f52c2cbff4aebc9
4
+ data.tar.gz: d3331bde0b5cea1d2e69081ab72747e72903f4c6977c9fb331e990ff0e1c31f4
5
5
  SHA512:
6
- metadata.gz: d01ca57fe8befccd8d0e66e23b2fe52ee76cdc934785106d620c29f267467aa387437097c0e23861de8fe0da4f71e3193f201b43bc12d0bf6184343cb7038759
7
- data.tar.gz: df8f42bfd7c0b163b17707be2412ef16f75350246ee2ddfed29217046c81277040800d75e8058da4e0d83a67c5ad39db30b5cdc2ce3b396035194325d8af4752
6
+ metadata.gz: deffa5fa0c1bb2cbdf0ca642400bfa67c0b4bdc2839472a3923f3fc1aacccc060159b0b5d8646ffb8d544b577020a1035c07956fb884132ad2293ff78e27a3f1
7
+ data.tar.gz: 35de48c7bf9adb335031dbf8c9036947bea84f57cf8c0c3a4b22551b3be682c1df584eb9cc883c09c1ec109fd25772d96ad6d978c96385e7e87149301b032ff4
data/Cargo.lock CHANGED
@@ -423,9 +423,9 @@ dependencies = [
423
423
 
424
424
  [[package]]
425
425
  name = "magnus"
426
- version = "0.8.2"
426
+ version = "0.9.2"
427
427
  source = "registry+https://github.com/rust-lang/crates.io-index"
428
- checksum = "3b36a5b126bbe97eb0d02d07acfeb327036c6319fd816139a49824a83b7f9012"
428
+ checksum = "b9068710e00d21be762edca59b9c4c8946dbdb29820d4f76ad559afea07982c9"
429
429
  dependencies = [
430
430
  "magnus-macros",
431
431
  "rb-sys",
@@ -435,9 +435,9 @@ dependencies = [
435
435
 
436
436
  [[package]]
437
437
  name = "magnus-macros"
438
- version = "0.8.0"
438
+ version = "0.9.1"
439
439
  source = "registry+https://github.com/rust-lang/crates.io-index"
440
- checksum = "47607461fd8e1513cb4f2076c197d8092d921a1ea75bd08af97398f593751892"
440
+ checksum = "9dafde2eeb66f090e0f326112323eb8324b12cb32b5d1210e541d3df6b0ce77e"
441
441
  dependencies = [
442
442
  "proc-macro2",
443
443
  "quote",
@@ -606,18 +606,18 @@ dependencies = [
606
606
 
607
607
  [[package]]
608
608
  name = "rb-sys"
609
- version = "0.9.128"
609
+ version = "0.9.130"
610
610
  source = "registry+https://github.com/rust-lang/crates.io-index"
611
- checksum = "45ca28513560e56cfb79a62b1fce363c73af170a182024ce880c77ee9429920a"
611
+ checksum = "02faf625bb10ba893e3ae620f19c9fb1b5f8fcae0fe4eb86bb3f2230fad75edb"
612
612
  dependencies = [
613
613
  "rb-sys-build",
614
614
  ]
615
615
 
616
616
  [[package]]
617
617
  name = "rb-sys-build"
618
- version = "0.9.128"
618
+ version = "0.9.130"
619
619
  source = "registry+https://github.com/rust-lang/crates.io-index"
620
- checksum = "ce04b2c55eff3a21aaa623fcc655d94373238e72cac6b3e1a3641ff31649f99a"
620
+ checksum = "05be6f9c86fe5482808162826f6c047d093b3670582296c770de1d94f8066694"
621
621
  dependencies = [
622
622
  "bindgen",
623
623
  "lazy_static",
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
 
@@ -11,7 +11,13 @@ crate-type = ["cdylib"]
11
11
  [dependencies]
12
12
  # magnus 0.8 is required for Ruby 4.0 C-API compatibility (0.7 fails to compile
13
13
  # against Ruby 4.0: rb_fiber_raise pointer mutability + RTypedData.typed_flag).
14
- magnus = "0.8"
14
+ #
15
+ # 0.9 is required to cross-compile for arm-linux-musl. On 32-bit musl, bindgen
16
+ # renders timespec's padding around tv_nsec as bitfield members, so magnus 0.8's
17
+ # `From<Timespec> for timespec` struct literal fails with E0063 (missing fields
18
+ # `_bitfield_1`, `_bitfield_align_1`). 0.9 builds the struct with mem::zeroed
19
+ # and assigns the two fields instead.
20
+ magnus = "0.9"
15
21
 
16
22
  # vodozemac 0.10.0 (2026-04-13), default features (libolm-compat).
17
23
  #
@@ -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