Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add convenience APIs around accessing Content, Files & Attachments #77

Merged
merged 3 commits into from Dec 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/Alchemy/Alchemy+Papyrus/Endpoint+Request.swift
Expand Up @@ -40,7 +40,7 @@ extension Client {
request: Request
) async throws -> (clientResponse: Client.Response, response: Response) {
let components = try endpoint.httpComponents(dto: request)
var request = withHeaders(components.headers)
var request = builder().withHeaders(components.headers)

if let body = components.body {
switch components.contentEncoding {
Expand Down
2 changes: 1 addition & 1 deletion Sources/Alchemy/Application/Application+Routing.swift
Expand Up @@ -157,7 +157,7 @@ extension Application {
if let convertible = value as? ResponseConvertible {
return try await convertible.response()
} else {
return try value.convert()
return try value.response()
}
})
}
Expand Down
82 changes: 46 additions & 36 deletions Sources/Alchemy/Client/Client.swift
Expand Up @@ -10,12 +10,10 @@ import NIOHTTP1
/// let response = try await Http.get("https://swift.org")
///
/// See `ClientProvider` for the request builder interface.
public final class Client: ClientProvider, Service {
public final class Client: Service {
/// A type for making http requests with a `Client`. Supports static or
/// streamed content.
public struct Request {
/// How long until this request times out.
public var timeout: TimeAmount? = nil
/// The url components.
public var urlComponents: URLComponents = URLComponents()
/// The request method.
Expand All @@ -28,6 +26,20 @@ public final class Client: ClientProvider, Service {
public var url: URL { urlComponents.url ?? URL(string: "/")! }
/// Remote host, resolved from `URL`.
public var host: String { urlComponents.url?.host ?? "" }
/// How long until this request times out.
public var timeout: TimeAmount? = nil
/// Custom config override when making this request.
public var config: HTTPClient.Configuration? = nil
/// Allows for extending storage on this type.
public var extensions = Extensions<Self>()

public init(url: String = "", method: HTTPMethod = .GET, headers: HTTPHeaders = [:], body: ByteContent? = nil, timeout: TimeAmount? = nil) {
self.urlComponents = URLComponents(string: url) ?? URLComponents()
self.method = method
self.headers = headers
self.body = body
self.timeout = timeout
}

/// The underlying `AsyncHTTPClient.HTTPClient.Request`.
fileprivate var _request: HTTPClient.Request {
Expand Down Expand Up @@ -59,7 +71,7 @@ public final class Client: ClientProvider, Service {

/// The response type of a request made with client. Supports static or
/// streamed content.
public struct Response {
public struct Response: ResponseInspector {
/// The request that resulted in this response
public var request: Client.Request
/// Remote host of the request.
Expand All @@ -72,6 +84,8 @@ public final class Client: ClientProvider, Service {
public let headers: HTTPHeaders
/// Response body.
public var body: ByteContent?
/// Allows for extending storage on this type.
public var extensions = Extensions<Self>()

/// Create a stubbed response with the given info. It will be returned
/// for any incoming request that matches the stub pattern.
Expand All @@ -81,56 +95,48 @@ public final class Client: ClientProvider, Service {
headers: HTTPHeaders = [:],
body: ByteContent? = nil
) -> Client.Response {
Client.Response(request: .init(), host: "", status: status, version: version, headers: headers, body: body)
Client.Response(request: Request(url: ""), host: "", status: status, version: version, headers: headers, body: body)
}
}

/// Helper for building http requests.
public final class Builder: RequestBuilder {
/// A request made with this builder returns a `Client.Response`.
public typealias Res = Response

/// Build using this builder.
public var builder: Builder { self }
/// The request being built.
public var partialRequest: Request = .init()

private let execute: (Request, HTTPClient.Configuration?) async throws -> Client.Response
private var configOverride: HTTPClient.Configuration? = nil
public struct Builder: RequestBuilder {
public var client: Client
public var urlComponents: URLComponents { get { request.urlComponents } set { request.urlComponents = newValue} }
public var method: HTTPMethod { get { request.method } set { request.method = newValue} }
public var headers: HTTPHeaders { get { request.headers } set { request.headers = newValue} }
public var body: ByteContent? { get { request.body } set { request.body = newValue} }
private var request: Client.Request

fileprivate init(execute: @escaping (Request, HTTPClient.Configuration?) async throws -> Client.Response) {
self.execute = execute
init(client: Client) {
self.client = client
self.request = Request()
}

/// Execute the built request using the backing client.
///
/// - Returns: The resulting response.
public func execute() async throws -> Response {
try await execute(partialRequest, configOverride)
public func execute() async throws -> Client.Response {
try await client.execute(req: request)
}

/// Sets an `HTTPClient.Configuration` for this request only. See the
/// `swift-server/async-http-client` package for configuration
/// options.
public func withClientConfig(_ config: HTTPClient.Configuration) -> Builder {
self.configOverride = config
return self
with { $0.request.config = config }
}

/// Timeout if the request doesn't finish in the given time amount.
public func withTimeout(_ timeout: TimeAmount) -> Builder {
with { $0.timeout = timeout }
with { $0.request.timeout = timeout }
}

/// Stub this client, causing it to respond to all incoming requests with a
/// stub matching the request url or a default `200` stub.
public func stub(_ stubs: [(String, Client.Response)] = []) {
self.client.stubs = stubs
}
}

/// A request made with this builder returns a `Client.Response`.
public typealias Res = Response

/// The underlying `AsyncHTTPClient.HTTPClient` used for making requests.
public var httpClient: HTTPClient
/// The builder to defer to when building requests.
public var builder: Builder { Builder(execute: execute) }

private var stubWildcard: Character = "*"
private var stubs: [(pattern: String, response: Response)]?
private(set) var stubbedRequests: [Client.Request]
Expand All @@ -143,6 +149,10 @@ public final class Client: ClientProvider, Service {
self.stubbedRequests = []
}

public func builder() -> Builder {
Builder(client: self)
}

/// Shut down the underlying http client.
public func shutdown() throws {
try httpClient.syncShutdown()
Expand All @@ -161,13 +171,13 @@ public final class Client: ClientProvider, Service {
/// - config: A custom configuration for the client that will execute the
/// request
/// - Returns: The request's response.
func execute(req: Request, config: HTTPClient.Configuration?) async throws -> Response {
private func execute(req: Request) async throws -> Response {
guard stubs == nil else {
return stubFor(req)
}

let deadline: NIODeadline? = req.timeout.map { .now() + $0 }
let httpClientOverride = config.map { HTTPClient(eventLoopGroupProvider: .shared(httpClient.eventLoopGroup), configuration: $0) }
let httpClientOverride = req.config.map { HTTPClient(eventLoopGroupProvider: .shared(httpClient.eventLoopGroup), configuration: $0) }
defer { try? httpClientOverride?.syncShutdown() }
let promise = Loop.group.next().makePromise(of: Response.self)
_ = (httpClientOverride ?? httpClient)
Expand Down
174 changes: 0 additions & 174 deletions Sources/Alchemy/Client/ClientProvider.swift

This file was deleted.

2 changes: 1 addition & 1 deletion Sources/Alchemy/Client/ClientResponse+Helpers.swift
Expand Up @@ -87,7 +87,7 @@ extension ByteContent {
if Env.LOG_FULL_CLIENT_ERRORS ?? false {
switch self {
case .buffer(let buffer):
return buffer.string() ?? "N/A"
return buffer.string
case .stream:
return "<stream>"
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Alchemy/Commands/Serve/RunServe.swift
Expand Up @@ -116,7 +116,7 @@ final class RunServe: Command {
extension Router: HBRouter {
public func respond(to request: HBRequest) -> EventLoopFuture<HBResponse> {
request.eventLoop
.asyncSubmit { await self.handle(request: Request(hbRequest: request)) }
.asyncSubmit { try await self.handle(request: Request(hbRequest: request)).collect() }
.map { HBResponse(status: $0.status, headers: $0.headers, body: $0.hbResponseBody) }
}

Expand Down