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

feat(http-client): http client wrapper #17

Merged
merged 9 commits into from
Mar 29, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "swift-core",
platforms: [
.macOS(.v10_15)
.macOS(.v12)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
Expand Down
4 changes: 4 additions & 0 deletions Sources/YData/Client/Internal.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import Foundation

// Defines the `Internal` namespace
public enum Internal {}
55 changes: 55 additions & 0 deletions Sources/YData/Client/InternalClient+Concurrency.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Foundation
import Vapor

public extension InternalClient {
func send<Request: InternalRequest, Resp: Response>(_ request: Request) async throws -> Resp
where Request.Content: Encodable {
return try await self.send(request).get()
}

func send<Request: InternalRequest, Resp: Response>(_ request: Request) async throws -> Resp {
return try await self.send(request).get()
}

func send<Request: InternalRequest, Resp: InternalModel>(_ request: Request) async throws -> Resp
where Request.Content: Encodable {
var clientRequest = buildClientRequest(for: request)

try request.content.flatMap { try clientRequest.content.encode($0, as: .json) }

return try await httpClient.send(clientRequest).mapToInternalModel()
}

func send<Request: InternalRequest, Resp: InternalModel>(_ request: Request) async throws -> Resp {
let clientRequest = buildClientRequest(for: request)

return try await httpClient.send(clientRequest).mapToInternalModel()
}
}

private extension ClientResponse {
func mapToInternalModel<R>() async throws -> R where R: InternalModel {
switch status.code {
case (100..<400):
do {
return try content.decode(R.self)
} catch {
throw Internal.ErrorResponse(headers: [:],
status: .internalServerError,
message: "failed to decode response \(error)")
}
default:
do {
let contentError = try content.decode(Internal.ServiceError.self)
throw Internal.ErrorResponse(headers: headers,
status: status,
message:contentError.message)
} catch {
throw Internal.ErrorResponse(headers: [:],
status: .internalServerError,
message: "failed to decode response with error \(error)")
}
}
}
}

94 changes: 94 additions & 0 deletions Sources/YData/Client/InternalClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import Foundation
import Vapor

public protocol InternalClient {
var scheme: URI.Scheme { get }
var host: String { get }
var port: Int? { get }
var basePath: String? { get }

var httpClient: Vapor.Client { get }
var logger: Logger { get }

func send<Req: InternalRequest, Resp: Response>(_ request: Req) -> EventLoopFuture<Resp>
}

public enum InternalClientError: Error {
case encode(Error)
}

public extension InternalClient {
var scheme: URI.Scheme { URI.Scheme("http") }
var basePath: String? { nil }

func send<Request: InternalRequest, R: Response>(_ request: Request) -> EventLoopFuture<R>
renatoaguimaraes marked this conversation as resolved.
Show resolved Hide resolved
where Request.Content: Encodable {

var clientRequest = buildClientRequest(for: request)

do {
try request.content.flatMap { try clientRequest.content.encode($0, as: .json) }
} catch {
return httpClient.eventLoop.makeFailedFuture(InternalClientError.encode(error))
}

return httpClient.send(clientRequest)
.always { self.logger.info("response for request \(clientRequest.url): \($0)") }
.mapToInternalResponse()
}

func send<Request: InternalRequest, R: Response>(_ request: Request) -> EventLoopFuture<R> {
renatoaguimaraes marked this conversation as resolved.
Show resolved Hide resolved

let clientRequest = buildClientRequest(for: request)

return httpClient.send(clientRequest)
.always { self.logger.info("response for request \(clientRequest.url): \($0)") }
.mapToInternalResponse()
}

func buildClientRequest<R: InternalRequest>(for request: R) -> ClientRequest {
renatoaguimaraes marked this conversation as resolved.
Show resolved Hide resolved
renatoaguimaraes marked this conversation as resolved.
Show resolved Hide resolved
let path = basePath.flatMap { base in request.path.flatMap { "\(base)/\($0)" } ?? base } ?? request.path ?? ""

let query = request.query.flatMap { queries in
queries.compactMap { query -> String? in
guard let value = query.value else { return nil }

guard let escapedValue = value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
return nil
}

return String(format: "@%=@%", query.name, escapedValue)
}.joined(separator: "&")
}

let url = URI(scheme: scheme, host: host, port: port, path: path, query: query)

var clientRequest = ClientRequest()
request.headers.flatMap { clientRequest.headers = .init($0.map { (key, value) in (key, value) }) }
clientRequest.method = request.method
clientRequest.url = url
return clientRequest
}
}

private extension EventLoopFuture where Value == ClientResponse {
func mapToInternalResponse<R>() -> EventLoopFuture<R> where R: Response {
return self.flatMapResult { response -> Result<R, Internal.ErrorResponse> in
switch response.status.code {
case (100..<400):
return .success(R(headers: response.headers, status: response.status, body: response.body))
default:
do {
let contentError = try response.content.decode(Internal.ServiceError.self)
return .failure(Internal.ErrorResponse(headers: response.headers,
status: response.status,
message:contentError.message))
} catch {
return .failure(Internal.ErrorResponse(headers: [:],
status: .internalServerError,
message: "failed to decode response with error \(error)"))
}
}
}
}
}
3 changes: 3 additions & 0 deletions Sources/YData/Client/InternalModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Foundation

public typealias InternalModel = Codable
53 changes: 53 additions & 0 deletions Sources/YData/Client/InternalRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Vapor

public protocol InternalRequest {
associatedtype Content

var method: HTTPMethod { get }
var path: String? { get }
var headers: HTTPHeaders? { get }
var query: [URLQueryItem]? { get }
var content: Content? { get }
}

public extension Internal {
struct NoContentRequest: InternalRequest {
public typealias Content = Optional<Void>

public let method: HTTPMethod
public let path: String?
public let headers: HTTPHeaders?
public let query: [URLQueryItem]?
public let content: Content? = nil

public init(method: HTTPMethod,
path: String? = nil,
headers: HTTPHeaders? = nil,
query: [URLQueryItem]? = nil) {
self.method = method
self.path = path
self.headers = headers
self.query = query
}
}

struct ContentRequest<Content: Encodable>: InternalRequest {
public let method: HTTPMethod
public let path: String?
public let headers: HTTPHeaders?
public let query: [URLQueryItem]?
public let content: Content?

public init(method: HTTPMethod,
path: String? = nil,
headers: HTTPHeaders? = nil,
query: [URLQueryItem]? = nil,
content: Content? = nil) {
self.method = method
self.path = path
self.headers = headers
self.query = query
self.content = content
}
}
}
58 changes: 58 additions & 0 deletions Sources/YData/Client/InternalResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import Vapor

public extension Internal {
struct ErrorResponse: Error {
public let headers: HTTPHeaders
public let status: HTTPResponseStatus
public let message: String
}

struct SuccessResponse: Response {
public var headers: HTTPHeaders
public let status: HTTPResponseStatus
public var body: ByteBuffer?

public init(headers: HTTPHeaders, status: HTTPResponseStatus, body: ByteBuffer?) {
self.headers = headers
self.status = status
self.body = body
}
}
}

extension Internal.ErrorResponse: AbortError {
public var reason: String { message }
}

public extension Internal.SuccessResponse {
private struct _ContentContainer: ContentContainer {
var body: ByteBuffer?
var headers: HTTPHeaders

var contentType: HTTPMediaType? { headers.contentType }

func decode<D>(_ decodable: D.Type, using decoder: ContentDecoder) throws -> D where D : Decodable {
guard let body = self.body else {
throw Abort(.lengthRequired)
}
return try decoder.decode(D.self, from: body, headers: self.headers)
}

mutating func encode<E>(_ encodable: E, using encoder: ContentEncoder) throws where E : Encodable {
var body = ByteBufferAllocator().buffer(capacity: 0)
try encoder.encode(encodable, to: &body, headers: &self.headers)
self.body = body
}
}

var content: ContentContainer {
get {
return _ContentContainer(body: self.body, headers: self.headers)
}
set {
let container = (newValue as! _ContentContainer)
self.body = container.body
self.headers = container.headers
}
}
}
8 changes: 8 additions & 0 deletions Sources/YData/Client/InternalServiceError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Foundation
import Vapor

public extension Internal {
struct ServiceError: Decodable {
public let message: String
}
}
49 changes: 49 additions & 0 deletions Sources/YData/Client/Response.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import Vapor

public protocol Response: ResponseEncodable {
var headers: HTTPHeaders { get set }
var status: HTTPResponseStatus { get }
var body: ByteBuffer? { get set }

var content: ContentContainer { get set }

init(headers: HTTPHeaders, status: HTTPResponseStatus, body: ByteBuffer?)
}

public extension Response {
func encodeResponse(for request: Request) -> EventLoopFuture<Vapor.Response> {
let response = Vapor.Response(status: status, headers: headers)
response.body ?= body.flatMap(Vapor.Response.Body.init)
return request.eventLoop.makeSucceededFuture(response)
}
}

public extension Response {
@inlinable
func map<NewValue>(_ callback: (ContentContainer) throws -> (NewValue)) throws -> Self where NewValue: Content {
let newValue = try callback(content)

var newResponse = Self.init(headers: headers, status: status, body: nil)
try newResponse.content.encode(newValue)

return newResponse
}

@inlinable
func map(_ callback: (ContentContainer) throws -> (Void)) throws -> Self {
let _ = try callback(content)

return Self.init(headers: headers, status: status, body: nil)
}
}

public extension EventLoopFuture where Value: Response {
func mapContent<NewValue>(_ callback: @escaping (ContentContainer) throws -> (NewValue))
-> EventLoopFuture<Value> where NewValue: Content { flatMapThrowing { try $0.map(callback) } }

func flatMapContentThrowing<NewValue>(_ callback: @escaping (ContentContainer) throws -> (NewValue))
-> EventLoopFuture<NewValue> { flatMapThrowing { try callback($0.content) } }

func flatMapContentResult<NewValue, E>(_ callback: @escaping (ContentContainer) -> Result<NewValue, E>)
-> EventLoopFuture<NewValue> where NewValue: Content, E: Error { flatMapResult { callback($0.content) } }
}