Skip to content

Commit

Permalink
refactor: Remove callbacks from HttpClient (#73)
Browse files Browse the repository at this point in the history
  • Loading branch information
fabriziodemaria committed Jan 30, 2024
1 parent 9cac324 commit 403712a
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 153 deletions.
44 changes: 20 additions & 24 deletions Sources/ConfidenceProvider/Apply/FlagApplierWithRetries.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,16 @@ final class FlagApplierWithRetries: FlagApplier {

await appliesToSend.asyncForEach { chunk in
await self.writeStatus(resolveToken: resolveEvent.resolveToken, events: chunk, status: .sending)
await executeApply(
let success = await executeApply(
resolveToken: resolveEvent.resolveToken,
items: chunk
) { success in
guard success else {
await self.writeStatus(resolveToken: resolveEvent.resolveToken, events: chunk, status: .created)
return
}
// Set 'sent' property of apply events to true
await self.writeStatus(resolveToken: resolveEvent.resolveToken, events: chunk, status: .sent)
)
guard success else {
await self.writeStatus(resolveToken: resolveEvent.resolveToken, events: chunk, status: .created)
return
}
// Set 'sent' property of apply events to true
await self.writeStatus(resolveToken: resolveEvent.resolveToken, events: chunk, status: .sent)
}
}
}
Expand Down Expand Up @@ -107,9 +106,8 @@ final class FlagApplierWithRetries: FlagApplier {

private func executeApply(
resolveToken: String,
items: [FlagApply],
completion: @escaping (Bool) async -> Void
) async {
items: [FlagApply]
) async -> Bool {
let applyFlagRequestItems = items.map { applyEvent in
AppliedFlagRequestItem(
flag: applyEvent.name,
Expand All @@ -124,25 +122,23 @@ final class FlagApplierWithRetries: FlagApplier {
sdk: Sdk(id: metadata.name, version: metadata.version)
)

await performRequest(request: request) { result in
switch result {
case .success:
await completion(true)
case .failure(let error):
self.logApplyError(error: error)
await completion(false)
}
let result = await performRequest(request: request)
switch result {
case .success:
return true
case .failure(let error):
self.logApplyError(error: error)
return false
}
}

private func performRequest(
request: ApplyFlagsRequest,
completion: @escaping (ApplyFlagResult) async -> Void
) async {
request: ApplyFlagsRequest
) async -> ApplyFlagResult {
do {
try await httpClient.post(path: ":apply", data: request, completion: completion)
return try await httpClient.post(path: ":apply", data: request)
} catch {
await completion(.failure(handleError(error: error)))
return .failure(handleError(error: error))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,23 +36,23 @@ public class RemoteConfidenceClient: ConfidenceClient {
)

do {
let result: HttpClientResponse<ResolveFlagsResponse> =
try await self.httpClient.post(path: ":resolve", data: request)

guard result.response.status == .ok else {
throw result.response.mapStatusToError(error: result.decodedError)
}

guard let response = result.decodedData else {
throw OpenFeatureError.parseError(message: "Unable to parse request response")
}

let resolvedValues = try response.resolvedFlags.map { resolvedFlag in
try convert(resolvedFlag: resolvedFlag, ctx: ctx)
let result: HttpClientResult<ResolveFlagsResponse> =
try await self.httpClient.post(path: ":resolve", data: request)
switch result {
case .success(let successData):
guard successData.response.status == .ok else {
throw successData.response.mapStatusToError(error: successData.decodedError)
}
guard let response = successData.decodedData else {
throw OpenFeatureError.parseError(message: "Unable to parse request response")
}
let resolvedValues = try response.resolvedFlags.map { resolvedFlag in
try convert(resolvedFlag: resolvedFlag, ctx: ctx)
}
return ResolvesResult(resolvedValues: resolvedValues, resolveToken: response.resolveToken)
case .failure(let errorData):
throw handleError(error: errorData)
}
return ResolvesResult(resolvedValues: resolvedValues, resolveToken: response.resolveToken)
} catch let error {
throw handleError(error: error)
}
}

Expand Down
8 changes: 1 addition & 7 deletions Sources/ConfidenceProvider/Http/HttpClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,7 @@ import Foundation
typealias HttpClientResult<T> = Result<HttpClientResponse<T>, Error>

protocol HttpClient {
func post<T: Decodable>(
path: String,
data: Codable,
completion: @escaping (HttpClientResult<T>) async -> Void
) async throws

func post<T: Decodable>(path: String, data: Codable) async throws -> HttpClientResponse<T>
func post<T: Decodable>(path: String, data: Codable) async throws -> HttpClientResult<T>
}

struct HttpClientResponse<T> {
Expand Down
108 changes: 27 additions & 81 deletions Sources/ConfidenceProvider/Http/NetworkClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,113 +43,59 @@ final class NetworkClient: HttpClient {

func post<T: Decodable>(
path: String,
data: Codable,
completion: @escaping (HttpClientResult<T>) async -> Void
) async throws {
data: Codable
) async throws -> HttpClientResult<T> {
let request = try buildRequest(path: path, data: data)
await perform(request: request, retry: self.retry) { response, data, error in
if let error {
await completion(.failure(error))
return
}
let requestResult = await perform(request: request, retry: self.retry)
if let error = requestResult.error {
return .failure(error)
}

guard let response, let data else {
await completion(.failure(ConfidenceError.internalError(message: "Bad response")))
return
}
guard let response = requestResult.httpResponse, let data = requestResult.data else {
return .failure(ConfidenceError.internalError(message: "Bad response"))
}

do {
let httpClientResult: HttpClientResponse<T> =
try self.buildResponse(response: response, data: data)
await completion(.success(httpClientResult))
} catch {
await completion(.failure(error))
}
do {
let httpClientResult: HttpClientResponse<T> =
try self.buildResponse(response: response, data: data)
return .success(httpClientResult)
} catch {
return .failure(error)
}
}

private func perform(
request: URLRequest,
retry: Retry,
completion: @escaping (HTTPURLResponse?, Data?, Error?) async -> Void
) async {
retry: Retry
) async -> RequestResult {
let retryHandler = retry.handler()
let retryWait: TimeInterval? = retryHandler.retryIn()

do {
let (data, response) = try await self.session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
await completion(nil, nil, HttpClientError.invalidResponse)
return
return RequestResult(httpResponse: nil, data: nil, error: HttpClientError.invalidResponse)
}
if self.shouldRetry(httpResponse: httpResponse), let retryWait {
try? await Task.sleep(nanoseconds: UInt64(retryWait * 1_000_000_000))
await self.perform(request: request, retry: retry, completion: completion)
return
return await self.perform(request: request, retry: retry)
}
await completion(httpResponse, data, nil)
return RequestResult(httpResponse: httpResponse, data: data, error: nil)
} catch {
if self.shouldRetry(error: error), let retryWait {
try? await Task.sleep(nanoseconds: UInt64(retryWait * 1_000_000_000))
await self.perform(request: request, retry: retry, completion: completion)
return await self.perform(request: request, retry: retry)
} else {
await completion(nil, nil, error)
return RequestResult(httpResponse: nil, data: nil, error: error)
}
}
}
}

// MARK: Async

func post<T: Decodable>(path: String, data: Codable) async throws -> HttpClientResponse<T> {
let request = try buildRequest(path: path, data: data)
let result = try await perform(request: request, retry: self.retry)

return try buildResponse(response: result.response, data: result.data)
}

private func perform(
request: URLRequest,
retry: Retry
) async throws -> (response: HTTPURLResponse, data: Data?) {
let retryHandler = retry.handler()
let retryWait: TimeInterval? = retryHandler.retryIn()

var resultData: Data?
var resultResponse: HTTPURLResponse?

do {
let result: (Data, URLResponse) = try await self.session.data(for: request)

guard let httpResponse = result.1 as? HTTPURLResponse else {
throw HttpClientError.invalidResponse
}

if shouldRetry(httpResponse: httpResponse) {
if let retryWait {
try await Task.sleep(nanoseconds: UInt64(retryWait * 1_000_000_000))
return try await perform(request: request, retry: retry)
}
}

resultData = result.0
resultResponse = httpResponse
} catch {
if shouldRetry(error: error) {
if let retryWait {
try await Task.sleep(nanoseconds: UInt64(retryWait * 1_000_000_000))
return try await perform(request: request, retry: retry)
}
}

throw error
}

guard let resultResponse else {
throw HttpClientError.internalError
}

return (resultResponse, resultData)
}
struct RequestResult {
var httpResponse: HTTPURLResponse?
var data: Data?
var error: Error?
}

// MARK: Private
Expand Down
29 changes: 4 additions & 25 deletions Tests/ConfidenceProviderTests/Helpers/HttpClientMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,34 +19,13 @@ final class HttpClientMock: HttpClient {
self.testMode = testMode
}

func post<T>(
path: String,
data: Codable,
completion: @escaping (ConfidenceProvider.HttpClientResult<T>) async -> Void
) async throws where T: Decodable {
do {
let result: HttpClientResponse<T> = try handlePost(path: path, data: data)
await completion(.success(result))
} catch {
await completion(.failure(error))
}
}

func post<T>(
path: String, data: Codable
) async throws -> ConfidenceProvider.HttpClientResponse<T> where T: Decodable {
try handlePost(path: path, data: data)
}

func post<T>(
path: String, data: Codable
) throws -> ConfidenceProvider.HttpClientResponse<T> where T: Decodable {
func post<T>(path: String, data: Codable) async throws -> ConfidenceProvider.HttpClientResult<T> where T: Decodable {
try handlePost(path: path, data: data)
}

private func handlePost<T>(
path: String, data: Codable
) throws -> ConfidenceProvider.HttpClientResponse<T> where T: Decodable {
) throws -> ConfidenceProvider.HttpClientResult<T> where T: Decodable {
defer {
expectation?.fulfill()
}
Expand All @@ -56,12 +35,12 @@ final class HttpClientMock: HttpClient {

switch testMode {
case .success:
return HttpClientResponse(response: HTTPURLResponse())
return .success(HttpClientResponse(response: HTTPURLResponse()))
case .failFirstChunk:
if postCallCounter == 1 {
throw HttpClientError.invalidResponse
} else {
return HttpClientResponse(response: HTTPURLResponse())
return .success(HttpClientResponse(response: HTTPURLResponse()))
}
case .offline:
throw HttpClientError.invalidResponse
Expand Down

0 comments on commit 403712a

Please sign in to comment.