Skip to content

Commit

Permalink
feat(client): removed generic content from Request (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
portellaa authored Apr 11, 2022
1 parent a3712c4 commit 2ae4463
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 146 deletions.
45 changes: 4 additions & 41 deletions Sources/YData/Client/InternalClient+Concurrency.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,11 @@ 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, Response: InternalResponse>(_ request: Request) async throws -> Response {
try await 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 {
try await (send(request) as EventLoopFuture<ClientResponse>).get().mapToInternalModel()
}

func send<Request: InternalRequest, Resp: InternalModel>(_ request: Request) async throws -> Resp {
try await (send(request) as EventLoopFuture<ClientResponse>).get().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)")
}
}
func send<Request: InternalRequest, C: Decodable>(_ request: Request) async throws -> C {
try await (send(request) as EventLoopFuture<ClientResponse>).get().mapToModel()
}
}

32 changes: 9 additions & 23 deletions Sources/YData/Client/InternalClient.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import Foundation
import Vapor

public extension Internal {
enum Client {}
}

public protocol InternalClient {
var scheme: URI.Scheme { get }
var host: String { get }
Expand All @@ -9,8 +13,6 @@ public protocol InternalClient {

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

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

public enum InternalClientError: Error {
Expand All @@ -21,27 +23,10 @@ public extension InternalClient {
var scheme: URI.Scheme { URI.Scheme("http") }
var basePath: String? { nil }

func send<Request: InternalRequest, R: Response>(_ request: Request) -> EventLoopFuture<R>
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> {

func send<Request: InternalRequest, Response: InternalResponse>(_ request: Request) -> EventLoopFuture<Response> {
let clientRequest = buildClientRequest(for: request)

return httpClient.send(clientRequest)
return httpClient.send(buildClientRequest(for: request))
.always { self.logger.info("response for request \(clientRequest.url): \($0)") }
.mapToInternalResponse()
}
Expand All @@ -67,12 +52,13 @@ public extension InternalClient {
request.headers.flatMap { clientRequest.headers = .init($0.map { (key, value) in (key, value) }) }
clientRequest.method = request.method
clientRequest.url = url
clientRequest.body = request.body
return clientRequest
}
}

private extension EventLoopFuture where Value == ClientResponse {
func mapToInternalResponse<R>() -> EventLoopFuture<R> where R: Response {
private extension EventLoopFuture where Value: InternalResponse {
func mapToInternalResponse<R>() -> EventLoopFuture<R> where R: InternalResponse {
return self.flatMapResult { response -> Result<R, Internal.ErrorResponse> in
switch response.status.code {
case (100..<400):
Expand Down
85 changes: 59 additions & 26 deletions Sources/YData/Client/InternalRequest.swift
Original file line number Diff line number Diff line change
@@ -1,53 +1,86 @@
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 }
var content: ContentContainer { get }
var body: ByteBuffer? { get }
}

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

struct ContentRequest: InternalRequest {
public let method: HTTPMethod
public let path: String?
public let headers: HTTPHeaders?
public var headers: HTTPHeaders?
public let query: [URLQueryItem]?
public let content: Content? = nil

public var body: ByteBuffer?

public init(method: HTTPMethod,
path: String? = nil,
headers: HTTPHeaders? = nil,
query: [URLQueryItem]? = nil) {
public init<C: Content>(method: HTTPMethod,
path: String? = nil,
headers: HTTPHeaders? = nil,
query: [URLQueryItem]? = nil,
content: C) throws {
self.method = method
self.path = path
self.headers = headers
self.query = query
try self.content.encode(content)
}
}

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) {
path: String? = nil,
headers: HTTPHeaders? = nil,
query: [URLQueryItem]? = nil) {
self.method = method
self.path = path
self.headers = headers
self.query = query
self.content = content
}
}
}

public extension Internal.ContentRequest {
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
}
}
}

extension Optional where Wrapped == HTTPHeaders {
static func +=(left: inout Self, right: Wrapped) {
if var left = left {
left.add(contentsOf: right)
}

left = right
}
}
70 changes: 70 additions & 0 deletions Sources/YData/Client/InternalResponse+Vapor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import Foundation
import Vapor

extension ResponseEncodable where Self: InternalResponse {
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 EventLoopFuture where Value: InternalResponse {
func mapContent<NewValue>(_ callback: @escaping (ContentContainer) throws -> (NewValue))
-> EventLoopFuture<NewValue> where NewValue: Content {
flatMapThrowing { try callback($0.content) }
}

func mapToContent<R>() -> EventLoopFuture<R> where R: Decodable {
flatMapThrowing { response -> R in try response.content.decode(R.self) }
}

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

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

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

public extension ClientResponse {
func mapToModel<C>() throws -> C where C: Decodable {
switch status.code {
case (100..<400):

do {
return try content.decode(C.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)")
}
}
}
}
12 changes: 11 additions & 1 deletion Sources/YData/Client/InternalResponse.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import Vapor

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

var content: ContentContainer { get }

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

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

struct SuccessResponse: Response {
struct SuccessResponse: InternalResponse {
public var headers: HTTPHeaders
public let status: HTTPResponseStatus
public var body: ByteBuffer?
Expand Down
55 changes: 0 additions & 55 deletions Sources/YData/Client/Response.swift

This file was deleted.

0 comments on commit 2ae4463

Please sign in to comment.