Skip to content

Commit

Permalink
Migrate to Hummingbird 2
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuawright11 committed Jul 14, 2024
1 parent 1525858 commit 5fcf688
Show file tree
Hide file tree
Showing 49 changed files with 649 additions and 669 deletions.
23 changes: 18 additions & 5 deletions Alchemy/AlchemyX/Router+Papyrus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ extension Router {
path: String,
action: @escaping (RouterRequest) async throws -> RouterResponse
) {
let method = HTTPMethod(rawValue: method)
let method = HTTPRequest.Method(rawValue: method)!
on(method, at: path) { req in
try await Request.$current
.withValue(req) {
Expand All @@ -29,12 +29,25 @@ extension Request {

extension RouterResponse {
fileprivate func response() -> Alchemy.Response {
Response(
status: .init(statusCode: status),
headers: .init(headers.map { $0 }),
Alchemy.Response(
status: .init(integerLiteral: status),
headers: fields,
body: body.map { .data($0) }
)
}

var fields: HTTPFields {
var fields = HTTPFields()
for (key, value) in headers {
guard let name = HTTPField.Name(key) else {
continue
}

fields[name] = value
}

return fields
}
}

extension Alchemy.Request {
Expand All @@ -43,7 +56,7 @@ extension Alchemy.Request {
url: url,
method: method.rawValue,
headers: Dictionary(
headers.map { $0 },
headers.map { ($0.name.rawName, $0.value) },
uniquingKeysWith: { first, _ in first }
),
body: body?.data
Expand Down
15 changes: 10 additions & 5 deletions Alchemy/AlchemyX/Router+Resource.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import AlchemyX
import Pluralize
import ServiceLifecycle

extension Application {
@discardableResult
Expand All @@ -11,17 +12,21 @@ extension Application {
) -> Self where R.Identifier: SQLValueConvertible & LosslessStringConvertible {
use(ResourceController<R>(db: db, tableName: table))
if updateTable {
Lifecycle.register(
label: "Migrate_\(R.self)",
start: .async { try await db.updateSchema(R.self) },
shutdown: .none
)
Container.main.services.append(ResourceMigrationService<R>(db: db))
}

return self
}
}

struct ResourceMigrationService<R: Resource>: ServiceLifecycle.Service, @unchecked Sendable {
let db: Database

func run() async throws {
try await db.updateSchema(R.self)
}
}

extension Router {
@discardableResult
public func useResource<R: Resource>(
Expand Down
35 changes: 18 additions & 17 deletions Alchemy/Application/Application.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public protocol Application: Router {

/// Boots the app's dependencies. Don't override the default for this unless
/// you want to prevent default Alchemy services from loading.
func bootPlugins()
func bootPlugins() async throws
/// Setup your application here. Called after all services are registered.
func boot() throws

Expand Down Expand Up @@ -50,7 +50,7 @@ public extension Application {
var container: Container { .main }
var plugins: [Plugin] { [] }

func bootPlugins() {
func bootPlugins() async throws {
let alchemyPlugins: [Plugin] = [
Core(),
Schedules(),
Expand All @@ -62,9 +62,15 @@ public extension Application {
caches,
queues,
]


let allPlugins = alchemyPlugins + plugins

for plugin in alchemyPlugins + plugins {
plugin.register(in: self)
plugin.registerServices(in: self)
}

for plugin in allPlugins {
try await plugin.boot(app: self)
}
}

Expand All @@ -91,12 +97,14 @@ public extension Application {
}
}

import ServiceLifecycle

// MARK: Running

public extension Application {
func run() async throws {
do {
bootPlugins()
try await bootPlugins()
try boot()
bootRouter()
try await start()
Expand All @@ -118,7 +126,7 @@ public extension Application {

// 0. Start the application lifecycle.

try await lifecycle.start()
try await serviceGroup.run()

// 1. Parse and run a `Command` based on the application arguments.

Expand All @@ -128,21 +136,14 @@ public extension Application {
// 2. Wait for lifecycle or immediately shut down depending on if the
// command should run indefinitely.

if command.runUntilStopped {
wait()
} else {
try await stop()
if !command.runUntilStopped {
await stop()
}
}

/// Waits indefinitely for the application to be stopped.
func wait() {
lifecycle.wait()
}

/// Stops the application.
func stop() async throws {
try await lifecycle.shutdown()
func stop() async {
await serviceGroup.triggerGracefulShutdown()
}

// @main support
Expand Down
26 changes: 19 additions & 7 deletions Alchemy/Application/Plugins/Core.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ struct Core: Plugin {
return current
}

// 4. Register Lifecycle
app.container.register(Services()).singleton()

// 4. Register ServiceGroup

app.container.register { container in
var logger: Logger = container.require()
Expand All @@ -44,11 +46,9 @@ struct Core: Plugin {
logger.logLevel = .notice
}

return ServiceLifecycle(
configuration: ServiceLifecycle.Configuration(
logger: logger,
installBacktrace: !container.env.isTesting
)
return ServiceGroup(
services: container.services.services,
logger: logger
)
}.singleton()
}
Expand All @@ -62,12 +62,20 @@ struct Core: Plugin {
}
}

public final class Services {
fileprivate var services: [ServiceLifecycle.Service] = []

func append(_ service: ServiceLifecycle.Service) {
services.append(service)
}
}

extension Application {
public var env: Environment {
container.require()
}

public var lifecycle: ServiceLifecycle {
public var serviceGroup: ServiceGroup {
container.require()
}
}
Expand All @@ -77,6 +85,10 @@ extension Container {
require()
}

var services: Services {
require()
}

fileprivate var coreCount: Int {
env.isTesting ? 1 : System.coreCount
}
Expand Down
14 changes: 7 additions & 7 deletions Alchemy/Exports.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@
// Foundation
@_exported import Foundation

// HTTPTypes
@_exported import struct HTTPTypes.HTTPField
@_exported import struct HTTPTypes.HTTPFields
@_exported import struct HTTPTypes.HTTPRequest
@_exported import struct HTTPTypes.HTTPResponse

// Lifecycle
@_exported import Lifecycle
@_exported import ServiceLifecycle

// Logging
@_exported import Logging
Expand All @@ -24,9 +30,3 @@

// NIOCore
@_exported import enum NIOCore.SocketAddress

// NIOHTTP1
@_exported import struct NIOHTTP1.HTTPHeaders
@_exported import enum NIOHTTP1.HTTPMethod
@_exported import struct NIOHTTP1.HTTPVersion
@_exported import enum NIOHTTP1.HTTPResponseStatus
34 changes: 23 additions & 11 deletions Alchemy/Filesystem/File.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public struct File: Codable, ResponseConvertible, ModelProperty {
}

/// Get a temporary url for this resource.
public func temporaryUrl(expires: TimeAmount, headers: HTTPHeaders = [:]) async throws -> URL {
public func temporaryUrl(expires: TimeAmount, headers: HTTPFields = [:]) async throws -> URL {
switch source {
case .filesystem(let filesystem, let path):
return try await (filesystem ?? Storage).temporaryURL(path, expires: expires, headers: headers)
Expand Down Expand Up @@ -121,7 +121,7 @@ public struct File: Codable, ResponseConvertible, ModelProperty {
try await _response(disposition: .attachment(filename: name.inQuotes))
}

private func _response(disposition: HTTPHeaders.ContentDisposition? = nil) async throws -> Response {
private func _response(disposition: HTTPFields.ContentDisposition? = nil) async throws -> Response {
let content = try await getContent()
let response = Response(status: .ok, body: content, contentType: contentType)
response.headers.contentDisposition = disposition
Expand Down Expand Up @@ -154,30 +154,42 @@ public struct File: Codable, ResponseConvertible, ModelProperty {
// As of now, streamed files aren't possible over request multipart.
extension File: MultipartPartConvertible {
public var multipart: MultipartPart? {
var headers: HTTPHeaders = [:]
var headers: HTTPFields = [:]
headers.contentType = contentType
headers.contentDisposition = HTTPHeaders.ContentDisposition(value: "form-data", name: nil, filename: name)
headers.contentDisposition = HTTPFields.ContentDisposition(value: "form-data", name: nil, filename: name)
headers.contentLength = size
guard let content = self.content else {
Log.warning("Unable to convert a filesystem reference to a `MultipartPart`. Please load the contents of the file first.")
return nil
}

return MultipartPart(headers: headers, body: content.data)
return MultipartPart(headers: headers.nioHeaders, body: content.data)
}

public init?(multipart: MultipartPart) {
let fileExtension = multipart.headers.contentType?.fileExtension.map { ".\($0)" } ?? ""
let fileName = multipart.headers.contentDisposition?.filename ?? multipart.headers.contentDisposition?.name
let fileSize = multipart.headers.contentLength ?? multipart.body.writerIndex
if multipart.headers.contentDisposition?.filename == nil {
let fileExtension = multipart.fields.contentType?.fileExtension.map { ".\($0)" } ?? ""
let fileName = multipart.fields.contentDisposition?.filename ?? multipart.fields.contentDisposition?.name
let fileSize = multipart.fields.contentLength ?? multipart.body.writerIndex

if multipart.fields.contentDisposition?.filename == nil {
Log.warning("A multipart part had no name or filename in the Content-Disposition header, using a random UUID for the file name.")
}

// If there is no filename in the content disposition included (technically not required via RFC 7578) set to a random UUID.
let name = (fileName ?? UUID().uuidString) + fileExtension
let contentType = multipart.headers.contentType
let contentType = multipart.fields.contentType
self.init(name: name, source: .http(clientContentType: contentType), content: .buffer(multipart.body), size: fileSize)
}
}

extension MultipartPart {
var fields: HTTPFields {
HTTPFields(headers, splitCookie: false)
}
}

extension HTTPFields {
var nioHeaders: HTTPHeaders {
.init(map { ($0.name.rawName, $0.value) })
}
}
2 changes: 1 addition & 1 deletion Alchemy/Filesystem/Filesystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public final class Filesystem: Service {
return try await create(directoryUrl.appendingPathComponent(name).path, content: content)
}

public func temporaryURL(_ filepath: String, expires: TimeAmount, headers: HTTPHeaders = [:]) async throws -> URL {
public func temporaryURL(_ filepath: String, expires: TimeAmount, headers: HTTPFields = [:]) async throws -> URL {
try await provider.temporaryURL(filepath, expires: expires, headers: headers)
}

Expand Down
4 changes: 2 additions & 2 deletions Alchemy/Filesystem/FilesystemProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ public protocol FilesystemProvider {
func delete(_ filepath: String) async throws

/// Create a temporary URL to a file at the given path.
func temporaryURL(_ filepath: String, expires: TimeAmount, headers: HTTPHeaders) async throws -> URL
func temporaryURL(_ filepath: String, expires: TimeAmount, headers: HTTPFields) async throws -> URL

/// Get a URL for the file at the given path.
func url(_ filepath: String) throws -> URL

Expand Down
Loading

0 comments on commit 5fcf688

Please sign in to comment.