Skip to content

Commit

Permalink
Add additional result builder capabilities. (#584)
Browse files Browse the repository at this point in the history
* Add additional result builder capabilities.

- Adds support for conditional if-else statement syntax in result builders
- Adds support for conditional if statement syntax in result builders
- Adds support for for loop statement syntax in result builders
- Adds support for optional unwrapping statement syntax in result builders.
- Adds tests for validating the additional supported syntax.
- Runs script for updating contributors.

* Consolidate Conditional/Empty into Optional Middleware.

* Hide result builder helper types from documentation.

---------

Co-authored-by: Adam Fowler <[email protected]>
  • Loading branch information
connor-ricks and adam-fowler authored Oct 15, 2024
1 parent dd3d571 commit ac00a22
Show file tree
Hide file tree
Showing 6 changed files with 298 additions and 36 deletions.
5 changes: 5 additions & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ needs to be listed here.
- Brian Michel <[email protected]>
- Callum Todd <[email protected]>
- Chris <[email protected]>
- Connor Ricks <[email protected]>
- Derek Clarkson <[email protected]>
- Florion <[email protected]>
- Garrett Moseke <[email protected]>
- Iceman <[email protected]>
- Joannis Orlandos <[email protected]>
- Jonathan Pulfer <[email protected]>
Expand All @@ -32,6 +34,7 @@ needs to be listed here.
- 0xpablo <[email protected]>
- Michael Stegeman <[email protected]>
- Ronald Mannak <[email protected]>
- Carl Downing <[email protected]>
- Nicholas Trienens <[email protected]>
- Mahdi Bahrami <[email protected]>
- Joseph Heck <[email protected]>
Expand All @@ -40,8 +43,10 @@ needs to be listed here.
- Gao Lei <[email protected]>
- Kanz <[email protected]>
- Kia Abdi <[email protected]>
- Michael <[email protected]>
- Michael Critz <[email protected]>
- Patrick Heneise <[email protected]>
- xavgru12 <[email protected]>
- Ádám Rocska <[email protected]>

**Updating this list**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,6 @@
//
//===----------------------------------------------------------------------===//

// MARK: - Middleware2

public struct _Middleware2<M0: MiddlewareProtocol, M1: MiddlewareProtocol>: MiddlewareProtocol where M0.Input == M1.Input, M0.Context == M1.Context, M0.Output == M1.Output {
public typealias Input = M0.Input
public typealias Output = M0.Output
public typealias Context = M0.Context

@usableFromInline let m0: M0
@usableFromInline let m1: M1

@inlinable
public init(_ m0: M0, _ m1: M1) {
self.m0 = m0
self.m1 = m1
}

@inlinable
public func handle(_ input: M0.Input, context: M0.Context, next: (M0.Input, M0.Context) async throws -> M0.Output) async throws -> M0.Output {
try await self.m0.handle(input, context: context) { input, context in
try await self.m1.handle(input, context: context, next: next)
}
}
}

extension _Middleware2: RouterMiddleware where M0.Input == Request, M0.Output == Response {}

// MARK: - MiddlewareFixedTypeBuilder

/// Middleware stack result builder
///
/// Generates a middleware stack from the elements inside the result builder. The input,
Expand All @@ -64,4 +36,24 @@ public enum MiddlewareFixedTypeBuilder<Input, Output, Context> {
) -> _Middleware2<M0, M1> where M0.Input == M1.Input, M0.Output == M1.Output, M0.Context == M1.Context {
_Middleware2(m0, m1)
}

public static func buildOptional<M0: MiddlewareProtocol>(_ component: M0?) -> _OptionalMiddleware<M0> {
_OptionalMiddleware(middleware: component)
}

public static func buildEither<M0: MiddlewareProtocol>(
first content: M0
) -> M0 {
content
}

public static func buildEither<M0: MiddlewareProtocol>(
second content: M0
) -> M0 {
content
}

public static func buildArray<M0: MiddlewareProtocol>(_ components: [M0]) -> _SpreadMiddleware<M0> {
return _SpreadMiddleware(middlewares: components)
}
}
50 changes: 50 additions & 0 deletions Sources/Hummingbird/Middleware/MiddlewareModule/_Middleware2.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2024 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

/// A middleware that composes two other middlewares.
///
/// The two provided middlewares will be chained together, with `M0` first executing, followed by `M1`.
///
/// You won't typically construct this middleware directly, but instead will use result builder syntax.
///
/// ```swift
/// router.addMiddleware {
/// MiddlewareOne()
/// MiddlewareTwo()
/// }
/// ```
@_documentation(visibility: internal)
public struct _Middleware2<M0: MiddlewareProtocol, M1: MiddlewareProtocol>: MiddlewareProtocol where M0.Input == M1.Input, M0.Context == M1.Context, M0.Output == M1.Output {
public typealias Input = M0.Input
public typealias Output = M0.Output
public typealias Context = M0.Context

@usableFromInline let m0: M0
@usableFromInline let m1: M1

@inlinable
public init(_ m0: M0, _ m1: M1) {
self.m0 = m0
self.m1 = m1
}

@inlinable
public func handle(_ input: M0.Input, context: M0.Context, next: (M0.Input, M0.Context) async throws -> M0.Output) async throws -> M0.Output {
try await self.m0.handle(input, context: context) { input, context in
try await self.m1.handle(input, context: context, next: next)
}
}
}

extension _Middleware2: RouterMiddleware where M0.Input == Request, M0.Output == Response {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2024 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

/// A middleware that can handle an optional middleware.
///
/// This middleware is useful for situations where you want to optionally unwrap a middleware.
///
/// You won't typically construct this middleware directly, but instead will use standard `if`-`else`
/// statements in a parser builder to automatically build conditional middleware:
///
/// ```swift
/// router.addMiddleware {
/// if let middleware {
/// middleware
/// }
/// ...
/// }
/// ```
@_documentation(visibility: internal)
public struct _OptionalMiddleware<M0: MiddlewareProtocol>: MiddlewareProtocol {
public typealias Input = M0.Input
public typealias Output = M0.Output
public typealias Context = M0.Context

public let middleware: M0?

@inlinable
public func handle(_ input: M0.Input, context: M0.Context, next: (M0.Input, M0.Context) async throws -> M0.Output) async throws -> M0.Output {
guard let middleware else {
return try await next(input, context)
}

return try await middleware.handle(input, context: context, next: next)
}
}

extension _OptionalMiddleware: RouterMiddleware where M0.Input == Request, M0.Output == Response {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2024 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

/// A middleware that can handle an array of middleware.
///
/// This middleware is useful for situations where you want to compose an array of middleware together.
///
/// You won't typically construct this middleware directly, but instead will use standard `for` loop
/// statements in a result builder to automatically build spread middleware:
///
/// ```swift
/// router.addMiddleware {
/// for logger in loggers {
/// LoggingMiddleware(logger: logger)
/// }
/// }
/// ```
@_documentation(visibility: internal)
public struct _SpreadMiddleware<M0: MiddlewareProtocol>: MiddlewareProtocol {
public typealias Input = M0.Input
public typealias Output = M0.Output
public typealias Context = M0.Context

let middlewares: [M0]

public func handle(_ input: Input, context: Context, next: (Input, Context) async throws -> Output) async throws -> Output {
return try await handle(middlewares: self.middlewares, input: input, context: context, next: next)

func handle(middlewares: some Collection<M0>, input: Input, context: Context, next: (Input, Context) async throws -> Output) async throws -> Output {
guard let current = middlewares.first else {
return try await next(input, context)
}

return try await current.handle(input, context: context, next: { input, context in
try await handle(middlewares: middlewares.dropFirst(), input: input, context: context, next: next)
})
}
}
}

extension _SpreadMiddleware: RouterMiddleware where M0.Input == Request, M0.Output == Response {}
131 changes: 123 additions & 8 deletions Tests/HummingbirdTests/MiddlewareTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -452,14 +452,6 @@ final class MiddlewareTests: XCTestCase {
}

func testMiddlewareResultBuilder() async throws {
struct TestMiddleware<Context: RequestContext>: RouterMiddleware {
let string: String
public func handle(_ request: Request, context: Context, next: (Request, Context) async throws -> Response) async throws -> Response {
var response = try await next(request, context)
response.headers[values: .test].append(self.string)
return response
}
}
let router = Router()
router.addMiddleware {
TestMiddleware(string: "first")
Expand All @@ -477,6 +469,129 @@ final class MiddlewareTests: XCTestCase {
}
}
}

func testMiddlewareEitherResultBuilder() async throws {
func test(shouldUseFirst: Bool) async throws {
let router = Router()
router.addMiddleware {
if shouldUseFirst {
TestMiddleware(string: "first")
} else {
TestMiddleware(string: "second")
}
}
router.get("/hello") { _, _ in "Hello" }
let app = Application(responder: router.buildResponder())
try await app.test(.router) { client in
try await client.execute(uri: "/hello", method: .get) { response in
XCTAssertEqual(response.headers[values: .test].first, shouldUseFirst ? "first" : "second")
XCTAssertEqual(response.headers[values: .test].count, 1)
}
}
}

/// The first middleware should be the only middleware.
try await test(shouldUseFirst: true)
/// The second middleware should be the only middleware.
try await test(shouldUseFirst: false)
}

func testMiddlewareOptionalIfResultBuilder() async throws {
func test(shouldUseFirst: Bool) async throws {
let router = Router()
router.addMiddleware {
if shouldUseFirst {
TestMiddleware(string: "first")
}

TestMiddleware(string: "second")
}
router.get("/hello") { _, _ in "Hello" }
let app = Application(responder: router.buildResponder())
try await app.test(.router) { client in
try await client.execute(uri: "/hello", method: .get) { response in
// headers come back in opposite order as middleware is applied to responses in that order
XCTAssertEqual(response.headers[values: .test].first, "second")

if shouldUseFirst {
XCTAssertEqual(response.headers[values: .test].last, "first")
XCTAssertEqual(response.headers[values: .test].count, 2)
} else {
XCTAssertEqual(response.headers[values: .test].count, 1)
}
}
}
}

/// The first middleware should be included along with the second middleware.
try await test(shouldUseFirst: true)

/// The first middleware should be excluded, leaving only the second middleware.
try await test(shouldUseFirst: false)
}

func testMiddlewareOptionalUnwrapResultBuilder() async throws {
func test<M: RouterMiddleware>(middleware: M?) async throws where M.Context == BasicRequestContext {
let router = Router()
router.addMiddleware {
if let middleware {
middleware
}

TestMiddleware(string: "second")
}
router.get("/hello") { _, _ in "Hello" }
let app = Application(responder: router.buildResponder())
try await app.test(.router) { client in
try await client.execute(uri: "/hello", method: .get) { response in
// headers come back in opposite order as middleware is applied to responses in that order
XCTAssertEqual(response.headers[values: .test].first, "second")

if middleware != nil {
XCTAssertEqual(response.headers[values: .test].last, "first")
XCTAssertEqual(response.headers[values: .test].count, 2)
} else {
XCTAssertEqual(response.headers[values: .test].count, 1)
}
}
}
}

/// The first middleware should be included along with the second middleware.
try await test(middleware: TestMiddleware(string: "first"))

/// The first middleware should be excluded, leaving only the second middleware.
try await test(middleware: Optional<TestMiddleware>.none)
}

func testMiddlewareArrayResultBuilder() async throws {
let limit = 5
let router = Router()
router.addMiddleware {
for i in 0..<limit {
TestMiddleware(string: String(i))
}
}
router.get("/hello") { _, _ in "Hello" }
let app = Application(responder: router.buildResponder())
try await app.test(.router) { client in
try await client.execute(uri: "/hello", method: .get) { response in
XCTAssertEqual(response.headers[values: .test], (0..<limit).reversed().map {
String($0)
})
}
}
}
}

/// Middleware used in tests. Adds the provided `String` to the header's `.test` value.
struct TestMiddleware<Context: RequestContext>: RouterMiddleware {
let string: String
public func handle(_ request: Request, context: Context, next: (Request, Context) async throws -> Response) async throws -> Response {
var response = try await next(request, context)
response.headers[values: .test].append(self.string)
return response
}
}

/// LogHandler used in tests. Stores all log entries in provided `LogAccumalator``
Expand Down

0 comments on commit ac00a22

Please sign in to comment.