feat: Minimal user api controller on hummingbird app, not sure there's big wins over vapor.

This commit is contained in:
2025-01-14 09:19:04 -05:00
parent 4f47f1aed8
commit c8bcffa0b5
5 changed files with 105 additions and 30 deletions

View File

@@ -1,5 +1,5 @@
{ {
"originHash" : "68600988594927fcb25434476f598e1a813cb64030b7d9d8bf5b480e13a11989", "originHash" : "0c4aa955a400075a593dfba3804996f803c9e812e765f72d0026e12c182b751c",
"pins" : [ "pins" : [
{ {
"identity" : "async-http-client", "identity" : "async-http-client",
@@ -82,6 +82,15 @@
"version" : "2.0.2" "version" : "2.0.2"
} }
}, },
{
"identity" : "hummingbird-fluent",
"kind" : "remoteSourceControl",
"location" : "https://github.com/hummingbird-project/hummingbird-fluent.git",
"state" : {
"revision" : "45459ea5b541c6a96b87d1be848e384593b7dde3",
"version" : "2.0.0"
}
},
{ {
"identity" : "leaf", "identity" : "leaf",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",

View File

@@ -27,7 +27,8 @@ let package = Package(
.package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "1.6.3"), .package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "1.6.3"),
.package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"), .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"),
.package(url: "https://github.com/hummingbird-project/hummingbird-auth.git", from: "2.0.2"), .package(url: "https://github.com/hummingbird-project/hummingbird-auth.git", from: "2.0.2"),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0") .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"),
.package(url: "https://github.com/hummingbird-project/hummingbird-fluent.git", from: "2.0.0")
], ],
targets: [ targets: [
.executableTarget( .executableTarget(
@@ -56,8 +57,11 @@ let package = Package(
.executableTarget( .executableTarget(
name: "HApp", name: "HApp",
dependencies: [ dependencies: [
"DatabaseClientLive",
.product(name: "Hummingbird", package: "hummingbird"), .product(name: "Hummingbird", package: "hummingbird"),
.product(name: "ArgumentParser", package: "swift-argument-parser") .product(name: "HummingbirdFluent", package: "hummingbird-fluent"),
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver")
], ],
swiftSettings: swiftSettings swiftSettings: swiftSettings
), ),

View File

@@ -1,65 +1,87 @@
import DatabaseClient
import DatabaseClientLive
import FluentSQLiteDriver
import Hummingbird import Hummingbird
import HummingbirdFluent
import Logging import Logging
public struct AppConfiguration { public protocol AppArguments {
var inMemoryDatabase: Bool { get }
public let hostname: String var migrate: Bool { get }
public let port: Int var revert: Bool { get }
public let logLevel: Logger.Level? var hostname: String { get }
var port: Int { get }
public init(hostname: String, port: Int, logLevel: Logger.Level? = nil) { var logLevel: Logger.Level? { get }
self.hostname = hostname
self.port = port
self.logLevel = logLevel
}
} }
/// Build application /// Build application
/// - Parameter arguments: application arguments /// - Parameter arguments: application arguments
public func buildApplication(_ arguments: AppConfiguration) async throws -> some ApplicationProtocol { public func buildApplication(_ arguments: some AppArguments) async throws -> some ApplicationProtocol {
let environment = Environment() let environment = Environment()
let logger = { let logger = {
var logger = Logger(label: "Todos") var logger = Logger(label: "PurchaseOrders")
logger.logLevel = arguments.logLevel ?? logger.logLevel = arguments.logLevel ??
environment.get("LOG_LEVEL").map { Logger.Level(rawValue: $0) ?? .info } ?? environment.get("LOG_LEVEL").map { Logger.Level(rawValue: $0) ?? .info } ??
.info .info
return logger return logger
}() }()
let fluent = Fluent(logger: logger)
if arguments.inMemoryDatabase {
fluent.databases.use(.sqlite(.memory), as: .sqlite)
} else {
fluent.databases.use(.sqlite(.file("hdb.sqlite")), as: .sqlite)
}
let dbClient = DatabaseClient.live(database: fluent.db())
try await fluent.migrations.add(dbClient.migrations())
if arguments.revert {
try await fluent.revert()
}
if arguments.migrate || arguments.inMemoryDatabase {
try await fluent.migrate()
}
let fluentPersist = await FluentPersistDriver(fluent: fluent)
let router = Router() let router = Router()
// Add middleware // Add middleware
router.addMiddleware {
// logging middleware // logging middleware
LogRequestsMiddleware(.info) router.add(middleware: LogRequestsMiddleware(.info))
} router.add(middleware: FileMiddleware(logger: logger))
router.add(middleware: CORSMiddleware(
allowOrigin: .originBased,
allowHeaders: [.contentType],
allowMethods: [.get, .options, .post, .delete, .put, .patch]
))
// Add health endpoint // Add health endpoint
router.get("/health") { _, _ -> HTTPResponse.Status in router.get("/health") { _, _ -> HTTPResponse.Status in
return .ok return .ok
} }
UserController(db: dbClient.users).addRoutes(to: router.group("api/users"))
// let router = buildRouter() // let router = buildRouter()
// //
let app = Application( var app = Application(
router: router, router: router,
configuration: .init( configuration: .init(
address: .hostname(arguments.hostname, port: arguments.port), address: .hostname(arguments.hostname, port: arguments.port),
serverName: "Todos" serverName: "Purchase-Orders"
), ),
logger: logger logger: logger
) )
app.addServices(fluent, fluentPersist)
return app return app
} }
/// Build router // Build router
// func buildRouter() -> Router<AppRequestContext> { // func buildRouter() -> Router<AppRequestContext> {
// let router = Router() // let router = Router()
// //

View File

@@ -3,7 +3,7 @@ import Hummingbird
import Logging import Logging
@main @main
struct App: AsyncParsableCommand { struct App: AsyncParsableCommand, AppArguments {
@Option(name: .shortAndLong) @Option(name: .shortAndLong)
var hostname: String = "127.0.0.1" var hostname: String = "127.0.0.1"
@@ -14,16 +14,28 @@ struct App: AsyncParsableCommand {
@Option(name: .shortAndLong) @Option(name: .shortAndLong)
var logLevel: Logger.Level? var logLevel: Logger.Level?
@Flag(name: .shortAndLong)
var migrate: Bool = false
@Flag(name: .shortAndLong)
var revert: Bool = false
@Flag(name: .shortAndLong)
var inMemoryDatabase: Bool = false
func run() async throws { func run() async throws {
let app = try await buildApplication(.init(hostname: hostname, port: port, logLevel: logLevel)) let app = try await buildApplication(self)
try await app.runService() try await app.runService()
} }
} }
/// Extend `Logger.Level` so it can be used as an argument /// Extend `Logger.Level` so it can be used as an argument
#if hasFeature(RetroactiveAttribute) #if hasFeature(RetroactiveAttribute)
extension Logger.Level: @retroactive ExpressibleByArgument {} extension Logger.Level: @retroactive ExpressibleByArgument {
public init?(argument: String) {
self.init(rawValue: argument)
}
}
#else #else
extension Logger.Level: ExpressibleByArgument {} extension Logger.Level: ExpressibleByArgument {}
#endif #endif

View File

@@ -0,0 +1,28 @@
import DatabaseClient
import Hummingbird
import HummingbirdFluent
import SharedModels
struct UserController<Context: RequestContext> {
let db: DatabaseClient.Users
func addRoutes(to group: RouterGroup<Context>) {
group.get(use: list)
.post(use: create)
}
@Sendable
func list(_ request: Request, context: Context) async throws -> [User] {
try await db.fetchAll()
}
@Sendable
func create(_ request: Request, context: Context) async throws -> User {
let create = try await request.decode(as: User.Create.self, context: context)
let user = try await db.create(create)
return user
}
}
extension User.Create: ResponseCodable {}
extension User: ResponseCodable {}