Skip to content

Commit

Permalink
Route Parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
swhitty committed Jul 13, 2024
1 parent 79cf615 commit b37b5ab
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 17 deletions.
8 changes: 8 additions & 0 deletions FlyingFox/Sources/HTTPServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ public final actor HTTPServer {
}

#if compiler(>=5.9)

public func appendRoute<each P: HTTPRouteParameterValue>(
_ route: HTTPRoute,
handler: @Sendable @escaping (HTTPRequest, repeat each P) async throws -> HTTPResponse
) {
handlers.appendRoute(route, handler: handler)
}

public func appendRoute<each P: HTTPRouteParameterValue>(
_ route: HTTPRoute,
handler: @Sendable @escaping (repeat each P) async throws -> HTTPResponse
Expand Down
12 changes: 12 additions & 0 deletions FlyingFox/Sources/Handlers/RoutedHTTPHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ public struct RoutedHTTPHandler: HTTPHandler, Sendable {
}

#if compiler(>=5.9)

public mutating func appendRoute<each P: HTTPRouteParameterValue>(
_ route: HTTPRoute,
handler: @Sendable @escaping (HTTPRequest, repeat each P) async throws -> HTTPResponse
) {
let closure = ClosureHTTPHandler { request in
let params = try route.extractParameterValues(of: (repeat each P).self, from: request)
return try await handler(request, repeat each params)
}
append((route, closure))
}

public mutating func appendRoute<each P: HTTPRouteParameterValue>(
_ route: HTTPRoute,
handler: @Sendable @escaping (repeat each P) async throws -> HTTPResponse
Expand Down
9 changes: 7 additions & 2 deletions FlyingFox/Tests/HTTPRequest+Mock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,13 @@ extension HTTPRequest {
body: body)
}

static func make(_ url: String) -> Self {
static func make(method: HTTPMethod = .GET, _ url: String, headers: [HTTPHeader: String] = [:]) -> Self {
let (path, query) = HTTPDecoder.readComponents(from: url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)
return HTTPRequest.make(path: path, query: query)
return HTTPRequest.make(
method: method,
path: path,
query: query,
headers: headers
)
}
}
6 changes: 6 additions & 0 deletions FlyingFox/Tests/HTTPResponse+Mock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,10 @@ extension HTTPResponse {
HTTPResponse(headers: headers,
webSocket: handler)
}

var bodyString: String? {
get async throws {
try await String(data: bodyData, encoding: .utf8)
}
}
}
21 changes: 16 additions & 5 deletions FlyingFox/Tests/HTTPServerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -437,21 +437,32 @@ final class HTTPServerTests: XCTestCase {
}

#if compiler(>=5.9)
func testRoutes_To_ParamaterPack() async throws {

func testRoutes_To_ParamaterPackWithRequest() async throws {
let server = HTTPServer.make()
await server.appendRoute("/fish/:id") { (id: String) in
await server.appendRoute("/fish/:id") { (request: HTTPRequest, id: String) in
HTTPResponse.make(statusCode: .ok, body: "Hello \(id)".data(using: .utf8)!)
}
await server.appendRoute("/chips/:id") { (id: String) in
HTTPResponse.make(statusCode: .ok, body: "Hello \(id)".data(using: .utf8)!)
}
let port = try await startServerWithPort(server)

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

try await socket.writeRequest(.make(path: "/fish/chips"))
try await socket.writeRequest(.make("/fish/🐟", headers: [.connection: "keep-alive"]))

await AsyncAssertEqual(
try await socket.readResponse().bodyString,
"Hello 🐟"
)

try await socket.writeRequest(.make("/chips/🍟"))

await AsyncAssertEqual(
try await socket.readResponse().bodyData,
"Hello chips".data(using: .utf8)
try await socket.readResponse().bodyString,
"Hello 🍟"
)
}
#endif
Expand Down
10 changes: 0 additions & 10 deletions FlyingFox/Tests/Handlers/RoutedHTTPHandlerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,3 @@ private extension HTTPRoute {
return methods + " /" + path
}
}

private extension HTTPResponse {

var bodyString: String? {
get async throws {
try await String(data: bodyData, encoding: .utf8)
}
}

}
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- [Usage](#usage)
- [Handlers](#handlers)
- [Routes](#routes)
- [Route Parameters](#route-parameters)
- [Macros](#preview-macro-handler)
- [WebSockets](#websockets)
- [FlyingSocks](#flyingsocks)
Expand Down Expand Up @@ -213,6 +214,14 @@ route ~= HTTPRequest(method: .GET, path: "/hello/dog/world") // true
route ~= HTTPRequest(method: .GET, path: "/hello/fish/sea") // false
```

Routes can include [parameters](#route-parameters) that match like wildcards allowing handlers to extract the value from the request.

```swift
let route = HTTPRoute("GET /hello/:beast/world")

let beast = request.routeParameters["beast"]
```

Trailing wildcards match all trailing path components:

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

## Route Parameters

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`.

```swift
handler.appendRoute("GET /creature/:name?type=:beast") { request in
let name = request.routeParameters["name"]
let beast = request.routeParameters["beast"]
return HTTPResponse(statusCode: .ok)
}
```

When using Swift 5.9+, route parameters can be automatically extracted and mapped to closure parameters of handlers.

```swift
enum Beast: String, HTTPRouteParameterValue {
case fish
case dog
}

handler.appendRoute("GET /creature/:name?type=:beast") { (name: String, beast: Beast) -> HTTPResponse in
return HTTPResponse(statusCode: .ok)
}
```

The request can be optionally included.

```swift
handler.appendRoute("GET /creature/:name?type=:beast") { (request: HTTPRequest, name: String, beast: Beast) -> HTTPResponse in
return HTTPResponse(statusCode: .ok)
}
```

`String`, `Int`, `Double`, `Bool` and any type that conforms to `HTTPRouteParameterValue` can be extracted.

## WebSockets
`HTTPResponse` can switch the connection to the [WebSocket](https://datatracker.ietf.org/doc/html/rfc6455) protocol by provding a `WSHandler` within the response payload.

Expand Down

0 comments on commit b37b5ab

Please sign in to comment.