Template support for input parameters #4

Merged
javier merged 81 commits from feature/arguments-templating into main 2025-02-17 22:11:06 +00:00
12 changed files with 258 additions and 16 deletions
Showing only changes of commit c08dbe5602 - Show all commits

View File

@ -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()
}
}

View File

@ -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?
}
}

View File

@ -0,0 +1,11 @@
import Logging
public protocol AppArguments {
// MARK: Properties
var hostname: String { get }
var logLevel: Logger.Level? { get }
var port: Int { get }
}

View File

@ -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<AppRequestContext> {
let router = Router()
router.add(middleware: LogRequestsMiddleware(logger.logLevel))
router.get("/") { _,_ in
""
}
return router
}
}

View File

@ -0,0 +1,11 @@
import Hummingbird
extension Environment {
// MARK: Computed
public var logLevel: String? {
self.get("LOG_LEVEL")
}
}

View File

@ -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

View File

@ -5,7 +5,7 @@ import PackageDescription
let package = Package(
name: "App",
platforms: [
.macOS(.v10_15)
.macOS(.v14)
],
products: [
.executable(name: "App", targets: ["App"]),

View File

@ -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: "")
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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/"
}
}

View File

@ -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"
]
}
}