Skip to content
17 changes: 13 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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")
])
]
)
4 changes: 3 additions & 1 deletion Sources/NetworkKit/EndPoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
6 changes: 4 additions & 2 deletions Sources/NetworkKit/NetworkError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,7 +32,7 @@ public enum NetworkError: Error {
return "Unauthorized URL"
case .unexpectedStatusCode:
return "Status Code Error"
default:
case .unknown:
return "Unknown Error"
}
}
Expand Down
186 changes: 140 additions & 46 deletions Sources/NetworkKit/Networkable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,62 +5,128 @@
// Created by kanagasabapathy on 01/01/24.
//

import Combine
import Foundation

public protocol Networkable {
func sendRequest<T: Decodable>(urlStr: String) async throws -> T
func sendRequest<T: Decodable>(endpoint: EndPoint) async throws -> T
func sendRequest<T: Decodable>(endpoint: EndPoint, resultHandler: @Sendable @escaping (Result<T, NetworkError>) -> Void)
func sendRequest<T: Decodable>(endpoint: EndPoint, type: T.Type) -> AnyPublisher<T, NetworkError>
@_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<T: Decodable & Sendable>(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<T>(endpoint: EndPoint, type: T.Type) -> AnyPublisher<T, NetworkError> 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<T: Decodable & Sendable>(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<T: Decodable & Sendable>(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<T: Decodable & Sendable>(endpoint: EndPoint, resultHandler: @Sendable @escaping (Result<T, NetworkError>) -> Void)
}

public final class NetworkService: Networkable {
public func sendRequest<T>(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<T>(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<T>(endpoint: EndPoint, type: T.Type) -> AnyPublisher<T, NetworkError> 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<T>(endpoint: EndPoint, type: T.Type) -> AnyPublisher<T, NetworkError> 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
}
.decode(type: T.self, decoder: JSONDecoder())
.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
}
}
.eraseToAnyPublisher()
}

public func sendRequest<T: Decodable>(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<T: Decodable & Sendable>(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)
Expand All @@ -78,51 +144,79 @@ 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<T: Decodable>(endpoint: EndPoint,
resultHandler: @Sendable @escaping (Result<T, NetworkError>) -> 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<T>(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<T: Decodable & Sendable>(
endpoint: EndPoint,
resultHandler: @Sendable @escaping (Result<T, NetworkError>) -> 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
}
guard let data = data else {
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
Expand Down
6 changes: 4 additions & 2 deletions Sources/NetworkKit/RequestMethod.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}