Skip to content

Commit f8abf0d

Browse files
committed
🧪 ✨ 📚 Add userInfo and improve docs.
1 parent 6fe13be commit f8abf0d

File tree

4 files changed

+92
-27
lines changed

4 files changed

+92
-27
lines changed

README.md

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@ You can use `Group` to help organize your routes and cut down on the repetitiven
2727
app.register {
2828
Group("api", "v1") {
2929
Group("movies") {
30+
GET { ... }
3031
GET("latest") { ... }
3132
GET("popular") { ... }
3233
GET(":movie") { ... }
3334
}
3435
Group("books") {
36+
GET { ... }
3537
GET("new") { ... }
3638
GET("trending") { ... }
3739
GET(":book") { ... }
@@ -68,17 +70,16 @@ If you have more than one middleware... no worries, `.middleware(_:)` accepts a
6870
```swift
6971
Group {
7072
GET("foo") { ... }
71-
Group {
72-
GET("bar") { ... }
73-
}
74-
.middleware(Authentication(), Validator())
73+
GET("bar") { ... }
74+
.middleware(Authentication(), Validator())
75+
.middleware(Reporter())
7576
}
7677
.middleware(Logging())
7778
```
7879

79-
Remember that order matters here. Incoming requests will always execute middleware from top to bottom. So in the above example, the order of an incoming request would be as follows ➡️ `Logging`, `Authentication`, `Validator`. Outgoing respones will always execute middleware in the reverse order. ➡️ `Validator`, `Authentication`, `Logging`.
80+
Remember that order matters here. Incoming requests will always execute middleware from the top of the tree to the bottom. So in the above example, the order of an incoming request would be as follows ➡️ `Logging`, `Reporter`, `Authentication`, `Validator`. Outgoing respones will always execute middleware in the reverse order. ➡️ `Validator`, `Authentication`, `Reporter`, `Logging`.
8081

81-
### Making Custom Route Components
82+
### Custom Route Components
8283

8384
Often times, as your routes grow, a single large definition can become unwieldly and cumbersome to read and update. Organization of routes can be straightforward with `@RouteBuilder`
8485

@@ -168,7 +169,6 @@ app.register {
168169

169170
For existing vapor applications, it may be unreasonable or unwieldly to rewrite your entire routing stack in one go. You can start with replacing smaller sections of your route definitions by registering a `RouteComponent` on any `RoutesBuilder` in your application.
170171

171-
172172
```swift
173173
let users = app.grouped("users")
174174
users.get(":user") { ... }
@@ -182,25 +182,16 @@ books.register {
182182
}
183183
```
184184

185-
### RouteModifiers
186-
187-
- Currently undocumented.
185+
### Route Metadata
188186

189-
## TODO
190-
191-
- [Confirmed] Implement Websocket support
192-
- [Confirmed] Handle routes at the root of a RouteComponent.
187+
Vapor supports adding metadata to your routes. To add your own metadata to a route within a `@RouteBuilder`, make use of either the `.description(_:)` modifier or the `.userInfo(key:value:)` modifier.
193188

194189
```swift
195-
Group(":movie") {
196-
... How do we handle JUST ":movie"?
197-
GET("credits") { ... }
198-
GET("category") { ... }
190+
Group {
191+
GET("hello") {
192+
...
193+
}
194+
.description("Says hello")
195+
.userInfo(key: "isBeta", value: true)
199196
}
200-
```
201-
202-
- [Maybe] Implement EnvironmentObject style support.
203-
- [Maybe] Implement Service support
204-
- [Maybe] Route modifier for description.
205-
- [Maybe] Route modifier for caseInsensitive.
206-
- [Maybe] Route modifier for defaultMaxBodySize.
197+
```
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//
2+
// MIT License
3+
//
4+
// Copyright (c) 2024 Connor Ricks
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files (the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions:
12+
//
13+
// The above copyright notice and this permission notice shall be included in all
14+
// copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
// SOFTWARE.
23+
24+
import Vapor
25+
26+
extension Route {
27+
/// Adds the provided value to the route's `userInfo` dictionary at the sepcified key.
28+
///
29+
/// > Important: Assigning a value to a key that already has a value will overwrite the previously set value.
30+
func userInfo(key: AnySendableHashable, value: Sendable) -> Route {
31+
userInfo[key] = value
32+
return self
33+
}
34+
}

Tests/VaporRouteBuilderTests/Helpers/XCTApplicationTest+Testing.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ extension XCTApplicationTester {
3737
line: Int = #line,
3838
column: Int = #column,
3939
assertMiddleware middleware: [TestMiddleware] = [],
40-
beforeRequest: @escaping (inout XCTHTTPRequest) async throws -> Void = { _ in }
40+
beforeRequest: @escaping (inout XCTHTTPRequest) async throws -> Void = { _ in },
41+
afterResponse: ((XCTHTTPResponse) async throws -> Void)? = nil
4142
) async throws -> XCTApplicationTester {
4243
func test() async throws -> XCTApplicationTester {
4344
try await self.test(
@@ -48,7 +49,7 @@ extension XCTApplicationTester {
4849
file: filePath,
4950
line: UInt(line),
5051
beforeRequest: beforeRequest,
51-
afterResponse: { res async in
52+
afterResponse: afterResponse ?? { res async in
5253
#expect(
5354
path == res.body.string,
5455
sourceLocation: SourceLocation(

Tests/VaporRouteBuilderTests/RouteComponents/RouteTests.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,43 @@ import XCTVapor
3434
#expect(app.routes.all.count == 1)
3535
}
3636
}
37+
38+
@Test func test_route_asChildOfGroupWithoutPath_producesRouteMatchingParent() async throws {
39+
try await Application.testing(content: {
40+
Group("A") {
41+
DELETE { _ in "delete" }
42+
GET { _ in "get" }
43+
PATCH { _ in "patch" }
44+
POST { _ in "post" }
45+
PUT { _ in "put" }
46+
Route(.OPTIONS) { _ in "options" }
47+
}
48+
}) { app in
49+
try await app.testing(.DELETE, "/A", afterResponse: { res in
50+
#expect(res.body.string == "delete")
51+
})
52+
53+
try await app.testing(.GET, "/A", afterResponse: { res in
54+
#expect(res.body.string == "get")
55+
})
56+
57+
try await app.testing(.PATCH, "/A", afterResponse: { res in
58+
#expect(res.body.string == "patch")
59+
})
60+
61+
try await app.testing(.POST, "/A", afterResponse: { res in
62+
#expect(res.body.string == "post")
63+
})
64+
65+
try await app.testing(.PUT, "/A", afterResponse: { res in
66+
#expect(res.body.string == "put")
67+
})
68+
69+
try await app.testing(.OPTIONS, "/A", afterResponse: { res in
70+
#expect(res.body.string == "options")
71+
})
72+
73+
#expect(app.routes.all.count == 6)
74+
}
75+
}
3776
}

0 commit comments

Comments
 (0)