Skip to content

Commit 6e2e702

Browse files
authored
0.3.0 (#8)
* 6.1.0 builds * update README for 0.3 * API changes - rename $Routing.prefix to $Routing.$prefix to avoid collision with a route named prefix - rename resolvedPath to path (path can be a string or a function, depending on the route definition, now) - add prefixedPath to work like rawPath but with the routing prefix - remove path from- and add prefixedPath to MacroRoutingRoute (protocol) + Update tests * don't escape arguments unless they need it
1 parent 04d6bd9 commit 6e2e702

File tree

12 files changed

+113
-35
lines changed

12 files changed

+113
-35
lines changed

.swift-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
6.2.0

Package.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
// swift-tools-version:6.2
2-
// The swift-tools-version declares the minimum version of Swift required to build this package.
1+
// swift-tools-version:6.1.0
32

43
import PackageDescription
54
import CompilerPluginSupport

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ The main benefits to this approach are:
9696
- route lookup with `Controller.$Routing.routeName` where `routeName` is the function name (or declared route name)
9797
- If you have a `@MacroRouting` controller, a `$Routing` property is synthesized (this is a controller-specific struct, which includes routing info for each of your declared routes), so you can look up routes progrmamatically, and at compile time (so you also get code completion, and you can change route paths by changing the value in `@GET("/login")`, seamlessly, if you don't change the name of `AuthController.logIn`, and you'll get help from the compiler if you *do* rename the `logIn` function.
9898
- you can still use the normal routing methods, including the documented [`RouteCollection`](https://docs.hummingbird.codes/2.0/documentation/hummingbird/routerguide#Route-Collections) + `addRoutes(…)` based approach
99-
- `hummingbird-macroroutes` provides a `RouteCollectionContainer` that wraps these to help hint that you shouldn't use the `atPath` signature (see below)
99+
- `hummingbird-MacroRouting` provides a `RouteCollectionContainer` that wraps these to help hint that you shouldn't use the `atPath` signature (see below)
100100
- a `.$routes` var is synthesized on the controller to contain this `RouteCollectionContainer`
101101
- you can construct route paths based on path arguments, all statically, so if anything changes, the compiler will warn you
102102

@@ -105,7 +105,7 @@ The main benefits to this approach are:
105105
In your `Package.swift`, put this into your `.dependencies`:
106106

107107
```swift
108-
.package(url: "https://github.com/sloatescoan/hummingbird-macrorouting.git", from: "0.2.1")
108+
.package(url: "https://github.com/sloatescoan/hummingbird-macrorouting.git", from: "0.3.0")
109109
```
110110

111111
…and in your `.target`/`.executableTarget`:
@@ -182,19 +182,19 @@ Additionally, route paths with arguments can be resolved through the synthesized
182182
}
183183
```
184184

185-
Where you might normally get the logs path with `ApiController.$Routing.logs.path`, here, the path has arguments. `.path` would return `/api/logs/{userId}/{timing}`, which isn't exactly useful for passing to a client if you want them to fetch "my logs for today", for example.
185+
Where you might normally get the logs path with `ApiController.$Routing.logs.path`, here, the path has arguments. If MacroRouting were to supply `.path` as a `String`, it would return `/api/logs/{userId}/{timing}`, which isn't exactly useful for passing to a client if you want them to fetch "my logs for today", for example.
186186

187-
This is where `.resolvedPath` comes in:
187+
This is where `.path()` comes in:
188188

189189
```swift
190-
let logsPath = ApiController.$Routing.logs.resolvedPath(userId: "123", timing: "2025-05-27")
190+
let logsPath = ApiController.$Routing.logs.path(userId: "123", timing: "2025-05-27")
191191
```
192192

193193
This will return: `/api/logs/123/2025-05-27`.
194194

195-
The argument names are synthesized by the HummingbirdMacroRouting, so they're available to well-behaving editors/IDEs:
195+
The argument names are synthesized by MacroRouting, so they're available to well-behaving editors/IDEs:
196196

197-
![IDE completion of `ApiController.$Routing.logs.resolvedPath`](https://files.scoat.es/IKYWGNmUCq.gif)
197+
![IDE completion of `ApiController.$Routing.logs.path`](https://files.scoat.es/IKYWGNmUCq.gif)
198198

199199
## Tests
200200

Sources/HummingbirdMacroRouting/Protocols/MacroRoutingRoute.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Hummingbird
22

33
public protocol MacroRoutingRoute: Sendable {
44
static var method: HTTPRequest.Method {get}
5-
static var path: String {get}
5+
static var prefixedPath: String {get}
66
static var rawPath: String {get}
77
static var name: String {get}
88
static var handler: String {get}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// this list is from
2+
// https://github.com/swiftlang/swift-syntax/blob/974c292380c1b7961f901ce43dfc794e8d7ffa57/CodeGeneration/Sources/SyntaxSupport/KeywordSpec.swift
3+
// it may need updating over time
4+
enum ReservedWord: String, RawRepresentable {
5+
case `Any`
6+
case `as`
7+
case `associatedtype`
8+
case `break`
9+
case `case`
10+
case `catch`
11+
case `class`
12+
case `continue`
13+
case `default`
14+
case `defer`
15+
case `deinit`
16+
case `do`
17+
case `else`
18+
case `enum`
19+
case `extension`
20+
case `fallthrough`
21+
case `false`
22+
case `fileprivate`
23+
case `for`
24+
case `func`
25+
case `guard`
26+
case `if`
27+
case `import`
28+
case `in`
29+
case `init`
30+
case `inout`
31+
case `internal`
32+
case `is`
33+
case `let`
34+
case `nil`
35+
case `operator`
36+
case `precedencegroup`
37+
case `private`
38+
case `Protocol`
39+
case `protocol`
40+
case `public`
41+
case `repeat`
42+
case `rethrows`
43+
case `return`
44+
case `self`
45+
case `Self`
46+
case `static`
47+
case `struct`
48+
case `subscript`
49+
case `super`
50+
case `switch`
51+
case `throw`
52+
case `throws`
53+
case `true`
54+
case `try`
55+
case `Type`
56+
case `typealias`
57+
case `var`
58+
case `where`
59+
case `while`
60+
}

Sources/RoutingMacros/RoutingMacros.swift

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ public struct RoutingMacro: ExtensionMacro {
167167
static let $all: [any MacroRoutingRoute.Type] = [
168168
\(routes.map({ "`" + $0.name + "`" + ".self" }).joined(separator: ", "))
169169
]
170-
static let prefix: String? = \(prefix == nil ? "nil" : "\"\(prefix!)\"")
170+
static let $prefix: String? = \(prefix == nil ? "nil" : "\"\(prefix!)\"")
171171
"""
172172

173173
for route in routes {
@@ -195,17 +195,25 @@ public struct RoutingMacro: ExtensionMacro {
195195
struct `\(route.name)`: MacroRoutingRoute {
196196
private init() {}
197197
static let method: HTTPRequest.Method = .\(route.method.rawValue.lowercased())
198-
static let rawPath: String = "\(route.path)"
199198
static let handler: String = "\(route.handler)"
200199
static let name: String = "\(route.name)"
200+
static let prefixedPath: String = "\(prefixedPath)"
201+
static let rawPath: String = "\(route.path)"
201202
"""
202203

203204
if captured.count > 0 {
204-
// for routes that have captured arguments, mark path as deprecated, and provide resolvedPath
205+
// for routes that have captured arguments, provide path(…) (formerly resolvedPath(…))
205206
code += """
206-
@available(*, deprecated, renamed: "rawPath", message: "path will be removed for routes that have captured arguments; you probably want resolvedPath(…)")
207-
static let path: String = "\(prefixedPath)"
207+
@available(*, deprecated, renamed: "path", message: "resolvedPath(…) has been renamed to path(…)")
208208
static func resolvedPath(\(captured.map({ "\($0): String"}).joined(separator: ", "))) -> String {
209+
path(\(captured.map({
210+
ReservedWord(rawValue: $0) == nil ?
211+
"\($0): \($0)"
212+
:
213+
"`\($0)`: `\($0)`"
214+
}).joined(separator: ", ")))
215+
}
216+
static func path(\(captured.map({ "\($0): String"}).joined(separator: ", "))) -> String {
209217
"/\(out.joined(separator: "/"))"
210218
}
211219
"""

Tests/HummingbirdMacroRoutingTests.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ struct SimpleMacroRoutingTests {
1212
func testStructureStatic() {
1313
#expect(Controller.$Routing.logIn.method == .get)
1414
#expect(Controller.$Routing.logIn.path == "/login")
15+
#expect(Controller.$Routing.logIn.prefixedPath == "/login")
1516

1617
#expect(Controller.$Routing.logOutHandler.method == .post)
1718
#expect(Controller.$Routing.logOutHandler.path == "/logout")
19+
#expect(Controller.$Routing.logOutHandler.prefixedPath == "/logout")
1820
}
1921

2022
@Test("Instance Structure")

Tests/Sources/TrickyMacroRoutingController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ struct TrickyMacroRoutingController {
1010
return .init(status: .ok, body: .init(byteBuffer: ByteBuffer(string: "Logged in")))
1111
}
1212

13-
@POST("/func/{throw}/{catch}")
13+
@POST("/func/{foo}/{throw}/{catch}")
1414
@Sendable func `func`(request: Request, context: Context) async throws -> Response {
1515
return .init(status: .ok, body: .init(byteBuffer: ByteBuffer(string: "Logged out")))
1616
}

Tests/TestPathArguments.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,40 +11,40 @@ struct MacroRoutingTestPathArguments {
1111
@Test("Static Structure")
1212
func testStructureStatic() {
1313
#expect(Controller.$Routing.bookTitle.method == .get)
14-
#expect(Controller.$Routing.bookTitle.path == "/book/:title")
14+
#expect(Controller.$Routing.bookTitle.rawPath == "/book/:title")
1515
}
1616

1717
@Test("Replacements")
1818
func testReplacements() {
1919
#expect(
20-
Controller.$Routing.bookTitle.resolvedPath(title: "cryptonomicon") == "/book/cryptonomicon"
20+
Controller.$Routing.bookTitle.path(title: "cryptonomicon") == "/book/cryptonomicon"
2121
)
2222
#expect(
23-
Controller.$Routing.movieTitle.resolvedPath(title: "ratatouille") == "/movie/ratatouille"
23+
Controller.$Routing.movieTitle.path(title: "ratatouille") == "/movie/ratatouille"
2424
)
2525

2626
#expect(
27-
Controller.$Routing.bookTitleYear.resolvedPath(
27+
Controller.$Routing.bookTitleYear.path(
2828
title: "cryptonomicon", year: "1999"
2929
) == "/book/cryptonomicon/1999"
3030
)
3131
#expect(
32-
Controller.$Routing.movieTitleYear.resolvedPath(
32+
Controller.$Routing.movieTitleYear.path(
3333
title: "ratatouille", year: "2007"
3434
) == "/movie/ratatouille/2007"
3535
)
3636

37-
#expect(Controller.$Routing.other.path == "/other/{one}/{two}/{three}/{four}/{five}")
37+
#expect(Controller.$Routing.other.rawPath == "/other/{one}/{two}/{three}/{four}/{five}")
3838
#expect(
39-
Controller.$Routing.other.resolvedPath(
39+
Controller.$Routing.other.path(
4040
one: "apple", two: "banana", three: "carrot",
4141
four: "durian", five: "eggplant"
4242
) == "/other/apple/banana/carrot/durian/eggplant"
4343
)
4444

45-
#expect(Controller.$Routing.mixed.path == "/mixed/{one}/:two/{three}/:four/{five}")
45+
#expect(Controller.$Routing.mixed.rawPath == "/mixed/{one}/:two/{three}/:four/{five}")
4646
#expect(
47-
Controller.$Routing.mixed.resolvedPath(
47+
Controller.$Routing.mixed.path(
4848
one: "apple", two: "banana", three: "carrot",
4949
four: "durian", five: "eggplant"
5050
) == "/mixed/apple/banana/carrot/durian/eggplant"

Tests/TestPrefix.swift

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ struct MacroRoutingTestPrefix {
1010

1111
@Test("Static Structure")
1212
func testStructureStatic() {
13-
#expect(Controller.$Routing.prefix == "/api")
13+
#expect(Controller.$Routing.$prefix == "/api")
1414

1515
#expect(Controller.$Routing.auth.method == .get)
16-
#expect(Controller.$Routing.auth.path == "/api/auth")
17-
#expect(Controller.$Routing.deauth.path == "/api/deauth")
16+
#expect(Controller.$Routing.auth.prefixedPath == "/api/auth")
17+
#expect(Controller.$Routing.deauth.prefixedPath == "/api/deauth")
1818
#expect(Controller.$Routing.auth.rawPath == "/auth")
1919
#expect(Controller.$Routing.deauth.rawPath == "/deauth")
20+
#expect(Controller.$Routing.auth.path == "/api/auth")
21+
#expect(Controller.$Routing.deauth.path == "/api/deauth")
2022
}
2123

2224
@Test("Instance Structure")
@@ -49,7 +51,11 @@ struct MacroRoutingTestPrefix {
4951

5052
@Test("Resolved Path")
5153
func testPathResolution() async throws {
54+
#expect(Controller.$Routing.auth.rawPath == "/auth")
55+
#expect(Controller.$Routing.auth.prefixedPath == "/api/auth")
5256
#expect(Controller.$Routing.auth.path == "/api/auth")
53-
#expect(Controller.$Routing.deauthId.resolvedPath(id: "test") == "/api/deauth/test")
57+
#expect(Controller.$Routing.deauthId.rawPath == "/deauth/:id")
58+
#expect(Controller.$Routing.deauthId.prefixedPath == "/api/deauth/:id")
59+
#expect(Controller.$Routing.deauthId.path(id: "test") == "/api/deauth/test")
5460
}
5561
}

0 commit comments

Comments
 (0)