diff --git a/Package.swift b/Package.swift index ca12103..48c4238 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.8 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -22,11 +22,20 @@ let package = Package( .target( name: "NetworkKit", swiftSettings: [ - .enableUpcomingFeature("StrictConcurrency"), - .enableExperimentalFeature("InternalImportsByDefault") + // Enable strict concurrency checking (Swift 6 language mode) + .enableUpcomingFeature("StrictConcurrency"), + + // Approachable Concurrency (Swift 6.2) - single feature flag + .enableUpcomingFeature("ApproachableConcurrency"), + + .enableExperimentalFeature("InternalImportsByDefault") ]), .testTarget( name: "NetworkKitTests", - dependencies: ["NetworkKit"]) + dependencies: ["NetworkKit"], + swiftSettings: [ + .enableUpcomingFeature("StrictConcurrency"), + .enableUpcomingFeature("ApproachableConcurrency") + ]) ] ) diff --git a/Sources/NetworkKit/EndPoint.swift b/Sources/NetworkKit/EndPoint.swift index 9041f0d..05b5d08 100644 --- a/Sources/NetworkKit/EndPoint.swift +++ b/Sources/NetworkKit/EndPoint.swift @@ -7,7 +7,9 @@ import Foundation -public protocol EndPoint { +/// Protocol defining network endpoint configuration +/// Conforming types must be Sendable for safe concurrent usage +public protocol EndPoint: Sendable { var host: String { get } var scheme: String { get } var path: String { get } diff --git a/Sources/NetworkKit/NetworkError.swift b/Sources/NetworkKit/NetworkError.swift index b1dde47..8afce46 100644 --- a/Sources/NetworkKit/NetworkError.swift +++ b/Sources/NetworkKit/NetworkError.swift @@ -7,7 +7,9 @@ import Foundation -public enum NetworkError: Error { +/// Network-related errors that can occur during request processing +/// Conforms to Sendable for safe concurrent usage +public enum NetworkError: Error, Sendable { case decode case generic case invalidURL @@ -30,7 +32,7 @@ public enum NetworkError: Error { return "Unauthorized URL" case .unexpectedStatusCode: return "Status Code Error" - default: + case .unknown: return "Unknown Error" } } diff --git a/Sources/NetworkKit/Networkable.swift b/Sources/NetworkKit/Networkable.swift index 0f3e675..104d664 100644 --- a/Sources/NetworkKit/Networkable.swift +++ b/Sources/NetworkKit/Networkable.swift @@ -5,43 +5,103 @@ // Created by kanagasabapathy on 01/01/24. // -import Combine -import Foundation - -public protocol Networkable { - func sendRequest(urlStr: String) async throws -> T - func sendRequest(endpoint: EndPoint) async throws -> T - func sendRequest(endpoint: EndPoint, resultHandler: @Sendable @escaping (Result) -> Void) - func sendRequest(endpoint: EndPoint, type: T.Type) -> AnyPublisher +@_exported import Combine +@_exported import Foundation + +/// Main networking protocol with full Swift 6 concurrency support +/// +/// This protocol provides two API styles: +/// - async/await: Modern Swift concurrency (inherits caller's isolation) +/// - Closures: Legacy callback-based API +/// +/// All generic types are constrained to Sendable for safe concurrent usage. +/// +public protocol Networkable: Sendable { + + /// Sends a network request using a URL string + /// - Parameter urlStr: The URL string to request + /// - Returns: Decoded response of type T + /// - Throws: NetworkError if the request fails + /// - Note: Inherits caller's isolation context (SE-0461) + func sendRequest(urlStr: String) async throws -> T + + /// Sends a network request and returns a Combine publisher + /// - Parameters: + /// - endpoint: The endpoint configuration + /// - type: The type to decode the response into + /// - Returns: Publisher that emits the decoded response or NetworkError + func sendRequest(endpoint: EndPoint, type: T.Type) -> AnyPublisher where T: Decodable & Sendable + + /// Bridging callbacks to async/await with continuation + /// Demonstrates bridging callback-based APIs to async/await + /// Useful pattern for wrapping legacy APIs without native async support + /// - Parameter endpoint: The endpoint configuration + /// - Returns: Decoded response of type T + /// - Throws: NetworkError if the request fails + func sendRequestWithContinuation(endpoint: EndPoint) async throws -> T + + /// Sends a network request using an endpoint configuration + /// - Parameter endpoint: The endpoint configuration + /// - Returns: Decoded response of type T + /// - Throws: NetworkError if the request fails + /// - Note: Inherits caller's isolation context (SE-0461) + func sendRequest(endpoint: EndPoint) async throws -> T + + /// Sends a network request with a completion handler + /// - Parameters: + /// - endpoint: The endpoint configuration + /// - resultHandler: Sendable completion handler called with the result + func sendRequest(endpoint: EndPoint, resultHandler: @Sendable @escaping (Result) -> Void) } -public final class NetworkService: Networkable { - public func sendRequest(urlStr: String) async throws -> T where T : Decodable { - guard let urlStr = urlStr as String?, let url = URL(string: urlStr) as URL?else { +/// Default implementation of the Networkable protocol +/// Thread-safe and Sendable-conformant for concurrent usage +public final class NetworkService: Networkable, @unchecked Sendable { + // Immutable URLSession for thread-safety + private let session: URLSession + + /// Initializes a new NetworkService with default configuration + public convenience init() { + self.init(configuration: .default) + } + + /// Initializes a new NetworkService + /// - Parameter configuration: URLSession configuration + public init(configuration: URLSessionConfiguration) { + self.session = URLSession(configuration: configuration) + } + + /// Sends a network request using a URL string + /// Inherits caller's isolation context (SE-0461) + public func sendRequest(urlStr: String) async throws -> T where T: Decodable & Sendable { + guard let url = URL(string: urlStr) else { throw NetworkError.invalidURL } - let (data, response) = try await URLSession.shared.data(from: url) - guard let response = response as? HTTPURLResponse, 200...299 ~= response.statusCode else { + let (data, response) = try await session.data(from: url) + guard let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode else { throw NetworkError.unexpectedStatusCode } - guard let data = data as Data? else { - throw NetworkError.unknown - } - guard let decodedResponse = try? JSONDecoder().decode(T.self, from: data) else { + do { + let decodedResponse = try JSONDecoder().decode(T.self, from: data) + return decodedResponse + } catch { throw NetworkError.decode } - return decodedResponse } - public func sendRequest(endpoint: EndPoint, type: T.Type) -> AnyPublisher where T: Decodable { + /// Sends a network request and returns a Combine publisher + /// - Parameters: + /// - endpoint: The endpoint configuration + /// - type: The type to decode the response into + /// - Returns: Publisher that emits the decoded response or NetworkError + public func sendRequest(endpoint: EndPoint, type: T.Type) -> AnyPublisher where T: Decodable & Sendable { guard let urlRequest = createRequest(endPoint: endpoint) else { - preconditionFailure("Failed URLRequest") + return Fail(error: .invalidURL).eraseToAnyPublisher() } - return URLSession.shared.dataTaskPublisher(for: urlRequest) - .subscribe(on: DispatchQueue.global(qos: .background)) + return session.dataTaskPublisher(for: urlRequest) .tryMap { data, response -> Data in - guard let response = response as? HTTPURLResponse, 200...299 ~= response.statusCode else { - throw NetworkError.invalidURL + guard let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode else { + throw NetworkError.unexpectedStatusCode } return data } @@ -49,8 +109,8 @@ public final class NetworkService: Networkable { .mapError { error -> NetworkError in if error is DecodingError { return NetworkError.decode - } else if let error = error as? NetworkError { - return error + } else if let netError = error as? NetworkError { + return netError } else { return NetworkError.unknown } @@ -58,9 +118,15 @@ public final class NetworkService: Networkable { .eraseToAnyPublisher() } - public func sendRequest(endpoint: EndPoint) async throws -> T { + /// Bridging callbacks to async/await with continuation + /// Demonstrates bridging callback-based APIs to async/await + /// Useful pattern for wrapping legacy APIs without native async support + /// - Parameter endpoint: The endpoint configuration + /// - Returns: Decoded response of type T + /// - Throws: NetworkError if the request fails + public func sendRequestWithContinuation(endpoint: EndPoint) async throws -> T { guard let urlRequest = createRequest(endPoint: endpoint) else { - throw NetworkError.decode + throw NetworkError.invalidURL } return try await withCheckedThrowingContinuation { continuation in let task = URLSession(configuration: .default, delegate: nil, delegateQueue: .main) @@ -78,28 +144,56 @@ public final class NetworkService: Networkable { continuation.resume(throwing: NetworkError.unknown) return } - guard let decodedResponse = try? JSONDecoder().decode(T.self, from: data) else { + // Decode response + do { + let decoded = try JSONDecoder().decode(T.self, from: data) + continuation.resume(returning: decoded) + } catch { continuation.resume(throwing: NetworkError.decode) - return } - continuation.resume(returning: decodedResponse) } task.resume() } } - public func sendRequest(endpoint: EndPoint, - resultHandler: @Sendable @escaping (Result) -> Void) { + /// Sends a network request using an endpoint configuration + /// Inherits caller's isolation context (SE-0461) + /// - Parameter endpoint: The endpoint configuration + /// - Returns: Decoded response of type T + /// - Throws: NetworkError if the request fails + public func sendRequest(endpoint: any EndPoint) async throws -> T where T : Decodable, T : Sendable { + guard let urlRequest = createRequest(endPoint: endpoint) else { + throw NetworkError.invalidURL + } + let (data, response) = try await session.data(for: urlRequest) + guard let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode else { + throw NetworkError.unexpectedStatusCode + } + do { + return try JSONDecoder().decode(T.self, from: data) + } catch { + throw NetworkError.decode + } + } + /// Sends a network request with a completion handler + /// - Parameters: + /// - endpoint: The endpoint configuration + /// - resultHandler: Sendable completion handler called with the result + public func sendRequest( + endpoint: EndPoint, + resultHandler: @Sendable @escaping (Result) -> Void + ) { guard let urlRequest = createRequest(endPoint: endpoint) else { + resultHandler(.failure(.invalidURL)) return } - let urlTask = URLSession.shared.dataTask(with: urlRequest) { data, response, error in - guard error == nil else { - resultHandler(.failure(.invalidURL)) + let urlTask = session.dataTask(with: urlRequest) { data, response, error in + if error != nil { + resultHandler(.failure(.unknown)) return } - guard let response = response as? HTTPURLResponse, 200...299 ~= response.statusCode else { + guard let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode else { resultHandler(.failure(.unexpectedStatusCode)) return } @@ -107,22 +201,22 @@ public final class NetworkService: Networkable { resultHandler(.failure(.unknown)) return } - guard let decodedResponse = try? JSONDecoder().decode(T.self, from: data) else { + do { + let decodedResponse = try JSONDecoder().decode(T.self, from: data) + resultHandler(.success(decodedResponse)) + } catch { resultHandler(.failure(.decode)) - return } - resultHandler(.success(decodedResponse)) } urlTask.resume() } - public init() { - - } -} + // MARK: - Private Helper Methods -extension Networkable { - fileprivate func createRequest(endPoint: EndPoint) -> URLRequest? { + /// Creates a URLRequest from an EndPoint configuration + /// - Parameter endPoint: The endpoint configuration + /// - Returns: A configured URLRequest, or nil if the URL is invalid + private func createRequest(endPoint: EndPoint) -> URLRequest? { var urlComponents = URLComponents() urlComponents.scheme = endPoint.scheme urlComponents.host = endPoint.host diff --git a/Sources/NetworkKit/RequestMethod.swift b/Sources/NetworkKit/RequestMethod.swift index 58cf8df..7df15b0 100644 --- a/Sources/NetworkKit/RequestMethod.swift +++ b/Sources/NetworkKit/RequestMethod.swift @@ -7,10 +7,12 @@ import Foundation -public enum RequestMethod: String { +/// HTTP request methods supported by the networking layer +/// Conforms to Sendable for safe concurrent usage +public enum RequestMethod: String, Sendable { case get = "GET" case post = "POST" case put = "PUT" - case PATCH = "PATCH" + case patch = "PATCH" case delete = "DELETE" }