Skip to content

Commit

Permalink
Merge pull request #18 from AppcentMobile/feature/stream-support
Browse files Browse the repository at this point in the history
Feature/stream support
  • Loading branch information
burakcolakappcent authored Dec 17, 2023
2 parents ed9909b + 3351cda commit 9860535
Show file tree
Hide file tree
Showing 7 changed files with 294 additions and 127 deletions.
3 changes: 3 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import PackageDescription

let package = Package(
name: "ACMNetworking",
platforms: [
.iOS(.v13), // Set the minimum iOS version here
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
Expand Down
134 changes: 12 additions & 122 deletions Sources/ACMNetworking/ACMNetworking.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,137 +29,27 @@ public class ACMNetworking: NSObject {
onSuccess: ACMGenericCallbacks.ResponseCallback<T>,
onError: ACMGenericCallbacks.ErrorCallback)
{
guard let urlRequest = baseRequest(to: endpoint) else {
ACMBaseLogger.error(ACMNetworkConstants.urlRequestErrorMessage)
return
}

task = endpoint.session(delegate: self).dataTask(with: urlRequest) { data, response, error in
guard error == nil else {
self.cancel()
let message = ACMStringUtils.shared.merge(list: [
ACMNetworkConstants.errorMessage,
error?.localizedDescription ?? "",
])
ACMBaseLogger.error(message)
onError?(ACMBaseNetworkError(message: ACMNetworkConstants.errorMessage, log: error?.localizedDescription, endpoint: endpoint))
return
}

guard response != nil else {
self.cancel()
let message = ACMStringUtils.shared.merge(list: [
ACMNetworkConstants.errorMessage,
ACMNetworkConstants.responseNullMessage,
])
ACMBaseLogger.error(message)
onError?(ACMBaseNetworkError(message: ACMNetworkConstants.errorMessage, log: ACMNetworkConstants.responseNullMessage, endpoint: endpoint))
return
}

guard let data = data else {
self.cancel()
let message = ACMStringUtils.shared.merge(list: [
ACMNetworkConstants.errorMessage,
ACMNetworkConstants.dataNullMessage,
])
ACMBaseLogger.error(message)
onError?(ACMBaseNetworkError(message: ACMNetworkConstants.errorMessage, log: ACMNetworkConstants.dataNullMessage, endpoint: endpoint))
return
}
guard let urlRequest = generateURLRequest(endpoint: endpoint) else { return }

if error?.isConnectivityError ?? false {
self.cancel()
let message = ACMStringUtils.shared.merge(list: [
ACMNetworkConstants.errorMessage,
ACMNetworkConstants.dataNullMessage,
])
ACMBaseLogger.error(message)
onError?(ACMBaseNetworkError(message: ACMNetworkConstants.errorMessage, log: ACMNetworkConstants.dataNullMessage, endpoint: endpoint))
return
}
task = endpoint.session(delegate: self).dataTask(with: urlRequest) { [weak self] data, response, error in
guard let self else { return }

guard let httpResponse = response as? HTTPURLResponse else {
self.cancel()
let message = ACMStringUtils.shared.merge(list: [
ACMNetworkConstants.errorMessage,
ACMNetworkConstants.httpStatusError,
])
ACMBaseLogger.error(message)
onError?(ACMBaseNetworkError(message: ACMNetworkConstants.errorMessage, log: ACMNetworkConstants.httpStatusError, endpoint: endpoint))
return
}
self.handleNilErrorResponse(with: endpoint, error: error, onError: onError)
self.handleNilResponse(with: endpoint, response: response, onError: onError)
self.handleConnectivityError(with: endpoint, error: error, onError: onError)

guard 200 ..< 300 ~= httpResponse.statusCode else {
let message = ACMStringUtils.shared.merge(list: [
ACMNetworkConstants.errorMessage,
ACMNetworkConstants.httpStatusError,
"-\(httpResponse.statusCode)",
ACMNetworkConstants.responseInfoMessage,
String(data: data, encoding: .utf8) ?? ""
])
ACMBaseLogger.error(message)

// MARK: Retry mechanism

guard let maxRetryCount = endpoint.retryCount else {
onError?(ACMBaseNetworkError(message: ACMNetworkConstants.errorMessage, log: ACMNetworkConstants.httpStatusError, endpoint: endpoint))
self.cancel()
return
}

if let currentRetryCount = currentRetryCount, currentRetryCount < maxRetryCount {
let nextRetryCount = currentRetryCount + 1
ACMBaseLogger.info(ACMStringUtils.shared.merge(list: [
String(format: ACMNetworkConstants.httpRetryCount, nextRetryCount, maxRetryCount),
]))
self.request(to: endpoint, currentRetryCount: nextRetryCount, onSuccess: onSuccess, onError: onError)
} else {
self.cancel()
}
guard let data = self.handleData(with: endpoint, data: data, onError: onError) else { return }
guard let httpResponse = self.handleHttpResponse(with: endpoint, response: response, onError: onError) else { return }

// Check if response is in valid http range
guard self.validateResponse(with: httpResponse) else {
self.executeRetry(with: endpoint, httpResponse: httpResponse, data: data, currentRetryCount: currentRetryCount, onSuccess: onSuccess, onError: onError)
return
}

self.cancel()

do {
let info = ACMStringUtils.shared.merge(list: [
ACMNetworkConstants.responseInfoMessage,
String(data: data, encoding: .utf8) ?? "",
])
ACMBaseLogger.info(info)

let responseObject = try JSONDecoder().decode(T.self, from: data)
onSuccess?(responseObject)
} catch let DecodingError.dataCorrupted(context) {
let message = ACMStringUtils.shared.merge(list: [
context.debugDescription,
])
ACMBaseLogger.error(message)
} catch let DecodingError.keyNotFound(key, context) {
let message = ACMStringUtils.shared.merge(list: [
"Key \(key) not found: \(context.debugDescription)",
"codingPath: \(context.codingPath)",
])
ACMBaseLogger.error(message)
} catch let DecodingError.valueNotFound(value, context) {
let message = ACMStringUtils.shared.merge(list: [
"Value \(value) not found: \(context.debugDescription)",
"codingPath: \(context.codingPath)",
])
ACMBaseLogger.error(message)
} catch let DecodingError.typeMismatch(type, context) {
let message = ACMStringUtils.shared.merge(list: [
"Type \(type) mismatch: \(context.debugDescription)",
"codingPath: \(context.codingPath)",
])
ACMBaseLogger.error(message)
} catch let e {
let errorMessage = String(format: ACMNetworkConstants.dataParseErrorMessage, e.localizedDescription)
ACMBaseLogger.warning(errorMessage)
onError?(ACMBaseNetworkError(message: ACMNetworkConstants.errorMessage, log: errorMessage, endpoint: endpoint))
}
self.handleResult(with: endpoint, data: data, onSuccess: onSuccess, onError: onError)
}
task?.resume()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
//
// ACMNetworking+Handle+Extensions.swift
//
//
// Created by Burak on 16.12.2023.
//

import Foundation

extension ACMNetworking {
func generateURLRequest(endpoint: ACMBaseEndpoint) -> URLRequest? {
guard let urlRequest = baseRequest(to: endpoint) else {
ACMBaseLogger.error(ACMNetworkConstants.urlRequestErrorMessage)
return nil
}
return urlRequest
}
}

extension ACMNetworking {
/// Handle if error occures
func handleNilErrorResponse(with endpoint: ACMBaseEndpoint, error: Error?, onError: ACMGenericCallbacks.ErrorCallback) {
guard error == nil else {
cancel()
let message = ACMStringUtils.shared.merge(list: [
ACMNetworkConstants.errorMessage,
error?.localizedDescription ?? "",
])
ACMBaseLogger.error(message)
onError?(ACMBaseNetworkError(message: ACMNetworkConstants.errorMessage, log: error?.localizedDescription, endpoint: endpoint))
return
}
}
}

extension ACMNetworking {
/// Handle if response is nil
func handleNilResponse(with endpoint: ACMBaseEndpoint, response: URLResponse?, onError: ACMGenericCallbacks.ErrorCallback) {
guard response != nil else {
cancel()
let message = ACMStringUtils.shared.merge(list: [
ACMNetworkConstants.errorMessage,
ACMNetworkConstants.responseNullMessage,
])
ACMBaseLogger.error(message)
onError?(ACMBaseNetworkError(message: ACMNetworkConstants.errorMessage, log: ACMNetworkConstants.responseNullMessage, endpoint: endpoint))
return
}
}
}

extension ACMNetworking {
/// Handle some connectivity error occures
func handleConnectivityError(with endpoint: ACMBaseEndpoint, error: Error?, onError: ACMGenericCallbacks.ErrorCallback) {
if error?.isConnectivityError ?? false {
cancel()
let message = ACMStringUtils.shared.merge(list: [
ACMNetworkConstants.errorMessage,
ACMNetworkConstants.dataNullMessage,
])
ACMBaseLogger.error(message)
onError?(ACMBaseNetworkError(message: ACMNetworkConstants.errorMessage, log: ACMNetworkConstants.dataNullMessage, endpoint: endpoint))
return
}
}
}

extension ACMNetworking {
/// Handle response data
func handleData(with endpoint: ACMBaseEndpoint, data: Data?, onError: ACMGenericCallbacks.ErrorCallback) -> Data? {
guard let data = data else {
cancel()
let message = ACMStringUtils.shared.merge(list: [
ACMNetworkConstants.errorMessage,
ACMNetworkConstants.dataNullMessage,
])
ACMBaseLogger.error(message)
onError?(ACMBaseNetworkError(message: ACMNetworkConstants.errorMessage, log: ACMNetworkConstants.dataNullMessage, endpoint: endpoint))
return nil
}
return data
}
}

extension ACMNetworking {
/// Handle http response
func handleHttpResponse(with endpoint: ACMBaseEndpoint, response: URLResponse?, onError: ACMGenericCallbacks.ErrorCallback) -> HTTPURLResponse? {
guard let httpResponse = response as? HTTPURLResponse else {
cancel()
let message = ACMStringUtils.shared.merge(list: [
ACMNetworkConstants.errorMessage,
ACMNetworkConstants.httpStatusError,
])
ACMBaseLogger.error(message)
onError?(ACMBaseNetworkError(message: ACMNetworkConstants.errorMessage, log: ACMNetworkConstants.httpStatusError, endpoint: endpoint))
return nil
}
return httpResponse
}
}

extension ACMNetworking {
/// Validates response with http success statuses
func validateResponse(with httpResponse: HTTPURLResponse) -> Bool {
return 200 ..< 300 ~= httpResponse.statusCode
}
}

extension ACMNetworking {
/// Execute retry mechanism
func executeRetry<T: Decodable>(with endpoint: ACMBaseEndpoint, httpResponse: HTTPURLResponse, data: Data, currentRetryCount: Int?, onSuccess: ACMGenericCallbacks.ResponseCallback<T>, onError: ACMGenericCallbacks.ErrorCallback) {
let message = ACMStringUtils.shared.merge(list: [
ACMNetworkConstants.errorMessage,
ACMNetworkConstants.httpStatusError,
"-\(httpResponse.statusCode)",
ACMNetworkConstants.responseInfoMessage,
String(data: data, encoding: .utf8) ?? "",
])
ACMBaseLogger.error(message)

// MARK: Retry mechanism

guard let maxRetryCount = endpoint.retryCount else {
onError?(ACMBaseNetworkError(message: ACMNetworkConstants.errorMessage, log: ACMNetworkConstants.httpStatusError, endpoint: endpoint))
cancel()
return
}

if let currentRetryCount = currentRetryCount, currentRetryCount < maxRetryCount {
let nextRetryCount = currentRetryCount + 1
ACMBaseLogger.info(ACMStringUtils.shared.merge(list: [
String(format: ACMNetworkConstants.httpRetryCount, nextRetryCount, maxRetryCount),
]))
request(to: endpoint, currentRetryCount: nextRetryCount, onSuccess: onSuccess, onError: onError)
} else {
cancel()
}
}
}

extension ACMNetworking {
/// Handle server response
func handleResult<T: Decodable>(with endpoint: ACMBaseEndpoint, data: Data, onSuccess: ACMGenericCallbacks.ResponseCallback<T>, onError: ACMGenericCallbacks.ErrorCallback) {
do {
let dataString = String(data: data, encoding: .utf8) ?? ""
let info = ACMStringUtils.shared.merge(list: [
ACMNetworkConstants.responseInfoMessage,
dataString,
])
ACMBaseLogger.info(info)

if endpoint.isStream == true {
let components = dataString
.components(separatedBy: "\n").filter { !$0.replacingOccurrences(of: " ", with: "").isEmpty }
.map { $0.replacingOccurrences(of: "data:", with: "")
.replacingOccurrences(of: " ", with: "")
}
.filter { !$0.contains("DONE") }
.joined(separator: ",")

let componentMerged = String(format: "[%@]", components)

if let data = componentMerged.toData {
let responseObject = try JSONDecoder().decode(T.self, from: data)
onSuccess?(responseObject)
}
} else {
let responseObject = try JSONDecoder().decode(T.self, from: data)
onSuccess?(responseObject)
}
} catch let DecodingError.dataCorrupted(context) {
let message = ACMStringUtils.shared.merge(list: [
context.debugDescription,
])
ACMBaseLogger.error(message)
} catch let DecodingError.keyNotFound(key, context) {
let message = ACMStringUtils.shared.merge(list: [
"Key \(key) not found: \(context.debugDescription)",
"codingPath: \(context.codingPath)",
])
ACMBaseLogger.error(message)
} catch let DecodingError.valueNotFound(value, context) {
let message = ACMStringUtils.shared.merge(list: [
"Value \(value) not found: \(context.debugDescription)",
"codingPath: \(context.codingPath)",
])
ACMBaseLogger.error(message)
} catch let DecodingError.typeMismatch(type, context) {
let message = ACMStringUtils.shared.merge(list: [
"Type \(type) mismatch: \(context.debugDescription)",
"codingPath: \(context.codingPath)",
])
ACMBaseLogger.error(message)
} catch let e {
let errorMessage = String(format: ACMNetworkConstants.dataParseErrorMessage, e.localizedDescription)
ACMBaseLogger.warning(errorMessage)
onError?(ACMBaseNetworkError(message: ACMNetworkConstants.errorMessage, log: errorMessage, endpoint: endpoint))
}
}
}
Loading

0 comments on commit 9860535

Please sign in to comment.