Skip to content

Commit b37b5ab

Browse files
committed
Route Parameters
1 parent 79cf615 commit b37b5ab

File tree

7 files changed

+93
-17
lines changed

7 files changed

+93
-17
lines changed

FlyingFox/Sources/HTTPServer.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,14 @@ public final actor HTTPServer {
6969
}
7070

7171
#if compiler(>=5.9)
72+
73+
public func appendRoute<each P: HTTPRouteParameterValue>(
74+
_ route: HTTPRoute,
75+
handler: @Sendable @escaping (HTTPRequest, repeat each P) async throws -> HTTPResponse
76+
) {
77+
handlers.appendRoute(route, handler: handler)
78+
}
79+
7280
public func appendRoute<each P: HTTPRouteParameterValue>(
7381
_ route: HTTPRoute,
7482
handler: @Sendable @escaping (repeat each P) async throws -> HTTPResponse

FlyingFox/Sources/Handlers/RoutedHTTPHandler.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ public struct RoutedHTTPHandler: HTTPHandler, Sendable {
4545
}
4646

4747
#if compiler(>=5.9)
48+
49+
public mutating func appendRoute<each P: HTTPRouteParameterValue>(
50+
_ route: HTTPRoute,
51+
handler: @Sendable @escaping (HTTPRequest, repeat each P) async throws -> HTTPResponse
52+
) {
53+
let closure = ClosureHTTPHandler { request in
54+
let params = try route.extractParameterValues(of: (repeat each P).self, from: request)
55+
return try await handler(request, repeat each params)
56+
}
57+
append((route, closure))
58+
}
59+
4860
public mutating func appendRoute<each P: HTTPRouteParameterValue>(
4961
_ route: HTTPRoute,
5062
handler: @Sendable @escaping (repeat each P) async throws -> HTTPResponse

FlyingFox/Tests/HTTPRequest+Mock.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,13 @@ extension HTTPRequest {
4747
body: body)
4848
}
4949

50-
static func make(_ url: String) -> Self {
50+
static func make(method: HTTPMethod = .GET, _ url: String, headers: [HTTPHeader: String] = [:]) -> Self {
5151
let (path, query) = HTTPDecoder.readComponents(from: url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)
52-
return HTTPRequest.make(path: path, query: query)
52+
return HTTPRequest.make(
53+
method: method,
54+
path: path,
55+
query: query,
56+
headers: headers
57+
)
5358
}
5459
}

FlyingFox/Tests/HTTPResponse+Mock.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,10 @@ extension HTTPResponse {
7575
HTTPResponse(headers: headers,
7676
webSocket: handler)
7777
}
78+
79+
var bodyString: String? {
80+
get async throws {
81+
try await String(data: bodyData, encoding: .utf8)
82+
}
83+
}
7884
}

FlyingFox/Tests/HTTPServerTests.swift

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -437,21 +437,32 @@ final class HTTPServerTests: XCTestCase {
437437
}
438438

439439
#if compiler(>=5.9)
440-
func testRoutes_To_ParamaterPack() async throws {
440+
441+
func testRoutes_To_ParamaterPackWithRequest() async throws {
441442
let server = HTTPServer.make()
442-
await server.appendRoute("/fish/:id") { (id: String) in
443+
await server.appendRoute("/fish/:id") { (request: HTTPRequest, id: String) in
444+
HTTPResponse.make(statusCode: .ok, body: "Hello \(id)".data(using: .utf8)!)
445+
}
446+
await server.appendRoute("/chips/:id") { (id: String) in
443447
HTTPResponse.make(statusCode: .ok, body: "Hello \(id)".data(using: .utf8)!)
444448
}
445449
let port = try await startServerWithPort(server)
446450

447451
let socket = try await AsyncSocket.connected(to: .inet(ip4: "127.0.0.1", port: port))
448452
defer { try? socket.close() }
449453

450-
try await socket.writeRequest(.make(path: "/fish/chips"))
454+
try await socket.writeRequest(.make("/fish/🐟", headers: [.connection: "keep-alive"]))
455+
456+
await AsyncAssertEqual(
457+
try await socket.readResponse().bodyString,
458+
"Hello 🐟"
459+
)
460+
461+
try await socket.writeRequest(.make("/chips/🍟"))
451462

452463
await AsyncAssertEqual(
453-
try await socket.readResponse().bodyData,
454-
"Hello chips".data(using: .utf8)
464+
try await socket.readResponse().bodyString,
465+
"Hello 🍟"
455466
)
456467
}
457468
#endif

FlyingFox/Tests/Handlers/RoutedHTTPHandlerTests.swift

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,3 @@ private extension HTTPRoute {
145145
return methods + " /" + path
146146
}
147147
}
148-
149-
private extension HTTPResponse {
150-
151-
var bodyString: String? {
152-
get async throws {
153-
try await String(data: bodyData, encoding: .utf8)
154-
}
155-
}
156-
157-
}

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- [Usage](#usage)
1414
- [Handlers](#handlers)
1515
- [Routes](#routes)
16+
- [Route Parameters](#route-parameters)
1617
- [Macros](#preview-macro-handler)
1718
- [WebSockets](#websockets)
1819
- [FlyingSocks](#flyingsocks)
@@ -213,6 +214,14 @@ route ~= HTTPRequest(method: .GET, path: "/hello/dog/world") // true
213214
route ~= HTTPRequest(method: .GET, path: "/hello/fish/sea") // false
214215
```
215216

217+
Routes can include [parameters](#route-parameters) that match like wildcards allowing handlers to extract the value from the request.
218+
219+
```swift
220+
let route = HTTPRoute("GET /hello/:beast/world")
221+
222+
let beast = request.routeParameters["beast"]
223+
```
224+
216225
Trailing wildcards match all trailing path components:
217226

218227
```swift
@@ -280,6 +289,41 @@ let route = HTTPRoute("POST *", body: .json(where: "food == 'fish'"))
280289
{"side": "chips", "food": "fish"}
281290
```
282291

292+
## Route Parameters
293+
294+
Routes can include named parameters within a path or query item using the `:` prefix. Any string supplied to this parameter will match the route, handlers can access the value of the string using `request.routePamaters`.
295+
296+
```swift
297+
handler.appendRoute("GET /creature/:name?type=:beast") { request in
298+
let name = request.routeParameters["name"]
299+
let beast = request.routeParameters["beast"]
300+
return HTTPResponse(statusCode: .ok)
301+
}
302+
```
303+
304+
When using Swift 5.9+, route parameters can be automatically extracted and mapped to closure parameters of handlers.
305+
306+
```swift
307+
enum Beast: String, HTTPRouteParameterValue {
308+
case fish
309+
case dog
310+
}
311+
312+
handler.appendRoute("GET /creature/:name?type=:beast") { (name: String, beast: Beast) -> HTTPResponse in
313+
return HTTPResponse(statusCode: .ok)
314+
}
315+
```
316+
317+
The request can be optionally included.
318+
319+
```swift
320+
handler.appendRoute("GET /creature/:name?type=:beast") { (request: HTTPRequest, name: String, beast: Beast) -> HTTPResponse in
321+
return HTTPResponse(statusCode: .ok)
322+
}
323+
```
324+
325+
`String`, `Int`, `Double`, `Bool` and any type that conforms to `HTTPRouteParameterValue` can be extracted.
326+
283327
## WebSockets
284328
`HTTPResponse` can switch the connection to the [WebSocket](https://datatracker.ietf.org/doc/html/rfc6455) protocol by provding a `WSHandler` within the response payload.
285329

0 commit comments

Comments
 (0)