From 4d78bf70df88623c71533776276ec65c8ce7bfe5 Mon Sep 17 00:00:00 2001 From: Kanagasabapathy Date: Thu, 30 Oct 2025 01:11:21 +0530 Subject: [PATCH 1/8] fix: export Combine and Foundation types for public API visibility With InternalImportsByDefault experimental feature enabled in Swift 6.2, imports are treated as internal by default. This caused compilation errors when public API methods used URLSessionConfiguration and AnyPublisher types. Changed to @_exported import to ensure these types remain accessible to consumers of the NetworkKit public API. --- Sources/NetworkKit/Networkable.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/NetworkKit/Networkable.swift b/Sources/NetworkKit/Networkable.swift index 0f3e675..967755f 100644 --- a/Sources/NetworkKit/Networkable.swift +++ b/Sources/NetworkKit/Networkable.swift @@ -5,8 +5,8 @@ // Created by kanagasabapathy on 01/01/24. // -import Combine -import Foundation +@_exported import Combine +@_exported import Foundation public protocol Networkable { func sendRequest(urlStr: String) async throws -> T From 8caf1b03e8f0ff0326f1af7b390a4db50ddfe3f6 Mon Sep 17 00:00:00 2001 From: Kanagasabapathy Date: Thu, 30 Oct 2025 01:16:13 +0530 Subject: [PATCH 2/8] feat: add Sendable conformance to core data types Added Sendable conformance to all data model types for Swift 6 concurrency: - EndPoint protocol: Sendable conformance with documentation - NetworkError enum: Sendable conformance with documentation - RequestMethod enum: Sendable conformance with documentation Additional improvements: - NetworkError: Changed 'default' to explicit 'case .unknown' (exhaustive) - RequestMethod: Standardized 'PATCH' to lowercase 'patch' (Swift naming) These changes ensure all data types can be safely shared across concurrency domains without data races. Related: Swift 6 strict concurrency checking --- Sources/NetworkKit/EndPoint.swift | 4 +++- Sources/NetworkKit/NetworkError.swift | 6 ++++-- Sources/NetworkKit/RequestMethod.swift | 6 ++++-- 3 files changed, 11 insertions(+), 5 deletions(-) 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/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" } From 8ef9ab3067b32c35907b5a0a3377b1b3654f2c4e Mon Sep 17 00:00:00 2001 From: Kanagasabapathy Date: Thu, 30 Oct 2025 02:01:59 +0530 Subject: [PATCH 3/8] refactor: make NetworkService thread-safe with proper DI Refactored NetworkService for Swift 6 concurrency safety: - Added @unchecked Sendable conformance (verified thread-safe) - Replaced URLSession.shared with immutable instance property - Added init(configuration:) and convenience init() - Enables dependency injection for testing Benefits: - No shared global state - Thread-safe by design (immutable properties) - Testable via URLSessionConfiguration injection - Proper initialization patterns Related: Swift 6 strict concurrency, dependency injection --- Sources/NetworkKit/Networkable.swift | 57 ++++++++++++++++++---------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/Sources/NetworkKit/Networkable.swift b/Sources/NetworkKit/Networkable.swift index 967755f..4a0f66d 100644 --- a/Sources/NetworkKit/Networkable.swift +++ b/Sources/NetworkKit/Networkable.swift @@ -8,32 +8,48 @@ @_exported import Combine @_exported 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 +public protocol Networkable: Sendable { + func sendRequest(urlStr: String) async throws -> T + func sendRequest(endpoint: EndPoint) async throws -> T + 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 { + public func sendRequest(endpoint: EndPoint, type: T.Type) -> AnyPublisher where T: Decodable & Sendable { guard let urlRequest = createRequest(endPoint: endpoint) else { preconditionFailure("Failed URLRequest") } @@ -58,7 +74,7 @@ public final class NetworkService: Networkable { .eraseToAnyPublisher() } - public func sendRequest(endpoint: EndPoint) async throws -> T { + public func sendRequest(endpoint: EndPoint) async throws -> T { guard let urlRequest = createRequest(endPoint: endpoint) else { throw NetworkError.decode } @@ -88,9 +104,10 @@ public final class NetworkService: Networkable { } } - public func sendRequest(endpoint: EndPoint, - resultHandler: @Sendable @escaping (Result) -> Void) { - + public func sendRequest( + endpoint: EndPoint, + resultHandler: @Sendable @escaping (Result) -> Void + ) { guard let urlRequest = createRequest(endPoint: endpoint) else { return } From abb92623030a5180727d552575a81d015a8f2d73 Mon Sep 17 00:00:00 2001 From: Kanagasabapathy Date: Thu, 30 Oct 2025 02:04:53 +0530 Subject: [PATCH 4/8] refactor: improve error handling, docs, and code organization Multiple improvements to NetworkService implementation: Error Handling: - Replaced try? with proper do-catch blocks - Changed preconditionFailure to Fail publisher (Combine) - Fixed incorrect error types (.invalidURL -> .unexpectedStatusCode) - Better variable naming (response -> httpResponse) Code Organization: - Converted extension to private methods - Added comprehensive documentation - Added MARK comments for navigation - Removed empty public init() These changes improve maintainability and prevent runtime crashes. Breaking: Combine method returns Fail instead of crashing. --- Sources/NetworkKit/Networkable.swift | 39 +++++++++++++--------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/Sources/NetworkKit/Networkable.swift b/Sources/NetworkKit/Networkable.swift index 4a0f66d..5dea30b 100644 --- a/Sources/NetworkKit/Networkable.swift +++ b/Sources/NetworkKit/Networkable.swift @@ -51,13 +51,12 @@ public final class NetworkService: Networkable, @unchecked Sendable { 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 } @@ -65,8 +64,8 @@ public final class NetworkService: Networkable, @unchecked Sendable { .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 } @@ -76,7 +75,7 @@ public final class NetworkService: Networkable, @unchecked Sendable { public func sendRequest(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) @@ -109,14 +108,15 @@ public final class NetworkService: Networkable, @unchecked Sendable { 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 } @@ -124,22 +124,19 @@ public final class NetworkService: Networkable, @unchecked Sendable { 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? { + private func createRequest(endPoint: EndPoint) -> URLRequest? { var urlComponents = URLComponents() urlComponents.scheme = endPoint.scheme urlComponents.host = endPoint.host From c48c4335d5f398f4266aa1e9bfde571f863ab2ef Mon Sep 17 00:00:00 2001 From: Kanagasabapathy Date: Thu, 30 Oct 2025 02:43:55 +0530 Subject: [PATCH 5/8] refactor: improve error handling, docs, and code organization --- Sources/NetworkKit/Networkable.swift | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/Sources/NetworkKit/Networkable.swift b/Sources/NetworkKit/Networkable.swift index 5dea30b..d7a170d 100644 --- a/Sources/NetworkKit/Networkable.swift +++ b/Sources/NetworkKit/Networkable.swift @@ -73,7 +73,7 @@ public final class NetworkService: Networkable, @unchecked Sendable { .eraseToAnyPublisher() } - public func sendRequest(endpoint: EndPoint) async throws -> T { + public func sendRequestWithContinuation(endpoint: EndPoint) async throws -> T { guard let urlRequest = createRequest(endPoint: endpoint) else { throw NetworkError.invalidURL } @@ -93,16 +93,32 @@ public final class NetworkService: Networkable, @unchecked Sendable { 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: 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 + } + } public func sendRequest( endpoint: EndPoint, resultHandler: @Sendable @escaping (Result) -> Void From bb9989e9d043c53814062400a2f1da956ebff400 Mon Sep 17 00:00:00 2001 From: Kanagasabapathy Date: Thu, 30 Oct 2025 02:53:38 +0530 Subject: [PATCH 6/8] feat: added comments to each method in Protocol and Implementation --- Sources/NetworkKit/Networkable.swift | 66 ++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/Sources/NetworkKit/Networkable.swift b/Sources/NetworkKit/Networkable.swift index d7a170d..8d096d7 100644 --- a/Sources/NetworkKit/Networkable.swift +++ b/Sources/NetworkKit/Networkable.swift @@ -8,9 +8,51 @@ @_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. +/// +/// Note: Combine support is available on NetworkService but not part of the protocol +/// due to Swift 6 limitations with generic associated types in protocols. 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) } @@ -49,6 +91,11 @@ public final class NetworkService: Networkable, @unchecked Sendable { } } + /// 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 { return Fail(error: .invalidURL).eraseToAnyPublisher() @@ -73,6 +120,12 @@ public final class NetworkService: Networkable, @unchecked Sendable { .eraseToAnyPublisher() } + /// 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.invalidURL @@ -105,6 +158,11 @@ public final class NetworkService: Networkable, @unchecked Sendable { } } + /// 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 @@ -119,6 +177,11 @@ public final class NetworkService: Networkable, @unchecked Sendable { 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 @@ -152,6 +215,9 @@ public final class NetworkService: Networkable, @unchecked Sendable { // MARK: - Private Helper Methods + /// 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 From 25b02ad655eb1bf5f5c018b8eb4fa5b576a2052b Mon Sep 17 00:00:00 2001 From: Kanagasabapathy Date: Thu, 30 Oct 2025 03:15:11 +0530 Subject: [PATCH 7/8] Removed comment --- Sources/NetworkKit/Networkable.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/NetworkKit/Networkable.swift b/Sources/NetworkKit/Networkable.swift index 8d096d7..104d664 100644 --- a/Sources/NetworkKit/Networkable.swift +++ b/Sources/NetworkKit/Networkable.swift @@ -16,8 +16,6 @@ /// /// All generic types are constrained to Sendable for safe concurrent usage. /// -/// Note: Combine support is available on NetworkService but not part of the protocol -/// due to Swift 6 limitations with generic associated types in protocols. public protocol Networkable: Sendable { /// Sends a network request using a URL string From 7db0ade06bb90872b50b564eff0d298f302e6104 Mon Sep 17 00:00:00 2001 From: Kanagasabapathy Date: Thu, 30 Oct 2025 03:28:58 +0530 Subject: [PATCH 8/8] feat: Added Approachable Concurrency flag --- Package.swift | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) 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") + ]) ] )