diff --git a/Library/Resources/Files/Sources/App/App b/Library/Resources/Files/Sources/App/App new file mode 100644 index 0000000..9183ae7 --- /dev/null +++ b/Library/Resources/Files/Sources/App/App @@ -0,0 +1,20 @@ +import AppLibrary +import ArgumentParser + +@main +struct App: AsyncParsableCommand { + + // MARK: Properties + + @OptionGroup var options: Options + + // MARK: Functions + + mutating func run() async throws { + let builder = AppBuilder(name: "App") + let app = try await builder(options) + + try await app.runService() + } + +} diff --git a/Library/Resources/Files/Sources/App/AppOptions b/Library/Resources/Files/Sources/App/AppOptions new file mode 100644 index 0000000..a6d835f --- /dev/null +++ b/Library/Resources/Files/Sources/App/AppOptions @@ -0,0 +1,20 @@ +import AppLibrary +import ArgumentParser +import Logging + +extension App { + struct Options: AppArguments, ParsableArguments { + + // MARK: Properties + + @Option(name: .shortAndLong) + var hostname: String = "127.0.0.1" + + @Option(name: .shortAndLong) + var port: Int = 8080 + + @Option(name: .shortAndLong) + var logLevel: Logger.Level? + + } +} diff --git a/Library/Resources/Files/Sources/Library/AppArguments b/Library/Resources/Files/Sources/Library/AppArguments new file mode 100644 index 0000000..40039e1 --- /dev/null +++ b/Library/Resources/Files/Sources/Library/AppArguments @@ -0,0 +1,11 @@ +import Logging + +public protocol AppArguments { + + // MARK: Properties + + var hostname: String { get } + var logLevel: Logger.Level? { get } + var port: Int { get } + +} diff --git a/Library/Resources/Files/Sources/Library/AppBuilder b/Library/Resources/Files/Sources/Library/AppBuilder new file mode 100644 index 0000000..1998089 --- /dev/null +++ b/Library/Resources/Files/Sources/Library/AppBuilder @@ -0,0 +1,69 @@ +import Hummingbird +import Logging + +public struct AppBuilder { + + // MARK: Properties + + private let environment: Environment + private let name: String + + // MARK: Initialisers + + public init(name: String) { + self.environment = Environment() + self.name = name + } + + // MARK: Functions + + public func callAsFunction( + _ arguments: some AppArguments + ) async throws -> some ApplicationProtocol { + let logger = { + var logger = Logger(label: name) + + logger.logLevel = arguments.logLevel + ?? environment.logLevel.flatMap { Logger.Level(rawValue: $0) ?? .info } + ?? .info + + return logger + }() + + let router = router(logger: logger) + + return Application( + router: router, + configuration: .init( + address: .hostname(arguments.hostname, port: arguments.port), + serverName: name + ), + logger: logger + ) + } + +} + +// MARK: - Helpers + +private extension AppBuilder { + + // MARK: Type aliases + + typealias AppRequestContext = BasicRequestContext + + // MARK: Functions + + func router(logger: Logger) -> Router { + let router = Router() + + router.add(middleware: LogRequestsMiddleware(logger.logLevel)) + + router.get("/") { _,_ in + "" + } + + return router + } + +} diff --git a/Library/Resources/Files/Sources/Library/Environment b/Library/Resources/Files/Sources/Library/Environment new file mode 100644 index 0000000..99d4d41 --- /dev/null +++ b/Library/Resources/Files/Sources/Library/Environment @@ -0,0 +1,11 @@ +import Hummingbird + +extension Environment { + + // MARK: Computed + + public var logLevel: String? { + self.get("LOG_LEVEL") + } + +} diff --git a/Library/Resources/Files/Sources/Library/LoggerLevel b/Library/Resources/Files/Sources/Library/LoggerLevel new file mode 100644 index 0000000..0d1abb6 --- /dev/null +++ b/Library/Resources/Files/Sources/Library/LoggerLevel @@ -0,0 +1,9 @@ +import ArgumentParser +import Logging + +/// 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/Library/Resources/Files/Sources/Package b/Library/Resources/Files/Sources/Package index 9d28cb6..0c804dc 100644 --- a/Library/Resources/Files/Sources/Package +++ b/Library/Resources/Files/Sources/Package @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "App", platforms: [ - .macOS(.v10_15) + .macOS(.v14) ], products: [ .executable(name: "App", targets: ["App"]), diff --git a/Library/Resources/Files/Sources/Test/AppTests b/Library/Resources/Files/Sources/Test/AppTests new file mode 100644 index 0000000..31f1868 --- /dev/null +++ b/Library/Resources/Files/Sources/Test/AppTests @@ -0,0 +1,33 @@ +import AppLibrary +import Hummingbird +import HummingbirdTesting +import Testing + +struct AppTests { + + // MARK: Properties + + private let arguments = TestArguments() + private let builder = AppBuilder(name: "App") + + // MARK: Route tests + + @Test(arguments: ["/"]) + func routes(_ uri: String) async throws { + let app = try await builder(arguments) + + try await app.test(.router) { client in + try await client.execute(uri: uri, method: .get) { response in + #expect(response.status == .ok) + #expect(response.body == .empty) + } + } + } + +} + +// MARK: ByteBuffer+Constants + +private extension ByteBuffer { + static let empty = ByteBuffer(string: "") +} diff --git a/Library/Resources/Files/Sources/Test/TestArguments b/Library/Resources/Files/Sources/Test/TestArguments new file mode 100644 index 0000000..c2f0b21 --- /dev/null +++ b/Library/Resources/Files/Sources/Test/TestArguments @@ -0,0 +1,12 @@ +import AppLibrary +import Logging + +struct TestArguments: AppArguments { + + // MARK: Properties + + let hostname = "127.0.0.1" + let port = 0 + let logLevel: Logger.Level? = .trace + +} diff --git a/Library/Sources/Internal/Enumerations/File.swift b/Library/Sources/Internal/Enumerations/File.swift index 203ff5e..861b114 100644 --- a/Library/Sources/Internal/Enumerations/File.swift +++ b/Library/Sources/Internal/Enumerations/File.swift @@ -1,10 +1,19 @@ enum File: String { + case app = "App" + case appArguments = "AppArguments" + case appBuilder = "AppBuilder" + case appOptions = "AppOptions" + case appTests = "AppTests" case dockerFile = "DockerFile" case dockerIgnore = "DockerIgnore" + case environment = "Environment" case gitIgnore = "GitIgnore" case license = "License" + case loggerLevel = "LoggerLevel" case package = "Package" case readme = "Readme" + case testArguments = "TestArguments" + } // MARK: - Properties @@ -15,30 +24,46 @@ extension File { var fileName: String { switch self { + case .app: "App.swift" + case .appArguments: "AppArguments.swift" + case .appBuilder: "AppBuilder.swift" + case .appOptions: "AppOptions.swift" + case .appTests: "AppTests.swift" case .dockerFile: "Dockerfile" case .dockerIgnore: ".dockerignore" + case .environment: "Environment+Properties.swift" case .gitIgnore: ".gitignore" case .license: "LICENSE" + case .loggerLevel: "LoggerLevel+Conformances.swift" case .readme: "README.md" case .package: "Package.swift" + case .testArguments: "TestArguments.swift" } } var filePath: String { - switch self { - default: folder.path + fileName - } + folder.path + fileName } var folder: Folder { switch self { + case .app, .appOptions: .app + case .appArguments, .appBuilder: .libraryPublic + case .appTests: .testCasesPublic + case .environment, .loggerLevel: .libraryInternal + case .testArguments: .testHelpers default: .root } } var resourcePath: String { - switch self { - default: "Resources/Files/Sources" + let basePath = "Resources/Files/Sources" + + return switch self { + case .app, .appOptions: "\(basePath)/App" + case .appArguments, .appBuilder, .environment, .loggerLevel: "\(basePath)/Library" + case .appTests, .testArguments: "\(basePath)/Test" + default: basePath } } diff --git a/Library/Sources/Internal/Enumerations/Folder.swift b/Library/Sources/Internal/Enumerations/Folder.swift index a732658..ba0bad6 100644 --- a/Library/Sources/Internal/Enumerations/Folder.swift +++ b/Library/Sources/Internal/Enumerations/Folder.swift @@ -16,13 +16,13 @@ extension Folder { var path: String { switch self { - case .app: "App/Sources" - case .libraryPublic: "Library/Sources/Public" - case .libraryInternal: "Library/Sources/Internal" + case .app: "App/Sources/" + case .libraryPublic: "Library/Sources/Public/" + case .libraryInternal: "Library/Sources/Internal/" case .root: "" - case .testCasesPublic: "Test/Sources/Cases/Public" - case .testCasesInternal: "Test/Sources/Cases/Internal" - case .testHelpers: "Test/Sources/Helpers" + case .testCasesPublic: "Test/Sources/Cases/Public/" + case .testCasesInternal: "Test/Sources/Cases/Internal/" + case .testHelpers: "Test/Sources/Helpers/" } } diff --git a/Test/Sources/Cases/Internal/Enumerations/FileTests.swift b/Test/Sources/Cases/Internal/Enumerations/FileTests.swift index f28e8d6..8e3b884 100644 --- a/Test/Sources/Cases/Internal/Enumerations/FileTests.swift +++ b/Test/Sources/Cases/Internal/Enumerations/FileTests.swift @@ -53,40 +53,72 @@ struct FileTests { private extension FileTests { enum Expectation { static let fileNames: [String] = [ + "App.swift", + "AppArguments.swift", + "AppBuilder.swift", + "AppOptions.swift", + "AppTests.swift", "Dockerfile", ".dockerignore", + "Environment+Properties.swift", ".gitignore", "LICENSE", + "LoggerLevel+Conformances.swift", "Package.swift", - "README.md" + "README.md", + "TestArguments.swift" ] static let filePaths: [String] = [ + "App/Sources/App.swift", + "Library/Sources/Public/AppArguments.swift", + "Library/Sources/Public/AppBuilder.swift", + "App/Sources/AppOptions.swift", + "Test/Sources/Cases/Public/AppTests.swift", "Dockerfile", ".dockerignore", + "Library/Sources/Internal/Environment+Properties.swift", ".gitignore", "LICENSE", + "Library/Sources/Internal/LoggerLevel+Conformances.swift", "Package.swift", - "README.md" + "README.md", + "Test/Sources/Helpers/TestArguments.swift" ] static let folders: [Folder] = [ + .app, + .libraryPublic, + .libraryPublic, + .app, + .testCasesPublic, .root, .root, + .libraryInternal, .root, .root, + .libraryInternal, .root, - .root + .root, + .testHelpers ] static let resourcePaths: [String] = [ + "Resources/Files/Sources/App", + "Resources/Files/Sources/Library", + "Resources/Files/Sources/Library", + "Resources/Files/Sources/App", + "Resources/Files/Sources/Test", "Resources/Files/Sources", "Resources/Files/Sources", + "Resources/Files/Sources/Library", "Resources/Files/Sources", "Resources/Files/Sources", + "Resources/Files/Sources/Library", "Resources/Files/Sources", - "Resources/Files/Sources" + "Resources/Files/Sources", + "Resources/Files/Sources/Test" ] } }