From 66e8c317b0c09c3f832b49eb0b6cdaa7ba54746c Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Mon, 5 Jan 2026 19:11:44 +0100 Subject: [PATCH 1/2] Update tutorial for new template --- .vscode/settings.json | 5 ++- .../code/Postgres/todos-postgres-03.swift | 26 +++++------ .../code/Postgres/todos-postgres-04.swift | 26 +++++------ .../code/Postgres/todos-postgres-05.swift | 33 +++++++------- .../code/Postgres/todos-postgres-06.swift | 20 --------- .../code/Postgres/todos-postgres-07.swift | 19 -------- .../code/Postgres/todos-postgres-08.swift | 15 ------- .../code/Postgres/todos-postgres-09.swift | 22 ++++----- .../code/Postgres/todos-postgres-10.swift | 21 ++++----- .../code/Postgres/todos-postgres-14.swift | 25 +++++------ .../code/Postgres/todos-postgres-21.swift | 19 ++++---- .../code/Postgres/todos-postgres-22.swift | 20 +++++---- .../code/Template/todos-template-02.sh | 4 +- .../code/Template/todos-template-03.swift | 24 +++++----- .../code/Template/todos-template-04.swift | 34 ++++++-------- .../code/Template/todos-template-05.swift | 17 +++---- .../code/Template/todos-template-07.sh | 2 +- .../code/Template/todos-template-08.sh | 10 +++-- .../code/Testing/todos-testing-01.swift | 24 +++++----- .../code/Testing/todos-testing-02.swift | 26 ++++++----- .../code/Testing/todos-testing-03.swift | 10 ----- .../code/Testing/todos-testing-08.swift | 25 +++-------- .../code/Testing/todos-testing-09.swift | 29 +++--------- .../code/Testing/todos-testing-10.swift | 45 ++++++++----------- .../code/Testing/todos-testing-11.swift | 34 ++++++-------- .../code/Testing/todos-testing-12.swift | 10 +---- .../code/Testing/todos-testing-13.swift | 16 ++----- .../Tutorials/Todos/Todos-1-Template.tutorial | 8 ++-- .../Tutorials/Todos/Todos-2-API.tutorial | 10 ++--- .../Tutorials/Todos/Todos-3-Testing.tutorial | 10 ++--- .../Tutorials/Todos/Todos-4-Postgres.tutorial | 18 ++------ 31 files changed, 226 insertions(+), 381 deletions(-) delete mode 100644 Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-06.swift delete mode 100644 Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-07.swift delete mode 100644 Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-08.swift delete mode 100644 Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-03.swift diff --git a/.vscode/settings.json b/.vscode/settings.json index 0b3a2feee4..1c97478872 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,6 @@ { - "swift.sourcekit-lsp.backgroundIndexing": "off" + "swift.sourcekit-lsp.backgroundIndexing": "off", + "[swift]": { + "editor.formatOnSave": false + } } \ No newline at end of file diff --git a/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-03.swift b/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-03.swift index 62e77fe68f..d9310a5ab6 100644 --- a/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-03.swift +++ b/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-03.swift @@ -1,32 +1,32 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.2 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "Todos", - platforms: [.macOS(.v14), .iOS(.v17), .tvOS(.v17)], + platforms: [.macOS(.v15), .iOS(.v18), .tvOS(.v18)], products: [ - .executable(name: "App", targets: ["App"]), + .executable(name: "App", targets: ["App"]) ], dependencies: [ .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"), - .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"), + .package(url: "https://github.com/apple/swift-configuration.git", from: "1.0.0", traits: [.defaults, "CommandLineArguments"]), ], targets: [ - .executableTarget( - name: "App", + .executableTarget(name: "App", dependencies: [ - .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "Configuration", package: "swift-configuration"), .product(name: "Hummingbird", package: "hummingbird"), - ] + ], + path: "Sources/App" ), - .testTarget( - name: "AppTests", + .testTarget(name: "AppTests", dependencies: [ .byName(name: "App"), - .product(name: "HummingbirdTesting", package: "hummingbird"), - ] - ), + .product(name: "HummingbirdTesting", package: "hummingbird") + ], + path: "Tests/AppTests" + ) ] ) diff --git a/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-04.swift b/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-04.swift index 945cc80f63..ddf11c6d20 100644 --- a/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-04.swift +++ b/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-04.swift @@ -1,34 +1,34 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.2 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "Todos", - platforms: [.macOS(.v14), .iOS(.v17), .tvOS(.v17)], + platforms: [.macOS(.v15), .iOS(.v18), .tvOS(.v18)], products: [ - .executable(name: "App", targets: ["App"]), + .executable(name: "App", targets: ["App"]) ], dependencies: [ .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"), - .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"), + .package(url: "https://github.com/apple/swift-configuration.git", from: "1.0.0", traits: [.defaults, "CommandLineArguments"]), .package(url: "https://github.com/vapor/postgres-nio.git", from: "1.21.0"), ], targets: [ - .executableTarget( - name: "App", + .executableTarget(name: "App", dependencies: [ - .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "Configuration", package: "swift-configuration"), .product(name: "Hummingbird", package: "hummingbird"), .product(name: "PostgresNIO", package: "postgres-nio"), - ] + ], + path: "Sources/App" ), - .testTarget( - name: "AppTests", + .testTarget(name: "AppTests", dependencies: [ .byName(name: "App"), - .product(name: "HummingbirdTesting", package: "hummingbird"), - ] - ), + .product(name: "HummingbirdTesting", package: "hummingbird") + ], + path: "Tests/AppTests" + ) ] ) diff --git a/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-05.swift b/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-05.swift index 46d38cd51c..a489efe7cc 100644 --- a/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-05.swift +++ b/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-05.swift @@ -1,19 +1,16 @@ -import Hummingbird -import Logging - -/// Application arguments protocol. We use a protocol so we can call -/// `buildApplication` inside Tests as well as in the App executable. -/// Any variables added here also have to be added to `App` in App.swift and -/// `TestArguments` in AppTest.swift -public protocol AppArguments { - var hostname: String { get } - var port: Int { get } - var logLevel: Logger.Level? { get } -} - -// Request context used by application -typealias AppRequestContext = BasicRequestContext - /// Build application -/// - Parameter arguments: application arguments -public func buildApplication(_ arguments: some AppArguments) async throws -> some ApplicationProtocol { +/// - Parameter reader: configuration reader +func buildApplication(reader: ConfigReader) async throws -> some ApplicationProtocol { + let logger = { + var logger = Logger(label: "Todos") + logger.logLevel = reader.string(forKey: "log.level", as: Logger.Level.self, default: .info) + return logger + }() + let router = try buildRouter() + let app = Application( + router: router, + configuration: ApplicationConfiguration(reader: reader.scoped(to: "http")), + logger: logger + ) + return app +} diff --git a/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-06.swift b/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-06.swift deleted file mode 100644 index afe1751add..0000000000 --- a/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-06.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Hummingbird -import Logging - -/// Application arguments protocol. We use a protocol so we can call -/// `buildApplication` inside Tests as well as in the App executable. -/// Any variables added here also have to be added to `App` in App.swift and -/// `TestArguments` in AppTest.swift -public protocol AppArguments { - var hostname: String { get } - var port: Int { get } - var logLevel: Logger.Level? { get } - var inMemoryTesting: Bool { get } -} - -// Request context used by application -typealias AppRequestContext = BasicRequestContext - -/// Build application -/// - Parameter arguments: application arguments -public func buildApplication(_ arguments: some AppArguments) async throws -> some ApplicationProtocol { diff --git a/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-07.swift b/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-07.swift deleted file mode 100644 index e8b4982d37..0000000000 --- a/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-07.swift +++ /dev/null @@ -1,19 +0,0 @@ -@main -struct App: AsyncParsableCommand, AppArguments { - @Option(name: .shortAndLong) - var hostname: String = "127.0.0.1" - - @Option(name: .shortAndLong) - var port: Int = 8080 - - @Option(name: .shortAndLong) - var logLevel: Logger.Level? - - @Flag - var inMemoryTesting: Bool = false - - func run() async throws { - let app = try await buildApplication(self) - try await app.runService() - } -} diff --git a/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-08.swift b/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-08.swift deleted file mode 100644 index d95a26ca79..0000000000 --- a/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-08.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Hummingbird -import HummingbirdTesting -import Logging -import XCTest - -@testable import App - -final class AppTests: XCTestCase { - struct TestArguments: AppArguments { - let hostname = "127.0.0.1" - let port = 8080 - let logLevel: Logger.Level? = nil - let inMemoryTesting = true - } - diff --git a/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-09.swift b/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-09.swift index fb66b53a18..26e99d6823 100644 --- a/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-09.swift +++ b/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-09.swift @@ -1,28 +1,22 @@ /// Build application -/// - Parameter arguments: application arguments -public func buildApplication(_ arguments: some AppArguments) async throws -> some ApplicationProtocol { - let environment = Environment() +/// - Parameter reader: configuration reader +func buildApplication(reader: ConfigReader) async throws -> some ApplicationProtocol { let logger = { - var logger = Logger(label: "todos-postgres-tutorial") - logger.logLevel = - arguments.logLevel ?? - environment.get("LOG_LEVEL").map { Logger.Level(rawValue: $0) ?? .info } ?? - .info + var logger = Logger(label: "Todos") + logger.logLevel = reader.string(forKey: "log.level", as: Logger.Level.self, default: .info) return logger }() - if !arguments.inMemoryTesting { + let inMemoryTesting = reader.bool(forKey: "db.inMemoryTesting", default: false) + if !inMemoryTesting { let client = PostgresClient( configuration: .init(host: "localhost", username: "todos", password: "todos", database: "hummingbird", tls: .disable), backgroundLogger: logger ) } let router = buildRouter() - var app = Application( + let app = Application( router: router, - configuration: .init( - address: .hostname(arguments.hostname, port: arguments.port), - serverName: "todos-postgres-tutorial" - ), + configuration: ApplicationConfiguration(reader: reader.scoped(to: "http")), logger: logger ) return app diff --git a/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-10.swift b/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-10.swift index 85b13d9907..83b28cd3a7 100644 --- a/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-10.swift +++ b/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-10.swift @@ -1,17 +1,14 @@ /// Build application -/// - Parameter arguments: application arguments -public func buildApplication(_ arguments: some AppArguments) async throws -> some ApplicationProtocol { - let environment = Environment() +/// - Parameter reader: configuration reader +func buildApplication(reader: ConfigReader) async throws -> some ApplicationProtocol { let logger = { - var logger = Logger(label: "todos-postgres-tutorial") - logger.logLevel = - arguments.logLevel ?? - environment.get("LOG_LEVEL").map { Logger.Level(rawValue: $0) ?? .info } ?? - .info + var logger = Logger(label: "Todos") + logger.logLevel = reader.string(forKey: "log.level", as: Logger.Level.self, default: .info) return logger }() + let inMemoryTesting = reader.bool(forKey: "db.inMemoryTesting", default: false) var postgresClient: PostgresClient? - if !arguments.inMemoryTesting { + if !inMemoryTesting { let client = PostgresClient( configuration: .init(host: "localhost", username: "todos", password: "todos", database: "hummingbird", tls: .disable), backgroundLogger: logger @@ -21,10 +18,7 @@ public func buildApplication(_ arguments: some AppArguments) async throws -> som let router = buildRouter() var app = Application( router: router, - configuration: .init( - address: .hostname(arguments.hostname, port: arguments.port), - serverName: "todos-postgres-tutorial" - ), + configuration: ApplicationConfiguration(reader: reader.scoped(to: "http")), logger: logger ) if let postgresClient { @@ -33,3 +27,4 @@ public func buildApplication(_ arguments: some AppArguments) async throws -> som return app } + diff --git a/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-14.swift b/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-14.swift index a4232e7a18..39d9fd5db8 100644 --- a/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-14.swift +++ b/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-14.swift @@ -1,18 +1,15 @@ /// Build application -/// - Parameter arguments: application arguments -public func buildApplication(_ arguments: some AppArguments) async throws -> some ApplicationProtocol { - let environment = Environment() +/// - Parameter reader: configuration reader +func buildApplication(reader: ConfigReader) async throws -> some ApplicationProtocol { let logger = { - var logger = Logger(label: "todos-postgres-tutorial") - logger.logLevel = - arguments.logLevel ?? - environment.get("LOG_LEVEL").map { Logger.Level(rawValue: $0) ?? .info } ?? - .info + var logger = Logger(label: "Todos") + logger.logLevel = reader.string(forKey: "log.level", as: Logger.Level.self, default: .info) return logger }() - var postgresRepository: TodoPostgresRepository? + let inMemoryTesting = reader.bool(forKey: "db.inMemoryTesting", default: false) + var postgresClient: PostgresClient? let router: Router - if !arguments.inMemoryTesting { + if !inMemoryTesting { let client = PostgresClient( configuration: .init(host: "localhost", username: "todos", password: "todos", database: "hummingbird", tls: .disable), backgroundLogger: logger @@ -23,12 +20,10 @@ public func buildApplication(_ arguments: some AppArguments) async throws -> som } else { router = buildRouter(TodoMemoryRepository()) } + let router = buildRouter() var app = Application( router: router, - configuration: .init( - address: .hostname(arguments.hostname, port: arguments.port), - serverName: "todos-postgres-tutorial" - ), + configuration: ApplicationConfiguration(reader: reader.scoped(to: "http")), logger: logger ) // if we setup a postgres service then add as a service and run createTable before @@ -41,3 +36,5 @@ public func buildApplication(_ arguments: some AppArguments) async throws -> som } return app } + + diff --git a/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-21.swift b/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-21.swift index d95a26ca79..9cf4dad117 100644 --- a/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-21.swift +++ b/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-21.swift @@ -1,15 +1,16 @@ +import Configuration +import Foundation import Hummingbird import HummingbirdTesting import Logging -import XCTest +import Testing @testable import App -final class AppTests: XCTestCase { - struct TestArguments: AppArguments { - let hostname = "127.0.0.1" - let port = 8080 - let logLevel: Logger.Level? = nil - let inMemoryTesting = true - } - +private let reader = ConfigReader(providers: [ + InMemoryProvider(values: [ + "host": "127.0.0.1", + "port": "0", + "log.level": "trace" + ]) +]) diff --git a/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-22.swift b/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-22.swift index eb47f359e8..2773ab30af 100644 --- a/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-22.swift +++ b/Hummingbird.docc/Tutorials/Todos/Resources/code/Postgres/todos-postgres-22.swift @@ -1,15 +1,17 @@ +import Configuration +import Foundation import Hummingbird import HummingbirdTesting import Logging -import XCTest +import Testing @testable import App -final class AppTests: XCTestCase { - struct TestArguments: AppArguments { - let hostname = "127.0.0.1" - let port = 8080 - let logLevel: Logger.Level? = nil - let inMemoryTesting = false - } - +private let reader = ConfigReader(providers: [ + InMemoryProvider(values: [ + "host": "127.0.0.1", + "port": "0", + "log.level": "trace", + "db.inMemoryTesting": false + ]) +]) diff --git a/Hummingbird.docc/Tutorials/Todos/Resources/code/Template/todos-template-02.sh b/Hummingbird.docc/Tutorials/Todos/Resources/code/Template/todos-template-02.sh index db1d8206c8..b99fa95e27 100644 --- a/Hummingbird.docc/Tutorials/Todos/Resources/code/Template/todos-template-02.sh +++ b/Hummingbird.docc/Tutorials/Todos/Resources/code/Template/todos-template-02.sh @@ -1,4 +1,6 @@ > ./template/configure.sh Todos Enter your package name: [Todos] > +Do you want to build an AWS Lambda function? [y/N] > Enter your executable name: [App] > -Include Visual Studio Code snippets: [Y/n] > +Do you want to use the OpenAPI generator? [y/N] > +Include Visual Studio Code snippets: [y/N] > diff --git a/Hummingbird.docc/Tutorials/Todos/Resources/code/Template/todos-template-03.swift b/Hummingbird.docc/Tutorials/Todos/Resources/code/Template/todos-template-03.swift index 62e77fe68f..7e51c22f18 100644 --- a/Hummingbird.docc/Tutorials/Todos/Resources/code/Template/todos-template-03.swift +++ b/Hummingbird.docc/Tutorials/Todos/Resources/code/Template/todos-template-03.swift @@ -1,32 +1,32 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.2 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "Todos", - platforms: [.macOS(.v14), .iOS(.v17), .tvOS(.v17)], + platforms: [.macOS(.v15), .iOS(.v18), .tvOS(.v18)], products: [ .executable(name: "App", targets: ["App"]), ], dependencies: [ .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"), - .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"), + .package(url: "https://github.com/apple/swift-configuration.git", from: "1.0.0", traits: [.defaults, "CommandLineArguments"]), ], targets: [ - .executableTarget( - name: "App", + .executableTarget(name: "App", dependencies: [ - .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "Configuration", package: "swift-configuration"), .product(name: "Hummingbird", package: "hummingbird"), - ] + ], + path: "Sources/App" ), - .testTarget( - name: "AppTests", + .testTarget(name: "AppTests", dependencies: [ .byName(name: "App"), - .product(name: "HummingbirdTesting", package: "hummingbird"), - ] - ), + .product(name: "HummingbirdTesting", package: "hummingbird") + ], + path: "Tests/AppTests" + ) ] ) diff --git a/Hummingbird.docc/Tutorials/Todos/Resources/code/Template/todos-template-04.swift b/Hummingbird.docc/Tutorials/Todos/Resources/code/Template/todos-template-04.swift index 04f47bb493..2685df8b2d 100644 --- a/Hummingbird.docc/Tutorials/Todos/Resources/code/Template/todos-template-04.swift +++ b/Hummingbird.docc/Tutorials/Todos/Resources/code/Template/todos-template-04.swift @@ -1,27 +1,21 @@ -import ArgumentParser +import Configuration import Hummingbird import Logging @main -struct App: AsyncParsableCommand, AppArguments { - @Option(name: .shortAndLong) - var hostname: String = "127.0.0.1" - - @Option(name: .shortAndLong) - var port: Int = 8080 - - @Option(name: .shortAndLong) - var logLevel: Logger.Level? - - func run() async throws { - let app = try await buildApplication(self) +struct App { + static func main() async throws { + // Application will read configuration from the following in the order listed + // Command line, Environment variables, dotEnv file, defaults provided in memory + let reader = try await ConfigReader(providers: [ + CommandLineArgumentsProvider(), + EnvironmentVariablesProvider(), + EnvironmentVariablesProvider(environmentFilePath: ".env", allowMissing: true), + InMemoryProvider(values: [ + "http.serverName": "Todos" + ]) + ]) + let app = try await buildApplication(reader: reader) try await app.runService() } } - -/// Extend `Logger.Level` so it can be used as an argument -#if hasFeature(RetroactiveAttribute) - extension Logger.Level: @retroactive ExpressibleByArgument {} -#else - extension Logger.Level: ExpressibleByArgument {} -#endif diff --git a/Hummingbird.docc/Tutorials/Todos/Resources/code/Template/todos-template-05.swift b/Hummingbird.docc/Tutorials/Todos/Resources/code/Template/todos-template-05.swift index a40080e4f5..a489efe7cc 100644 --- a/Hummingbird.docc/Tutorials/Todos/Resources/code/Template/todos-template-05.swift +++ b/Hummingbird.docc/Tutorials/Todos/Resources/code/Template/todos-template-05.swift @@ -1,22 +1,15 @@ /// Build application -/// - Parameter arguments: application arguments -public func buildApplication(_ arguments: some AppArguments) async throws -> some ApplicationProtocol { - let environment = Environment() +/// - Parameter reader: configuration reader +func buildApplication(reader: ConfigReader) async throws -> some ApplicationProtocol { let logger = { var logger = Logger(label: "Todos") - logger.logLevel = - arguments.logLevel ?? - environment.get("LOG_LEVEL").map { Logger.Level(rawValue: $0) ?? .info } ?? - .info + logger.logLevel = reader.string(forKey: "log.level", as: Logger.Level.self, default: .info) return logger }() - let router = buildRouter() + let router = try buildRouter() let app = Application( router: router, - configuration: .init( - address: .hostname(arguments.hostname, port: arguments.port), - serverName: "Todos" - ), + configuration: ApplicationConfiguration(reader: reader.scoped(to: "http")), logger: logger ) return app diff --git a/Hummingbird.docc/Tutorials/Todos/Resources/code/Template/todos-template-07.sh b/Hummingbird.docc/Tutorials/Todos/Resources/code/Template/todos-template-07.sh index 25c968b8be..308d57230f 100644 --- a/Hummingbird.docc/Tutorials/Todos/Resources/code/Template/todos-template-07.sh +++ b/Hummingbird.docc/Tutorials/Todos/Resources/code/Template/todos-template-07.sh @@ -1 +1 @@ -> curl -i localhost:8080/health +> curl -i localhost:8080 diff --git a/Hummingbird.docc/Tutorials/Todos/Resources/code/Template/todos-template-08.sh b/Hummingbird.docc/Tutorials/Todos/Resources/code/Template/todos-template-08.sh index a4f4f4dc7e..e2fd21234b 100644 --- a/Hummingbird.docc/Tutorials/Todos/Resources/code/Template/todos-template-08.sh +++ b/Hummingbird.docc/Tutorials/Todos/Resources/code/Template/todos-template-08.sh @@ -1,6 +1,8 @@ -> curl -i localhost:8080/health +> curl -i localhost:8080 HTTP/1.1 200 OK -Content-Length: 0 -Date: Fri, 6 Sep 2024 10:32:02 GMT -Server: Todos +Content-Type: text/plain; charset=utf-8 +Content-Length: 6 +Date: Mon, 05 Jan 2026 11:29:04 GMT +Server: template +Hello! diff --git a/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-01.swift b/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-01.swift index 62e77fe68f..7e51c22f18 100644 --- a/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-01.swift +++ b/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-01.swift @@ -1,32 +1,32 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.2 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "Todos", - platforms: [.macOS(.v14), .iOS(.v17), .tvOS(.v17)], + platforms: [.macOS(.v15), .iOS(.v18), .tvOS(.v18)], products: [ .executable(name: "App", targets: ["App"]), ], dependencies: [ .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"), - .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"), + .package(url: "https://github.com/apple/swift-configuration.git", from: "1.0.0", traits: [.defaults, "CommandLineArguments"]), ], targets: [ - .executableTarget( - name: "App", + .executableTarget(name: "App", dependencies: [ - .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "Configuration", package: "swift-configuration"), .product(name: "Hummingbird", package: "hummingbird"), - ] + ], + path: "Sources/App" ), - .testTarget( - name: "AppTests", + .testTarget(name: "AppTests", dependencies: [ .byName(name: "App"), - .product(name: "HummingbirdTesting", package: "hummingbird"), - ] - ), + .product(name: "HummingbirdTesting", package: "hummingbird") + ], + path: "Tests/AppTests" + ) ] ) diff --git a/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-02.swift b/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-02.swift index b701701ec6..a4a176a780 100644 --- a/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-02.swift +++ b/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-02.swift @@ -1,23 +1,27 @@ +import Configuration import Hummingbird import HummingbirdTesting import Logging -import XCTest +import Testing @testable import App -final class AppTests: XCTestCase { - struct TestArguments: AppArguments { - let hostname = "127.0.0.1" - let port = 0 - let logLevel: Logger.Level? = .trace - } +private let reader = ConfigReader(providers: [ + InMemoryProvider(values: [ + "host": "127.0.0.1", + "port": "0", + "log.level": "trace" + ]) +]) - func testApp() async throws { - let args = TestArguments() - let app = try await buildApplication(args) +@Suite +struct AppTests { + @Test + func app() async throws { + let app = try await buildApplication(reader: reader) try await app.test(.router) { client in try await client.execute(uri: "/", method: .get) { response in - XCTAssertEqual(response.body, ByteBuffer(string: "Hello!")) + #expect(response.body == ByteBuffer(string: "Hello!")) } } } diff --git a/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-03.swift b/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-03.swift deleted file mode 100644 index f8a5cce6bf..0000000000 --- a/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-03.swift +++ /dev/null @@ -1,10 +0,0 @@ -/// Application arguments protocol. -public protocol AppArguments { - var hostname: String { get } - var port: Int { get } - var logLevel: Logger.Level? { get } -} - -/// Build application -/// - Parameter arguments: application arguments -public func buildApplication(_ arguments: some AppArguments) async throws -> some ApplicationProtocol { diff --git a/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-08.swift b/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-08.swift index 2879c328ce..ca93c6eecd 100644 --- a/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-08.swift +++ b/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-08.swift @@ -1,25 +1,12 @@ -import Foundation -import Hummingbird -import HummingbirdTesting -import Logging -import XCTest - -@testable import App - -final class AppTests: XCTestCase { - struct TestArguments: AppArguments { - let hostname = "127.0.0.1" - let port = 0 - let logLevel: Logger.Level? = .trace - } - - func testCreate() async throws { - let app = try await buildApplication(TestArguments()) +@Suite +struct AppTests { + @Test func testCreate() async throws { + let app = try await buildApplication(reader: reader) try await app.test(.router) { client in try await client.execute(uri: "/todos", method: .post, body: ByteBuffer(string: #"{"title":"My first todo"}"#)) { response in - XCTAssertEqual(response.status, .created) + #expect(response.status == .created) let todo = try JSONDecoder().decode(Todo.self, from: response.body) - XCTAssertEqual(todo.title, "My first todo") + #expect(todo.title == "My first todo") } } } diff --git a/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-09.swift b/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-09.swift index b8b541ad76..8cff1cdbd0 100644 --- a/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-09.swift +++ b/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-09.swift @@ -1,39 +1,24 @@ -import Foundation -import Hummingbird -import HummingbirdTesting -import Logging -import XCTest - -@testable import App - -final class AppTests: XCTestCase { - struct TestArguments: AppArguments { - let hostname = "127.0.0.1" - let port = 0 - let logLevel: Logger.Level? = .trace - } - +@Suite +struct AppTests { struct CreateRequest: Encodable { let title: String let order: Int? } - static func create(title: String, order: Int? = nil, client: some TestClientProtocol) async throws -> Todo { + func create(title: String, order: Int? = nil, client: some TestClientProtocol) async throws -> Todo { let request = CreateRequest(title: title, order: order) let buffer = try JSONEncoder().encodeAsByteBuffer(request, allocator: ByteBufferAllocator()) return try await client.execute(uri: "/todos", method: .post, body: buffer) { response in - XCTAssertEqual(response.status, .created) + #expect(response.status == .created) return try JSONDecoder().decode(Todo.self, from: response.body) } } - // MARK: Tests - - func testCreate() async throws { - let app = try await buildApplication(TestArguments()) + @Test func testCreate() async throws { + let app = try await buildApplication(reader: reader) try await app.test(.router) { client in let todo = try await Self.create(title: "My first todo", client: client) - XCTAssertEqual(todo.title, "My first todo") + #expect(todo.title == "My first todo") } } } \ No newline at end of file diff --git a/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-10.swift b/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-10.swift index f39a2fb7b1..5a7c7251a1 100644 --- a/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-10.swift +++ b/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-10.swift @@ -1,36 +1,23 @@ -import Foundation -import Hummingbird -import HummingbirdTesting -import Logging -import XCTest - -@testable import App - -final class AppTests: XCTestCase { - struct TestArguments: AppArguments { - let hostname = "127.0.0.1" - let port = 0 - let logLevel: Logger.Level? = .trace - } - +@Suite +struct AppTests { struct CreateRequest: Encodable { let title: String let order: Int? } - static func create(title: String, order: Int? = nil, client: some TestClientProtocol) async throws -> Todo { + func create(title: String, order: Int? = nil, client: some TestClientProtocol) async throws -> Todo { let request = CreateRequest(title: title, order: order) let buffer = try JSONEncoder().encodeAsByteBuffer(request, allocator: ByteBufferAllocator()) return try await client.execute(uri: "/todos", method: .post, body: buffer) { response in - XCTAssertEqual(response.status, .created) + #expect(response.status == .created) return try JSONDecoder().decode(Todo.self, from: response.body) } } - static func get(id: UUID, client: some TestClientProtocol) async throws -> Todo? { + func get(id: UUID, client: some TestClientProtocol) async throws -> Todo? { try await client.execute(uri: "/todos/\(id)", method: .get) { response in // either the get request returned an 200 status or it didn't return a Todo - XCTAssert(response.status == .ok || response.body.readableBytes == 0) + #expect(response.status == .ok || response.body.readableBytes == 0) if response.body.readableBytes > 0 { return try JSONDecoder().decode(Todo.self, from: response.body) } else { @@ -39,9 +26,9 @@ final class AppTests: XCTestCase { } } - static func list(client: some TestClientProtocol) async throws -> [Todo] { + func list(client: some TestClientProtocol) async throws -> [Todo] { try await client.execute(uri: "/todos", method: .get) { response in - XCTAssertEqual(response.status, .ok) + #expect(response.status == .ok) return try JSONDecoder().decode([Todo].self, from: response.body) } } @@ -52,11 +39,17 @@ final class AppTests: XCTestCase { let completed: Bool? } - static func patch(id: UUID, title: String? = nil, order: Int? = nil, completed: Bool? = nil, client: some TestClientProtocol) async throws -> Todo? { + func patch( + id: UUID, + title: String? = nil, + order: Int? = nil, + completed: Bool? = nil, + client: some TestClientProtocol + ) async throws -> Todo? { let request = UpdateRequest(title: title, order: order, completed: completed) let buffer = try JSONEncoder().encodeAsByteBuffer(request, allocator: ByteBufferAllocator()) return try await client.execute(uri: "/todos/\(id)", method: .patch, body: buffer) { response in - XCTAssertEqual(response.status, .ok) + #expect(response.status == .ok) if response.body.readableBytes > 0 { return try JSONDecoder().decode(Todo.self, from: response.body) } else { @@ -65,13 +58,13 @@ final class AppTests: XCTestCase { } } - static func delete(id: UUID, client: some TestClientProtocol) async throws -> HTTPResponse.Status { + func delete(id: UUID, client: some TestClientProtocol) async throws -> HTTPResponse.Status { try await client.execute(uri: "/todos/\(id)", method: .delete) { response in response.status } } - static func deleteAll(client: some TestClientProtocol) async throws { + func deleteAll(client: some TestClientProtocol) async throws { try await client.execute(uri: "/todos", method: .delete) { _ in } } @@ -81,7 +74,7 @@ final class AppTests: XCTestCase { let app = try await buildApplication(TestArguments()) try await app.test(.router) { client in let todo = try await Self.create(title: "My first todo", client: client) - XCTAssertEqual(todo.title, "My first todo") + #expect(todo.title == "My first todo") } } } diff --git a/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-11.swift b/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-11.swift index 170073807b..5c8cc1e2eb 100644 --- a/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-11.swift +++ b/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-11.swift @@ -1,30 +1,22 @@ -import Foundation -import Hummingbird -import HummingbirdTesting -import Logging -import XCTest - -@testable import App - extension AppTests { - func testPatch() async throws { - let app = try await buildApplication(TestArguments()) + @Test func testPatch() async throws { + let app = try await buildApplication(reader: reader) try await app.test(.router) { client in // create todo - let todo = try await Self.create(title: "Deliver parcels to James", client: client) + let todo = try await self.create(title: "Deliver parcels to James", client: client) // rename it - _ = try await Self.patch(id: todo.id, title: "Deliver parcels to Claire", client: client) - let editedTodo = try await Self.get(id: todo.id, client: client) - XCTAssertEqual(editedTodo?.title, "Deliver parcels to Claire") + _ = try await self.patch(id: todo.id, title: "Deliver parcels to Claire", client: client) + let editedTodo = try await self.get(id: todo.id, client: client) + #expect(editedTodo?.title == "Deliver parcels to Claire") // set it to completed - _ = try await Self.patch(id: todo.id, completed: true, client: client) - let editedTodo2 = try await Self.get(id: todo.id, client: client) - XCTAssertEqual(editedTodo2?.completed, true) + _ = try await self.patch(id: todo.id, completed: true, client: client) + let editedTodo2 = try await self.get(id: todo.id, client: client) + #expect(editedTodo2?.completed == true) // revert it - _ = try await Self.patch(id: todo.id, title: "Deliver parcels to James", completed: false, client: client) - let editedTodo3 = try await Self.get(id: todo.id, client: client) - XCTAssertEqual(editedTodo3?.title, "Deliver parcels to James") - XCTAssertEqual(editedTodo3?.completed, false) + _ = try await self.patch(id: todo.id, title: "Deliver parcels to James", completed: false, client: client) + let editedTodo3 = try await self.get(id: todo.id, client: client) + #expect(editedTodo3?.title == "Deliver parcels to James") + #expect(editedTodo3?.completed == false) } } } diff --git a/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-12.swift b/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-12.swift index 2bae183809..3dc9e78e7f 100644 --- a/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-12.swift +++ b/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-12.swift @@ -1,13 +1,5 @@ -import Foundation -import Hummingbird -import HummingbirdTesting -import Logging -import XCTest - -@testable import App - extension AppTests { - func testAPI() async throws { + @Test func testAPI() async throws { let app = try await buildApplication(TestArguments()) try await app.test(.router) { client in // create two todos diff --git a/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-13.swift b/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-13.swift index 72a668b8ce..6ba43680a4 100644 --- a/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-13.swift +++ b/Hummingbird.docc/Tutorials/Todos/Resources/code/Testing/todos-testing-13.swift @@ -1,14 +1,6 @@ -import Foundation -import Hummingbird -import HummingbirdTesting -import Logging -import XCTest - -@testable import App - extension AppTests { - func testDeletingTodoTwiceReturnsBadRequest() async throws {} - func testGettingTodoWithInvalidUUIDReturnsBadRequest() async throws {} - func test30ConcurrentlyCreatedTodosAreAllCreated() async throws {} - func testUpdatingNonExistentTodoReturnsBadRequest() async throws {} + @Test func testDeletingTodoTwiceReturnsBadRequest() async throws {} + @Test func testGettingTodoWithInvalidUUIDReturnsBadRequest() async throws {} + @Test func test30ConcurrentlyCreatedTodosAreAllCreated() async throws {} + @Test func testUpdatingNonExistentTodoReturnsBadRequest() async throws {} } \ No newline at end of file diff --git a/Hummingbird.docc/Tutorials/Todos/Todos-1-Template.tutorial b/Hummingbird.docc/Tutorials/Todos/Todos-1-Template.tutorial index 83cb1dfe3e..351072e370 100644 --- a/Hummingbird.docc/Tutorials/Todos/Todos-1-Template.tutorial +++ b/Hummingbird.docc/Tutorials/Todos/Todos-1-Template.tutorial @@ -22,25 +22,25 @@ @Step { Now lets review what the template has setup. Open `Package.swift`. - You can see if has dependencies for Hummingbird and the Apple's Argument Parser library. + You can see if has dependencies for Hummingbird and the Apple's Configuration library. @Code(name: "Package.swift", file: todos-template-03.swift) } @Step { Open `Sources/App/App.swift` - This contains an `App` type conforming to `AsyncParsableCommand` with three options, the `hostname` and `port` are used to define the server bind address, `logLevel` sets the level of logging required. Finally the `run()` function which calls `buildApplication(_:)` to create an `Application` and then runs it using `runService()`. You can find out more about the argument parser library [here](https://apple.github.io/swift-argument-parser/documentation/argumentparser). + This contains an `App` tagged with `@main`. The `main` function in `App` sets up a `ConfigReader` to read configuration values from the command line arguments, environment variables, a `.env` file and in-memory values in that order. Then it calls `buildApplication(reader:)` with the `ConfigReader` to create an `Application` and runs it using `runService()`. You can find out more about the Configuration library [here](https://swiftpackageindex.com/apple/swift-configuration/1.0.0/documentation/configuration). @Code(name: "Sources/App/App.swift", file: todos-template-04.swift) } @Step { Open `Sources/App/Application+build.swift` to find the `buildApplication(_:) function. - Here we create a `Logger` with log level set by either the command line argument mentioned above, or the environment variable `LOG_LEVEL`. We then call a function `buildRouter()` and use the result of that to create our `Application`. + Here we create a `Logger` with log level extracted from the configuration reader mentioned above. We then call a function `buildRouter()` and use the result of that to create our `Application`. The application uses configuration values `HTTP_HOST` and `HTTP_PORT` to define its bind address. @Code(name: "buildApplication() - Sources/App/Application+build.swift", file: todos-template-05.swift) } @Step { If we look further down the file we can find the `buildRouter()` function. - Here we create the `Router`. We add a logging middleware to it (this logs all requests to the router). The function uses a result builder to create a stack of middleware, but you can also use `Router.add(middleware:)` to add individual middleware. Finally we add a single endpoint GET `/` which returns a String response: "Hello". + Here we create the `Router`. We add a logging middleware to it (this logs all requests to the router). The function uses a result builder to create a stack of middleware, but you can also use `Router.add(middleware:)` to add individual middleware. Finally we add a single endpoint GET `/` which returns a String response: "Hello!". @Code(name: "buildRouter() - Sources/App/Application+build.swift", file: todos-template-06.swift) } @Step { diff --git a/Hummingbird.docc/Tutorials/Todos/Todos-2-API.tutorial b/Hummingbird.docc/Tutorials/Todos/Todos-2-API.tutorial index 0f125167b6..402bfa7020 100644 --- a/Hummingbird.docc/Tutorials/Todos/Todos-2-API.tutorial +++ b/Hummingbird.docc/Tutorials/Todos/Todos-2-API.tutorial @@ -1,4 +1,4 @@ -@Tutorial(time: 20) { +@Tutorial(time: 15) { @Intro(title: "Add your application API") { Add some functionality to your application. @Image(source: "hummingbird.png", alt: "Hummingbird logo") @@ -21,7 +21,7 @@ } @Step { And add the TodoController endpoints to your router. The Todos API has a URI prefix of "todos". - @Code(name: "Sources/App/Application+build.swift", file: todos-api-02.swift) + @Code(name: "Sources/App/Application+build.swift", file: todos-api-02.swift, previousFile: todos-template-06.swift) } @Step { We are going to use the [repository design pattern](https://www.geeksforgeeks.org/repository-design-pattern/) to separate our storage concerns from our API. With this we should be able to create an API and test it without worrying about Database setup. @@ -48,7 +48,7 @@ } @Step { And add a generic repository member variable conforming to `TodoRepository` to be used by the `TodoController` routes. Generics allow us to use the same controller for different repository implementations. - @Code(name: "Sources/App/Controllers/TodoController.swift", file: todos-api-07.swift) + @Code(name: "Sources/App/Controllers/TodoController.swift", file: todos-api-07.swift, previousFile: todos-api-01.swift) } @Step { Go to `buildRouter()` in Application+build.swift @@ -64,7 +64,7 @@ } @Step { Our first endpoint is to return a Todo given an id in the URI. We extract the id from the URI, attempt to convert it to a UUID and then call the repository method `get` and return the result. The result is then converted to a response using the response encoder (JSONEncoder by default) attached to the context. - @Code(name: "Sources/App/Controllers/TodoController.swift", file: todos-api-09.swift) + @Code(name: "Sources/App/Controllers/TodoController.swift", file: todos-api-09.swift, previousFile: todos-api-07.swift) } @Step { This endpoint has a few other features. If it fails to convert the id to a UUID then it throws an `HTTPError`. This is an error that can be converted by the server to a valid HTTP response. If the server receives an error it cannot convert to an HTTP response it will return a 500 (Internal Server Error) HTTP error to the client. @@ -78,7 +78,7 @@ } @Step { Returning an object and not a raw Response, in general sets the response status to 200 (OK). In this situation we want to return a 201 (Created) status. We can do this by returning an `EditedResponse` which can be used to edit the status code and headers of a generated response. - @Code(name: "Sources/App/Controllers/TodoController.swift", file: todos-api-11.swift) + @Code(name: "Sources/App/Controllers/TodoController.swift", file: todos-api-11.swift, previousFile: todos-api-10.swift) } @Step { We now have an API we can test. Lets use curl to create a Todo. If we include the command line parameter `-i` we get the full HTTP response and can see that the status code is 201 (Created). diff --git a/Hummingbird.docc/Tutorials/Todos/Todos-3-Testing.tutorial b/Hummingbird.docc/Tutorials/Todos/Todos-3-Testing.tutorial index 08680fdf12..9cb0eb763d 100644 --- a/Hummingbird.docc/Tutorials/Todos/Todos-3-Testing.tutorial +++ b/Hummingbird.docc/Tutorials/Todos/Todos-3-Testing.tutorial @@ -1,4 +1,4 @@ -@Tutorial(time: 15) { +@Tutorial(time: 10) { @Intro(title: "Testing your application") { Test your application using the HummingbirdTesting framework @Image(source: "hummingbird.png", alt: "Hummingbird logo") @@ -19,13 +19,9 @@ @Step { Open Tests/AppTests/AppTests.swift - It contains one test, `testApp()`. This creates a copy of the Application using `buildApplication(_:)` and uses the Hummingbird test framework to verify the GET `/` endpoint returns a "Hello!" string. + It contains one test, `app()`. This creates a copy of the Application using `buildApplication(reader:)`. The application is configured using a custom test `ConfigReader` with default values for testing. We verify the GET `/` endpoint returns a "Hello!" string. @Code(name: "Tests/AppTests/AppTests.swift", file: todos-testing-02.swift) } - @Step { - We cannot create an instance of `App`, so need another way of passing the arguments to the `buildApplication` function in our tests. So `buildApplication(_:)` doesn't take `App` as a parameter. Instead its parameter is a type that conforms to the protocol `AppArguments` which includes the parameters the function needs. We then conform `App` to `AppArguments` and in our tests create a new type `TestArguments` which conforms to the protocol `AppArguments`. - @Code(name: "Sources/App/Application+build.swift", file: todos-testing-03.swift) - } } } @Section(title: "Test your application") { @@ -35,7 +31,7 @@ } @Steps { @Step { - Lets replace the `testApp` function with a test for the create todo function. Application testing is done with the function ``Hummingbird/ApplicationProtocol/test(_:_:)``. The first parameter indicates what test framework you want to use. Here we are using `.router` which sends our request directly to the router without a live server process. + Lets replace the `test` function with a test for the create todo function. Application testing is done with the function ``Hummingbird/ApplicationProtocol/test(_:_:)``. The first parameter indicates what test framework you want to use. Here we are using `.router` which sends our request directly to the router without a live server process. In the closure passed to `test` you are provided with a client to interact with the current test framework. With this you can send requests and verify the contents of their responses. @Code(name: "Tests/AppTests/AppTests.swift", file: todos-testing-08.swift) diff --git a/Hummingbird.docc/Tutorials/Todos/Todos-4-Postgres.tutorial b/Hummingbird.docc/Tutorials/Todos/Todos-4-Postgres.tutorial index 75b8d7994f..eaf82209b6 100644 --- a/Hummingbird.docc/Tutorials/Todos/Todos-4-Postgres.tutorial +++ b/Hummingbird.docc/Tutorials/Todos/Todos-4-Postgres.tutorial @@ -35,24 +35,12 @@ @Code(name: "Sources/App/Application+build.swift", file: todos-postgres-05.swift) } @Step { - we add a new requirement `inMemoryTesting` to `AppArguments`. This will decide whether we store Todos in memory or a Postgres database. - @Code(name: "Sources/App/Application+build.swift", file: todos-postgres-06.swift) - } - @Step { - We then need to add implementations of this requirement in Sources/App/App.swift - @Code(name: "Sources/App/App.swift", file: todos-postgres-07.swift) - } - @Step { - and Tests/AppTests/AppTests.swift - @Code(name: "Tests/AppTests/AppTests.swift", file: todos-postgres-08.swift) - } - @Step { - We are going to use `PostgresClient` from PostgresNIO for our Postgres support. The `inMemoryTesting` flag is used to decide on whether we should set one up. Note the Postgres configuration details are the same as the Postgres role we set up earlier in psql. + We are going to use `PostgresClient` from PostgresNIO for our Postgres support. We use the configuration value `db.inMemoryTesting` flag to decide on whether we should use the client. Note the Postgres configuration details are the same as the Postgres role we set up earlier in psql. @Code(name: "Sources/App/Application+build.swift", file: todos-postgres-09.swift) } @Step { `PostgresClient` sets up background processes that requires lifecycle management. You can add a service to `Application` to have its lifecycle managed as long as it conforms to `Service`. This is done by adding it to an internally held `ServiceGroup`. More details on `Service` and `ServiceGroup` can be found in the documentation for [Swift Service Lifecycle](https://swiftpackageindex.com/swift-server/swift-service-lifecycle/main/documentation/servicelifecycle). - @Code(name: "Sources/App/Application+build.swift", file: todos-postgres-10.swift) + @Code(name: "Sources/App/Application+build.swift", file: todos-postgres-10.swift, previousFile: todos-postgres-09.swift) } } } @@ -117,7 +105,7 @@ @Code(name: "Tests/AppTests/AppTests.swift", file: todos-postgres-21.swift) } @Step { - You can switch the `inMemoryTesting` boolean to false to test your Postgres solution. + You can add a `db.inMemoryTesting` configuration values to switch between testing your Postgres solution and the in memory solution. @Code(name: "Tests/AppTests/AppTests.swift", file: todos-postgres-22.swift) } @Step { From 932820d0a3f1b1f7039f19f619b4cc5520ed2a82 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Mon, 5 Jan 2026 19:23:10 +0100 Subject: [PATCH 2/2] App+build --- .../Tutorials/Todos/Todos-1-Template.tutorial | 6 +++--- .../Tutorials/Todos/Todos-2-API.tutorial | 12 ++++++------ .../Tutorials/Todos/Todos-4-Postgres.tutorial | 16 ++++++++-------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Hummingbird.docc/Tutorials/Todos/Todos-1-Template.tutorial b/Hummingbird.docc/Tutorials/Todos/Todos-1-Template.tutorial index 351072e370..73d0ae7ae1 100644 --- a/Hummingbird.docc/Tutorials/Todos/Todos-1-Template.tutorial +++ b/Hummingbird.docc/Tutorials/Todos/Todos-1-Template.tutorial @@ -32,16 +32,16 @@ @Code(name: "Sources/App/App.swift", file: todos-template-04.swift) } @Step { - Open `Sources/App/Application+build.swift` to find the `buildApplication(_:) function. + Open `Sources/App/App+build.swift` to find the `buildApplication(_:) function. Here we create a `Logger` with log level extracted from the configuration reader mentioned above. We then call a function `buildRouter()` and use the result of that to create our `Application`. The application uses configuration values `HTTP_HOST` and `HTTP_PORT` to define its bind address. - @Code(name: "buildApplication() - Sources/App/Application+build.swift", file: todos-template-05.swift) + @Code(name: "buildApplication() - Sources/App/App+build.swift", file: todos-template-05.swift) } @Step { If we look further down the file we can find the `buildRouter()` function. Here we create the `Router`. We add a logging middleware to it (this logs all requests to the router). The function uses a result builder to create a stack of middleware, but you can also use `Router.add(middleware:)` to add individual middleware. Finally we add a single endpoint GET `/` which returns a String response: "Hello!". - @Code(name: "buildRouter() - Sources/App/Application+build.swift", file: todos-template-06.swift) + @Code(name: "buildRouter() - Sources/App/App+build.swift", file: todos-template-06.swift) } @Step { We can run this application and use curl to test it works. diff --git a/Hummingbird.docc/Tutorials/Todos/Todos-2-API.tutorial b/Hummingbird.docc/Tutorials/Todos/Todos-2-API.tutorial index 402bfa7020..96da9ffab0 100644 --- a/Hummingbird.docc/Tutorials/Todos/Todos-2-API.tutorial +++ b/Hummingbird.docc/Tutorials/Todos/Todos-2-API.tutorial @@ -16,12 +16,12 @@ @Code(name: "Sources/App/Controllers/TodoController.swift", file: todos-api-01.swift) } @Step { - Go back to `buildRouter()` in Application+build.swift. Routers ensure a Request is _routed_ to the correct handler function. - @Code(name: "Sources/App/Application+build.swift", file: todos-template-06.swift) + Go back to `buildRouter()` in App+build.swift. Routers ensure a Request is _routed_ to the correct handler function. + @Code(name: "Sources/App/App+build.swift", file: todos-template-06.swift) } @Step { And add the TodoController endpoints to your router. The Todos API has a URI prefix of "todos". - @Code(name: "Sources/App/Application+build.swift", file: todos-api-02.swift, previousFile: todos-template-06.swift) + @Code(name: "Sources/App/App+build.swift", file: todos-api-02.swift, previousFile: todos-template-06.swift) } @Step { We are going to use the [repository design pattern](https://www.geeksforgeeks.org/repository-design-pattern/) to separate our storage concerns from our API. With this we should be able to create an API and test it without worrying about Database setup. @@ -51,12 +51,12 @@ @Code(name: "Sources/App/Controllers/TodoController.swift", file: todos-api-07.swift, previousFile: todos-api-01.swift) } @Step { - Go to `buildRouter()` in Application+build.swift - @Code(name: "Sources/App/Application+build.swift", file: todos-api-02.swift) + Go to `buildRouter()` in App+build.swift + @Code(name: "Sources/App/App+build.swift", file: todos-api-02.swift) } @Step { And add the repository parameter to the TodoController initializer. We are using the memory implementation of the `TodoRepository` we have already implemented previously. - @Code(name: "Sources/App/Application+build.swift", file: todos-api-08.swift) + @Code(name: "Sources/App/App+build.swift", file: todos-api-08.swift) } @Step { Return to TodoController.swift. We can now start adding our endpoints. An endpoint (or Route) is a function that replies to a request if the path and method match. diff --git a/Hummingbird.docc/Tutorials/Todos/Todos-4-Postgres.tutorial b/Hummingbird.docc/Tutorials/Todos/Todos-4-Postgres.tutorial index eaf82209b6..ee05099bdc 100644 --- a/Hummingbird.docc/Tutorials/Todos/Todos-4-Postgres.tutorial +++ b/Hummingbird.docc/Tutorials/Todos/Todos-4-Postgres.tutorial @@ -31,16 +31,16 @@ @Code(name: "Package.swift", file: todos-postgres-04.swift) } @Step { - In Sources/App/Application+build.swift... - @Code(name: "Sources/App/Application+build.swift", file: todos-postgres-05.swift) + In Sources/App/App+build.swift... + @Code(name: "Sources/App/App+build.swift", file: todos-postgres-05.swift) } @Step { We are going to use `PostgresClient` from PostgresNIO for our Postgres support. We use the configuration value `db.inMemoryTesting` flag to decide on whether we should use the client. Note the Postgres configuration details are the same as the Postgres role we set up earlier in psql. - @Code(name: "Sources/App/Application+build.swift", file: todos-postgres-09.swift) + @Code(name: "Sources/App/App+build.swift", file: todos-postgres-09.swift) } @Step { `PostgresClient` sets up background processes that requires lifecycle management. You can add a service to `Application` to have its lifecycle managed as long as it conforms to `Service`. This is done by adding it to an internally held `ServiceGroup`. More details on `Service` and `ServiceGroup` can be found in the documentation for [Swift Service Lifecycle](https://swiftpackageindex.com/swift-server/swift-service-lifecycle/main/documentation/servicelifecycle). - @Code(name: "Sources/App/Application+build.swift", file: todos-postgres-10.swift, previousFile: todos-postgres-09.swift) + @Code(name: "Sources/App/App+build.swift", file: todos-postgres-10.swift, previousFile: todos-postgres-09.swift) } } } @@ -63,16 +63,16 @@ @Code(name: "Sources/App/Repositories/TodoPostgresRepository.swift", file: todos-postgres-12.swift) } @Step { - Return to `buildApplication(_:)` in Application+build.swift... - @Code(name: "Sources/App/Application+build.swift", file: todos-postgres-10.swift) + Return to `buildApplication(_:)` in App+build.swift... + @Code(name: "Sources/App/App+build.swift", file: todos-postgres-10.swift) } @Step { Use the newly created `TodoPostgresRepository` and once the `PostgresClient` is running call `createTable`. - @Code(name: "Sources/App/Application+build.swift", file: todos-postgres-14.swift) + @Code(name: "Sources/App/App+build.swift", file: todos-postgres-14.swift) } @Step { Update `buildRouter(_:)` to take the repository as an argument and pass it to the controller. - @Code(name: "Sources/App/Application+build.swift", file: todos-postgres-23.swift) + @Code(name: "Sources/App/App+build.swift", file: todos-postgres-23.swift) } @Step { Back to TodoPostgresRepository.swift to start implementing our repository methods.