From 9abd7d329a7efe0d33747c652e94548da5705632 Mon Sep 17 00:00:00 2001 From: danthorpe Date: Thu, 19 Oct 2023 08:26:24 +0100 Subject: [PATCH] Use 2 spaces/tab --- Package.swift | 534 +++++++++--------- Sources/Networking/Bodies/DataBody.swift | 26 +- Sources/Networking/Bodies/EmptyBody.swift | 10 +- Sources/Networking/Bodies/JSONBody.swift | 24 +- .../Authentication/Authentication.swift | 12 +- .../AuthenticationDelegate.swift | 10 +- .../Authentication/AuthenticationMethod.swift | 4 +- .../Authentication/BasicAuthentication.swift | 2 +- .../Authentication/BearerAuthentication.swift | 6 +- .../HeaderBasedAuthentication.swift | 24 +- .../Components/CheckedStatusCode.swift | 38 +- Sources/Networking/Components/Delayed.swift | 48 +- .../Components/DuplicatesRemoved.swift | 64 +-- Sources/Networking/Components/Logged.swift | 92 +-- Sources/Networking/Components/Metrics.swift | 120 ++-- Sources/Networking/Components/Numbered.swift | 54 +- Sources/Networking/Components/Retry.swift | 412 +++++++------- Sources/Networking/Components/Server.swift | 10 +- Sources/Networking/Components/Throttled.swift | 98 ++-- .../Networking/Components/URLSession.swift | 164 +++--- Sources/Networking/Core/ActiveRequests.swift | 68 +-- Sources/Networking/Core/BytesReceived.swift | 82 +-- Sources/Networking/Core/HTTPRequestBody.swift | 20 +- Sources/Networking/Core/HTTPRequestData.swift | 334 +++++------ .../Core/HTTPRequestDataOption.swift | 26 +- .../Networking/Core/HTTPResponseData.swift | 192 +++---- .../Core/HTTPResponseMetadata.swift | 26 +- .../Networking/Core/NetworkEnvironment.swift | 114 ++-- .../Networking/Core/NetworkingComponent.swift | 106 ++-- .../Networking/Core/NetworkingModifier.swift | 26 +- Sources/Networking/Core/Partial.swift | 84 +-- Sources/Networking/Core/ProgressTracker.swift | 34 +- Sources/Networking/Core/Request.swift | 72 +-- .../Networking/{Core => Errors}/Errors.swift | 0 .../Errors/NetworkingError+Decoding.swift | 54 +- .../Networking/Errors/NetworkingError.swift | 4 +- Sources/Networking/Errors/StackError.swift | 80 +-- .../Options/ExpectedContentLength.swift | 10 +- .../Networking/Options/RequestTimeout.swift | 10 +- Sources/TestSupport/BytesReceived+.swift | 6 +- Sources/TestSupport/Logger+.swift | 2 +- Sources/TestSupport/Mocked.swift | 10 +- .../NetworkEnvironmentReporter.swift | 30 +- Sources/TestSupport/Reported.swift | 78 +-- Sources/TestSupport/StubbedResponse.swift | 154 ++--- .../TerminalNetworkingComponent.swift | 38 +- .../TestAuthenticationDelegate.swift | 8 +- .../Authentication/AuthenticationTests.swift | 46 +- .../BasicCredentialsTests.swift | 2 +- .../BearerCredentialsTests.swift | 2 +- .../HeaderBasedAuthenticationTests.swift | 34 +- .../Components/CheckedStatusCodeTests.swift | 92 +-- .../Components/DuplicatesRemovedTests.swift | 102 ++-- .../Components/LoggedTests.swift | 134 ++--- .../Components/MetricsTests.swift | 58 +- .../Components/NumberedTests.swift | 70 +-- .../Components/RetryTests.swift | 370 ++++++------ .../Components/ServerTests.swift | 186 +++--- .../Components/ThrottledTests.swift | 64 +-- .../Core/BytesReceivedTests.swift | 88 +-- .../ExpectedContentLengthOptionTests.swift | 12 +- .../Core/HTTPRequestDataTests.swift | 272 ++++----- .../Core/NetworkingComponent+DataTests.swift | 152 ++--- Tests/NetworkingTests/Core/PartialTests.swift | 104 ++-- .../Core/ProgessTrackerTests.swift | 72 +-- Tests/NetworkingTests/Core/RequestTests.swift | 44 +- .../Core/RequestTimeoutOptionTests.swift | 12 +- 67 files changed, 2668 insertions(+), 2668 deletions(-) rename Sources/Networking/{Core => Errors}/Errors.swift (100%) diff --git a/Package.swift b/Package.swift index 0a1a5a8d..845e84bd 100644 --- a/Package.swift +++ b/Package.swift @@ -6,10 +6,10 @@ var package = Package(name: "danthorpe-networking") // MARK: 💫 Package Customization package.platforms = [ - .macOS(.v13), - .iOS(.v16), - .tvOS(.v16), - .watchOS(.v9) + .macOS(.v13), + .iOS(.v16), + .tvOS(.v16), + .watchOS(.v9) ] // MARK: - 🧸 Module Names @@ -21,56 +21,56 @@ let TestSupport = "TestSupport" // MARK: - 🔑 Builders let 📦 = Module.builder( - withDefaults: .init( - name: "Basic Module", - dependsOn: [ ], - defaultWith: [ - .dependencies, - ], - unitTestsDependsOn: [ ], - plugins: [ .swiftLint ] - ) + withDefaults: .init( + name: "Basic Module", + dependsOn: [ ], + defaultWith: [ + .dependencies, + ], + unitTestsDependsOn: [ ], + plugins: [ .swiftLint ] + ) ) // MARK: - 🎯 Targets Helpers <+ 📦 { - $0.createUnitTests = false + $0.createUnitTests = false } Networking <+ 📦 { - $0.createProduct = .library(nil) - $0.dependsOn = [ - Helpers - ] - $0.with = [ - .algorithms, - .asyncAlgorithms, - .concurrencyExtras, - .httpTypes, - .httpTypesFoundation, - .shortID, - .tagged - ] - $0.unitTestsDependsOn = [ - TestSupport - ] - $0.unitTestsWith = [ - .assertionExtras, - .concurrencyExtras - ] + $0.createProduct = .library(nil) + $0.dependsOn = [ + Helpers + ] + $0.with = [ + .algorithms, + .asyncAlgorithms, + .concurrencyExtras, + .httpTypes, + .httpTypesFoundation, + .shortID, + .tagged + ] + $0.unitTestsDependsOn = [ + TestSupport + ] + $0.unitTestsWith = [ + .assertionExtras, + .concurrencyExtras + ] } TestSupport <+ 📦 { - $0.createUnitTests = false - $0.createProduct = .library(nil) - $0.dependsOn = [ - Networking, - Helpers - ] - $0.with = [ - .concurrencyExtras - ] + $0.createUnitTests = false + $0.createProduct = .library(nil) + $0.dependsOn = [ + Networking, + Helpers + ] + $0.with = [ + .concurrencyExtras + ] } @@ -80,9 +80,9 @@ TestSupport <+ 📦 { // MARK: - 🧮 Binary Targets & Plugins extension Target.PluginUsage { - static let swiftLint: Self = .plugin( - name: "SwiftLintPlugin", package: "danthorpe-swiftlint-plugin" - ) + static let swiftLint: Self = .plugin( + name: "SwiftLintPlugin", package: "danthorpe-swiftlint-plugin" + ) } @@ -94,51 +94,51 @@ extension Target.PluginUsage { // MARK: - 👜 3rd Party Dependencies package.dependencies = [ - .package(url: "https://github.com/apple/swift-algorithms", from: "1.0.0"), - .package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.2"), - .package(url: "https://github.com/apple/swift-async-algorithms", from: "0.1.0"), - .package(url: "https://github.com/apple/swift-http-types", from: "1.0.0"), - .package(url: "https://github.com/danthorpe/danthorpe-utilities", branch: "main"), - .package(url: "https://github.com/danthorpe/danthorpe-swiftlint-plugin", from: "0.1.0"), - .package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.0.0"), - .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.0.0"), - .package(url: "https://github.com/pointfreeco/swift-tagged", from: "0.10.0"), + .package(url: "https://github.com/apple/swift-algorithms", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.2"), + .package(url: "https://github.com/apple/swift-async-algorithms", from: "0.1.0"), + .package(url: "https://github.com/apple/swift-http-types", from: "1.0.0"), + .package(url: "https://github.com/danthorpe/danthorpe-utilities", branch: "main"), + .package(url: "https://github.com/danthorpe/danthorpe-swiftlint-plugin", from: "0.1.0"), + .package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.0.0"), + .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.0.0"), + .package(url: "https://github.com/pointfreeco/swift-tagged", from: "0.10.0"), ] extension Target.Dependency { - static let algorithms: Target.Dependency = .product( - name: "Algorithms", package: "swift-algorithms" - ) - static let assertionExtras: Target.Dependency = .product( - name: "AssertionExtras", package: "danthorpe-utilities" - ) - static let asyncAlgorithms: Target.Dependency = .product( - name: "AsyncAlgorithms", package: "swift-async-algorithms" - ) - static let concurrencyExtras: Target.Dependency = .product( - name: "ConcurrencyExtras", package: "swift-concurrency-extras" - ) - static let dependencies: Target.Dependency = .product( - name: "Dependencies", package: "swift-dependencies" - ) - static let deque: Target.Dependency = .product( - name: "DequeModule", package: "swift-collections" - ) - static let orderedCollections: Target.Dependency = .product( - name: "OrderedCollections", package: "swift-collections" - ) - static let httpTypes: Target.Dependency = .product( - name: "HTTPTypes", package: "swift-http-types" - ) - static let httpTypesFoundation: Target.Dependency = .product( - name: "HTTPTypesFoundation", package: "swift-http-types" - ) - static let shortID: Target.Dependency = .product( - name: "ShortID", package: "danthorpe-utilities" - ) - static let tagged: Target.Dependency = .product( - name: "Tagged", package: "swift-tagged" - ) + static let algorithms: Target.Dependency = .product( + name: "Algorithms", package: "swift-algorithms" + ) + static let assertionExtras: Target.Dependency = .product( + name: "AssertionExtras", package: "danthorpe-utilities" + ) + static let asyncAlgorithms: Target.Dependency = .product( + name: "AsyncAlgorithms", package: "swift-async-algorithms" + ) + static let concurrencyExtras: Target.Dependency = .product( + name: "ConcurrencyExtras", package: "swift-concurrency-extras" + ) + static let dependencies: Target.Dependency = .product( + name: "Dependencies", package: "swift-dependencies" + ) + static let deque: Target.Dependency = .product( + name: "DequeModule", package: "swift-collections" + ) + static let orderedCollections: Target.Dependency = .product( + name: "OrderedCollections", package: "swift-collections" + ) + static let httpTypes: Target.Dependency = .product( + name: "HTTPTypes", package: "swift-http-types" + ) + static let httpTypesFoundation: Target.Dependency = .product( + name: "HTTPTypesFoundation", package: "swift-http-types" + ) + static let shortID: Target.Dependency = .product( + name: "ShortID", package: "danthorpe-utilities" + ) + static let tagged: Target.Dependency = .product( + name: "Tagged", package: "swift-tagged" + ) } @@ -150,220 +150,220 @@ extension Target.Dependency { // MARK: - 🪄 Package Helpers extension String { - var dependency: Target.Dependency { - Target.Dependency.target(name: self) - } - var snapshotTests: String { "\(self)SnapshotTests" } - var tests: String { "\(self)Tests" } + var dependency: Target.Dependency { + Target.Dependency.target(name: self) + } + var snapshotTests: String { "\(self)SnapshotTests" } + var tests: String { "\(self)Tests" } } struct Module { - enum ProductType { - case library(Product.Library.LibraryType? = nil) + enum ProductType { + case library(Product.Library.LibraryType? = nil) + } + + typealias Builder = (inout Self) -> Void + + static func builder(withDefaults defaults: Module) -> (Builder?) -> Module { + { block in + var module = Self( + name: "TO BE REPLACED", + defaultWith: defaults.defaultWith, + swiftSettings: defaults.swiftSettings, + plugins: defaults.plugins + ) + block?(&module) + return module.merged(with: defaults) } - - typealias Builder = (inout Self) -> Void - - static func builder(withDefaults defaults: Module) -> (Builder?) -> Module { - { block in - var module = Self( - name: "TO BE REPLACED", - defaultWith: defaults.defaultWith, - swiftSettings: defaults.swiftSettings, - plugins: defaults.plugins - ) - block?(&module) - return module.merged(with: defaults) - } - } - - var name: String - var group: String? - var dependsOn: [String] - let defaultWith: [Target.Dependency] - var with: [Target.Dependency] - - var createProduct: ProductType? - var createTarget: Bool - var createUnitTests: Bool - var unitTestsDependsOn: [String] - var unitTestsWith: [Target.Dependency] - var createSnapshotTests: Bool - var snapshotTestsDependsOn: [String] - - var resources: [Resource]? - var swiftSettings: [SwiftSetting] - var plugins: [Target.PluginUsage] - - var dependencies: [Target.Dependency] { - defaultWith + with + dependsOn.map { $0.dependency } - } - - var productTargets: [String] { - createTarget ? [name] : dependsOn + } + + var name: String + var group: String? + var dependsOn: [String] + let defaultWith: [Target.Dependency] + var with: [Target.Dependency] + + var createProduct: ProductType? + var createTarget: Bool + var createUnitTests: Bool + var unitTestsDependsOn: [String] + var unitTestsWith: [Target.Dependency] + var createSnapshotTests: Bool + var snapshotTestsDependsOn: [String] + + var resources: [Resource]? + var swiftSettings: [SwiftSetting] + var plugins: [Target.PluginUsage] + + var dependencies: [Target.Dependency] { + defaultWith + with + dependsOn.map { $0.dependency } + } + + var productTargets: [String] { + createTarget ? [name] : dependsOn + } + + init( + name: String, + group: String? = nil, + dependsOn: [String] = [], + defaultWith: [Target.Dependency] = [], + with: [Target.Dependency] = [], + createProduct: ProductType? = nil, + createTarget: Bool = true, + createUnitTests: Bool = true, + unitTestsDependsOn: [String] = [], + unitTestsWith: [Target.Dependency] = [], + createSnapshotTests: Bool = false, + snapshotTestsDependsOn: [String] = [], + resources: [Resource]? = nil, + swiftSettings: [SwiftSetting] = [], + plugins: [Target.PluginUsage] = [] + ) { + self.name = name + self.group = group + self.dependsOn = dependsOn + self.defaultWith = defaultWith + self.with = with + self.createProduct = createProduct + self.createTarget = createTarget + self.createUnitTests = createUnitTests + self.unitTestsDependsOn = unitTestsDependsOn + self.unitTestsWith = unitTestsWith + self.createSnapshotTests = createSnapshotTests + self.snapshotTestsDependsOn = snapshotTestsDependsOn + self.resources = resources + self.swiftSettings = swiftSettings + self.plugins = plugins + } + + private func merged(with other: Self) -> Self { + var copy = self + copy.dependsOn = Set(dependsOn).union(other.dependsOn).sorted() + copy.unitTestsDependsOn = Set(unitTestsDependsOn).union(other.unitTestsDependsOn).sorted() + copy.snapshotTestsDependsOn = Set(snapshotTestsDependsOn).union(other.snapshotTestsDependsOn).sorted() + return copy + } + + func group(by group: String) -> Self { + var copy = self + if let existingGroup = self.group { + copy.group = "\(group)/\(existingGroup)" + } else { + copy.group = group } + return copy + } +} - init( - name: String, - group: String? = nil, - dependsOn: [String] = [], - defaultWith: [Target.Dependency] = [], - with: [Target.Dependency] = [], - createProduct: ProductType? = nil, - createTarget: Bool = true, - createUnitTests: Bool = true, - unitTestsDependsOn: [String] = [], - unitTestsWith: [Target.Dependency] = [], - createSnapshotTests: Bool = false, - snapshotTestsDependsOn: [String] = [], - resources: [Resource]? = nil, - swiftSettings: [SwiftSetting] = [], - plugins: [Target.PluginUsage] = [] - ) { - self.name = name - self.group = group - self.dependsOn = dependsOn - self.defaultWith = defaultWith - self.with = with - self.createProduct = createProduct - self.createTarget = createTarget - self.createUnitTests = createUnitTests - self.unitTestsDependsOn = unitTestsDependsOn - self.unitTestsWith = unitTestsWith - self.createSnapshotTests = createSnapshotTests - self.snapshotTestsDependsOn = snapshotTestsDependsOn - self.resources = resources - self.swiftSettings = swiftSettings - self.plugins = plugins +extension Package { + func add(module: Module) { + // Check should create a product + if case let .library(type) = module.createProduct { + products.append( + .library( + name: module.name, + type: type, + targets: module.productTargets + ) + ) } - - private func merged(with other: Self) -> Self { - var copy = self - copy.dependsOn = Set(dependsOn).union(other.dependsOn).sorted() - copy.unitTestsDependsOn = Set(unitTestsDependsOn).union(other.unitTestsDependsOn).sorted() - copy.snapshotTestsDependsOn = Set(snapshotTestsDependsOn).union(other.snapshotTestsDependsOn).sorted() - return copy + // Check should create a target + if module.createTarget { + let path = module.group.map { "\($0)/Sources/\(module.name)" } + targets.append( + .target( + name: module.name, + dependencies: module.dependencies, + path: path, + resources: module.resources, + swiftSettings: module.swiftSettings, + plugins: module.plugins + ) + ) } - - func group(by group: String) -> Self { - var copy = self - if let existingGroup = self.group { - copy.group = "\(group)/\(existingGroup)" - } else { - copy.group = group - } - return copy + // Check should add unit tests + if module.createUnitTests { + let path = module.group.map { "\($0)/Tests/\(module.name.tests)" } + targets.append( + .testTarget( + name: module.name.tests, + dependencies: [module.name.dependency] + + module.unitTestsDependsOn.map { $0.dependency } + + module.unitTestsWith + + [ ], + path: path, + plugins: module.plugins + ) + ) } -} - -extension Package { - func add(module: Module) { - // Check should create a product - if case let .library(type) = module.createProduct { - products.append( - .library( - name: module.name, - type: type, - targets: module.productTargets - ) - ) - } - // Check should create a target - if module.createTarget { - let path = module.group.map { "\($0)/Sources/\(module.name)" } - targets.append( - .target( - name: module.name, - dependencies: module.dependencies, - path: path, - resources: module.resources, - swiftSettings: module.swiftSettings, - plugins: module.plugins - ) - ) - } - // Check should add unit tests - if module.createUnitTests { - let path = module.group.map { "\($0)/Tests/\(module.name.tests)" } - targets.append( - .testTarget( - name: module.name.tests, - dependencies: [module.name.dependency] - + module.unitTestsDependsOn.map { $0.dependency } - + module.unitTestsWith - + [ ], - path: path, - plugins: module.plugins - ) - ) - } - // Check should add snapshot tests - if module.createSnapshotTests { - let path = module.group.map { "\($0)/Tests/\(module.name.snapshotTests)" } - targets.append( - .testTarget( - name: module.name.snapshotTests, - dependencies: [module.name.dependency] - + module.snapshotTestsDependsOn.map { $0.dependency } - + [ ], - path: path, - plugins: module.plugins - ) - ) - } + // Check should add snapshot tests + if module.createSnapshotTests { + let path = module.group.map { "\($0)/Tests/\(module.name.snapshotTests)" } + targets.append( + .testTarget( + name: module.name.snapshotTests, + dependencies: [module.name.dependency] + + module.snapshotTestsDependsOn.map { $0.dependency } + + [ ], + path: path, + plugins: module.plugins + ) + ) } + } } protocol ModuleGroupConvertible { - func makeGroup() -> [Module] + func makeGroup() -> [Module] } extension Module: ModuleGroupConvertible { - func makeGroup() -> [Module] { [self] } + func makeGroup() -> [Module] { [self] } } struct ModuleGroup { - var name: String - var modules: [Module] - init(_ name: String, @ModuleBuilder builder: () -> [Module]) { - self.name = name - self.modules = builder() - } + var name: String + var modules: [Module] + init(_ name: String, @ModuleBuilder builder: () -> [Module]) { + self.name = name + self.modules = builder() + } } extension ModuleGroup: ModuleGroupConvertible { - func makeGroup() -> [Module] { - modules.map { $0.group(by: name) } - } + func makeGroup() -> [Module] { + modules.map { $0.group(by: name) } + } } @resultBuilder struct ModuleBuilder { - static func buildBlock() -> [Module] { [] } - static func buildBlock(_ modules: ModuleGroupConvertible...) -> [Module] { - modules.flatMap { $0.makeGroup() } - } + static func buildBlock() -> [Module] { [] } + static func buildBlock(_ modules: ModuleGroupConvertible...) -> [Module] { + modules.flatMap { $0.makeGroup() } + } } infix operator <> extension String { - /// Adds the string as a module to the package, using the provided module - static func <+ (lhs: String, rhs: Module) { - var module = rhs - module.name = lhs - package.add(module: module) - } + /// Adds the string as a module to the package, using the provided module + static func <+ (lhs: String, rhs: Module) { + var module = rhs + module.name = lhs + package.add(module: module) + } } infix operator <+ extension String { - /// Adds the string as a module to the package, allowing for inline customization - static func <> (lhs: String, rhs: Module.Builder) { - var module = Module(name: lhs) - rhs(&module) - package.add(module: module) - } + /// Adds the string as a module to the package, allowing for inline customization + static func <> (lhs: String, rhs: Module.Builder) { + var module = Module(name: lhs) + rhs(&module) + package.add(module: module) + } } diff --git a/Sources/Networking/Bodies/DataBody.swift b/Sources/Networking/Bodies/DataBody.swift index 9dec9608..66e2db69 100644 --- a/Sources/Networking/Bodies/DataBody.swift +++ b/Sources/Networking/Bodies/DataBody.swift @@ -2,17 +2,17 @@ import Foundation import HTTPTypes public struct DataBody: HTTPRequestBody { - private let data: Data - public let additionalHeaders: HTTPFields - - public var isEmpty: Bool { data.isEmpty } - - public init(data: Data, additionalHeaders: HTTPFields) { - self.data = data - self.additionalHeaders = additionalHeaders - } - - public func encode() throws -> Data { - data - } + private let data: Data + public let additionalHeaders: HTTPFields + + public var isEmpty: Bool { data.isEmpty } + + public init(data: Data, additionalHeaders: HTTPFields) { + self.data = data + self.additionalHeaders = additionalHeaders + } + + public func encode() throws -> Data { + data + } } diff --git a/Sources/Networking/Bodies/EmptyBody.swift b/Sources/Networking/Bodies/EmptyBody.swift index 81f94c11..d1dd1bee 100644 --- a/Sources/Networking/Bodies/EmptyBody.swift +++ b/Sources/Networking/Bodies/EmptyBody.swift @@ -1,9 +1,9 @@ import Foundation public struct EmptyBody: HTTPRequestBody { - public let isEmpty = true - public init() { } - public func encode() throws -> Data { - Data() - } + public let isEmpty = true + public init() { } + public func encode() throws -> Data { + Data() + } } diff --git a/Sources/Networking/Bodies/JSONBody.swift b/Sources/Networking/Bodies/JSONBody.swift index 8655cf79..ae0c0e97 100644 --- a/Sources/Networking/Bodies/JSONBody.swift +++ b/Sources/Networking/Bodies/JSONBody.swift @@ -2,16 +2,16 @@ import Foundation import HTTPTypes public struct JSONBody: HTTPRequestBody { - public let isEmpty = false - public var additionalHeaders: HTTPFields = [ - .contentType: "application/json; charset=utf-8" - ] - private let _encode: () throws -> Data - public init(_ value: Body, encoder: JSONEncoder = JSONEncoder()) { - _encode = { try encoder.encode(value) } - } - - public func encode() throws -> Data { - try _encode() - } + public let isEmpty = false + public var additionalHeaders: HTTPFields = [ + .contentType: "application/json; charset=utf-8" + ] + private let _encode: () throws -> Data + public init(_ value: Body, encoder: JSONEncoder = JSONEncoder()) { + _encode = { try encoder.encode(value) } + } + + public func encode() throws -> Data { + try _encode() + } } diff --git a/Sources/Networking/Components/Authentication/Authentication.swift b/Sources/Networking/Components/Authentication/Authentication.swift index edcb387e..0b40d782 100644 --- a/Sources/Networking/Components/Authentication/Authentication.swift +++ b/Sources/Networking/Components/Authentication/Authentication.swift @@ -9,14 +9,14 @@ extension NetworkingComponent { struct Authentication: NetworkingModifier { typealias Credentials = Delegate.Credentials let delegate: Delegate - + func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream { guard let method = request.authenticationMethod, method == Credentials.method else { return upstream.send(request) } return ResponseStream { continuation in Task { - + // Fetch the initial credentials var credentials: Credentials do { @@ -32,10 +32,10 @@ struct Authentication: NetworkingModifier { ) return } - + // Update the request to use the credentials let newRequest = credentials.apply(to: request) - + // Process the stream do { for try await event in upstream.send(newRequest) { @@ -55,7 +55,7 @@ struct Authentication: NetworkingModifier { } } } - + func refresh( unauthorized credentials: inout Credentials, response: HTTPResponseData, @@ -99,7 +99,7 @@ extension AuthenticationError: NetworkingError { return response.request } } - + public var response: HTTPResponseData? { switch self { case .fetchCredentialsFailed: diff --git a/Sources/Networking/Components/Authentication/AuthenticationDelegate.swift b/Sources/Networking/Components/Authentication/AuthenticationDelegate.swift index 2811e47c..ff23b5c7 100644 --- a/Sources/Networking/Components/Authentication/AuthenticationDelegate.swift +++ b/Sources/Networking/Components/Authentication/AuthenticationDelegate.swift @@ -1,10 +1,10 @@ /// A system which can asynchronously fetch or refresh credentials /// in order to make authenticated HTTP requests public protocol AuthenticationDelegate: Sendable { // swiftlint:disable:this class_delegate_protocol - + /// A type which represents the credentials to be used associatedtype Credentials: AuthenticatingCredentials - + /// The entry point into the authentication flow /// /// Conforming types should manage their own state, providing thread safety @@ -12,7 +12,7 @@ public protocol AuthenticationDelegate: Sendable { // swiftlint:dis /// an external system. For example - present a login interface to the user /// to collect a username and password. func fetch(for request: HTTPRequestData) async throws -> Credentials - + /// After supplying a request with credentials, it is still possible to /// encounter HTTP unauthorized errors. In such an event, this method will /// be called, allowing for a single attempt to retry with a new set of @@ -22,10 +22,10 @@ public protocol AuthenticationDelegate: Sendable { // swiftlint:dis } public protocol AuthenticatingCredentials: Hashable, Sendable { - + /// The authentication method static var method: AuthenticationMethod { get } - + /// Create a new request making use of the credentials in whichever way /// suits their purpose. E.g. by appending a query parameter func apply(to request: HTTPRequestData) -> HTTPRequestData diff --git a/Sources/Networking/Components/Authentication/AuthenticationMethod.swift b/Sources/Networking/Components/Authentication/AuthenticationMethod.swift index 7072d422..ab54ce1b 100644 --- a/Sources/Networking/Components/Authentication/AuthenticationMethod.swift +++ b/Sources/Networking/Components/Authentication/AuthenticationMethod.swift @@ -1,8 +1,8 @@ public struct AuthenticationMethod: Hashable, RawRepresentable, Sendable, HTTPRequestDataOption { public static let defaultOption: Self? = nil - + public let rawValue: String - + public init(rawValue: String) { self.rawValue = rawValue } diff --git a/Sources/Networking/Components/Authentication/BasicAuthentication.swift b/Sources/Networking/Components/Authentication/BasicAuthentication.swift index 0ae9e6fa..da3763c7 100644 --- a/Sources/Networking/Components/Authentication/BasicAuthentication.swift +++ b/Sources/Networking/Components/Authentication/BasicAuthentication.swift @@ -7,7 +7,7 @@ extension AuthenticationMethod { public struct BasicCredentials: Hashable, Sendable, AuthenticatingCredentials, HTTPRequestDataOption { public static var method: AuthenticationMethod = .basic public static let defaultOption: Self? = nil - + public let user: String public let password: String diff --git a/Sources/Networking/Components/Authentication/BearerAuthentication.swift b/Sources/Networking/Components/Authentication/BearerAuthentication.swift index 9742d466..30ec8897 100644 --- a/Sources/Networking/Components/Authentication/BearerAuthentication.swift +++ b/Sources/Networking/Components/Authentication/BearerAuthentication.swift @@ -7,13 +7,13 @@ extension AuthenticationMethod { public struct BearerCredentials: Hashable, Sendable, Codable, HTTPRequestDataOption, AuthenticatingCredentials { public static let method: AuthenticationMethod = .bearer public static let defaultOption: Self? = nil - + public let token: String - + public init(token: String) { self.token = token } - + public func apply(to request: HTTPRequestData) -> HTTPRequestData { var copy = request copy.headerFields[.authorization] = "Bearer \(token)" diff --git a/Sources/Networking/Components/Authentication/HeaderBasedAuthentication.swift b/Sources/Networking/Components/Authentication/HeaderBasedAuthentication.swift index 6ac47b2e..9368fbea 100644 --- a/Sources/Networking/Components/Authentication/HeaderBasedAuthentication.swift +++ b/Sources/Networking/Components/Authentication/HeaderBasedAuthentication.swift @@ -1,26 +1,26 @@ public struct HeaderBasedAuthentication { actor StateMachine: AuthenticationDelegate { typealias Credentials = Delegate.Credentials - + private enum State { case idle case fetching(Task) case authorized(Credentials) } - + let delegate: Delegate private var state: State = .idle - + @NetworkEnvironment(\.logger) var logger - + init(delegate: Delegate) { self.delegate = delegate } - + private func set(state: State) { self.state = state } - + func fetch(for request: HTTPRequestData) async throws -> Credentials { switch state { case let .authorized(credentials): @@ -33,7 +33,7 @@ public struct HeaderBasedAuthentication { return try await task.value } } - + private func performCredentialFetch(for request: HTTPRequestData) async throws -> Credentials { logger?.info("🔐 Fetching credentials for \(Credentials.method.rawValue, privacy: .public) authorization method") do { @@ -45,17 +45,17 @@ public struct HeaderBasedAuthentication { throw AuthenticationError.fetchCredentialsFailed(request, Credentials.method, error) } } - + func refresh(unauthorized credentials: Credentials, from response: HTTPResponseData) async throws -> Credentials { if case let .fetching(task) = state { return try await task.value } - + let task = Task { try await performCredentialRefresh(unauthorized: credentials, from: response) } set(state: .fetching(task)) return try await task.value } - + private func performCredentialRefresh( unauthorized credentials: Credentials, from response: HTTPResponseData @@ -73,9 +73,9 @@ public struct HeaderBasedAuthentication { } } } - + fileprivate let state: StateMachine - + public init(delegate: Delegate) { state = StateMachine(delegate: delegate) } diff --git a/Sources/Networking/Components/CheckedStatusCode.swift b/Sources/Networking/Components/CheckedStatusCode.swift index 8815b4c8..362b0e59 100644 --- a/Sources/Networking/Components/CheckedStatusCode.swift +++ b/Sources/Networking/Components/CheckedStatusCode.swift @@ -2,27 +2,27 @@ import Foundation import HTTPTypes extension NetworkingComponent { - public func checkedStatusCode() -> some NetworkingComponent { - modified(CheckedStatusCode()) - } + public func checkedStatusCode() -> some NetworkingComponent { + modified(CheckedStatusCode()) + } } struct CheckedStatusCode: NetworkingModifier { - func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream { - ResponseStream( - upstream.send(request) - .map { try $0.onValue(perform: checkStatusCode) } - ) - } - - private func checkStatusCode(_ response: HTTPResponseData) throws { - guard response.status.isFailure else { return } - // Check for authentication issues - switch response.status { - case .unauthorized: - throw StackError.unauthorized(response) - default: - throw StackError.statusCode(response) - } + func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream { + ResponseStream( + upstream.send(request) + .map { try $0.onValue(perform: checkStatusCode) } + ) + } + + private func checkStatusCode(_ response: HTTPResponseData) throws { + guard response.status.isFailure else { return } + // Check for authentication issues + switch response.status { + case .unauthorized: + throw StackError.unauthorized(response) + default: + throw StackError.statusCode(response) } + } } diff --git a/Sources/Networking/Components/Delayed.swift b/Sources/Networking/Components/Delayed.swift index af8a3b5c..325e4ef2 100644 --- a/Sources/Networking/Components/Delayed.swift +++ b/Sources/Networking/Components/Delayed.swift @@ -2,33 +2,33 @@ import Dependencies import os.log extension NetworkingComponent { - public func delayed(by duration: Duration) -> some NetworkingComponent { - modified(Delayed(duration: duration)) - } + public func delayed(by duration: Duration) -> some NetworkingComponent { + modified(Delayed(duration: duration)) + } } struct Delayed: NetworkingModifier { - @Dependency(\.continuousClock) var clock - @NetworkEnvironment(\.instrument) var instrument - @NetworkEnvironment(\.logger) var logger - - let duration: Duration - - func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream { - ResponseStream { continuation in - Task { - do { - await instrument?.measureElapsedTime("Delay") - if duration > .zero { - logger?.info("⏳ \(request.debugDescription) delay for \(duration)") - } - try await clock.sleep(for: duration) - } catch { - continuation.finish(throwing: error) - } - - await upstream.send(request).redirect(into: continuation) - } + @Dependency(\.continuousClock) var clock + @NetworkEnvironment(\.instrument) var instrument + @NetworkEnvironment(\.logger) var logger + + let duration: Duration + + func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream { + ResponseStream { continuation in + Task { + do { + await instrument?.measureElapsedTime("Delay") + if duration > .zero { + logger?.info("⏳ \(request.debugDescription) delay for \(duration)") + } + try await clock.sleep(for: duration) + } catch { + continuation.finish(throwing: error) } + + await upstream.send(request).redirect(into: continuation) + } } + } } diff --git a/Sources/Networking/Components/DuplicatesRemoved.swift b/Sources/Networking/Components/DuplicatesRemoved.swift index 0f92ddd4..6479afd9 100644 --- a/Sources/Networking/Components/DuplicatesRemoved.swift +++ b/Sources/Networking/Components/DuplicatesRemoved.swift @@ -1,44 +1,44 @@ import Helpers extension NetworkingComponent { - public func duplicatesRemoved() -> some NetworkingComponent { - modified(DuplicatesRemoved()) - } + public func duplicatesRemoved() -> some NetworkingComponent { + modified(DuplicatesRemoved()) + } } private struct DuplicatesRemoved: NetworkingModifier { - let activeRequests = ActiveRequests() - func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream { - ResponseStream { continuation in - Task { - let stream = await self.activeRequests.send(upstream: upstream, request: request) - await stream.redirect(into: continuation, onTermination: { - if await activeRequests.shouldMeasureElapsedTime { - @NetworkEnvironment(\.instrument) var instrument - await instrument?.measureElapsedTime("DuplicatesRemoved") - } - }) - } - } + let activeRequests = ActiveRequests() + func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream { + ResponseStream { continuation in + Task { + let stream = await self.activeRequests.send(upstream: upstream, request: request) + await stream.redirect(into: continuation, onTermination: { + if await activeRequests.shouldMeasureElapsedTime { + @NetworkEnvironment(\.instrument) var instrument + await instrument?.measureElapsedTime("DuplicatesRemoved") + } + }) + } } + } } extension ActiveRequests { - - fileprivate func isDuplicate(request: HTTPRequestData) -> Value? { - active.values.first(where: { $0.request ~= request }) - } - - fileprivate func send( - upstream: NetworkingComponent, - request: HTTPRequestData - ) -> SharedStream { - if let existing = isDuplicate(request: request) { - shouldMeasureElapsedTime = true - @NetworkEnvironment(\.logger) var logger - logger?.info("👻 \(request.identifier) is a duplicate of \(existing.request.debugDescription)") - return existing.stream - } - return add(stream: upstream.send(request), for: request) + + fileprivate func isDuplicate(request: HTTPRequestData) -> Value? { + active.values.first(where: { $0.request ~= request }) + } + + fileprivate func send( + upstream: NetworkingComponent, + request: HTTPRequestData + ) -> SharedStream { + if let existing = isDuplicate(request: request) { + shouldMeasureElapsedTime = true + @NetworkEnvironment(\.logger) var logger + logger?.info("👻 \(request.identifier) is a duplicate of \(existing.request.debugDescription)") + return existing.stream } + return add(stream: upstream.send(request), for: request) + } } diff --git a/Sources/Networking/Components/Logged.swift b/Sources/Networking/Components/Logged.swift index 05717dc0..db575b8b 100644 --- a/Sources/Networking/Components/Logged.swift +++ b/Sources/Networking/Components/Logged.swift @@ -8,63 +8,63 @@ public typealias LogSuccess = @Sendable (HTTPRequestData, HTTPResponseData, Byte extension NetworkingComponent { - public func logged( - using logger: Logger, - onStart: LogStart? = nil, - onFailure: LogFailure? = nil, - onSuccess: LogSuccess? = nil - ) -> some NetworkingComponent { - modified( - Logged( - onStart: onStart ?? { logger.info("↗️ \($0.debugDescription)") }, - onFailure: onFailure ?? { request, error in - logger.error("⚠️ \(request.debugDescription), error: \(String(describing: error))") - }, - onSuccess: onSuccess ?? { request, response, _ in - logger.info("🆗 \(response.debugDescription)") - logger.info("↙️ \(request.debugDescription)") - } - ) - ) - .networkEnvironment(\.logger) { logger } - } + public func logged( + using logger: Logger, + onStart: LogStart? = nil, + onFailure: LogFailure? = nil, + onSuccess: LogSuccess? = nil + ) -> some NetworkingComponent { + modified( + Logged( + onStart: onStart ?? { logger.info("↗️ \($0.debugDescription)") }, + onFailure: onFailure ?? { request, error in + logger.error("⚠️ \(request.debugDescription), error: \(String(describing: error))") + }, + onSuccess: onSuccess ?? { request, response, _ in + logger.info("🆗 \(response.debugDescription)") + logger.info("↙️ \(request.debugDescription)") + } + ) + ) + .networkEnvironment(\.logger) { logger } + } } extension Logger: NetworkEnvironmentKey { } extension NetworkEnvironmentValues { - public var logger: Logger? { - get { self[Logger.self] } - set { self[Logger.self] = newValue } - } + public var logger: Logger? { + get { self[Logger.self] } + set { self[Logger.self] = newValue } + } } struct Logged: NetworkingModifier { - typealias OnStart = LogStart - typealias OnFailure = LogFailure - typealias OnSuccess = LogSuccess + typealias OnStart = LogStart + typealias OnFailure = LogFailure + typealias OnSuccess = LogSuccess - let onStart: OnStart - let onFailure: OnFailure - let onSuccess: OnSuccess + let onStart: OnStart + let onFailure: OnFailure + let onSuccess: OnSuccess - func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream { - ResponseStream { continuation in - Task { - await onStart(request) - do { - for try await element in upstream.send(request) { - if case let .value(response, bytesReceived) = element { - await onSuccess(response.request, response, bytesReceived) - } - continuation.yield(element) - } - continuation.finish() - } catch { - await onFailure(request, error) - continuation.finish(throwing: error) - } + func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream { + ResponseStream { continuation in + Task { + await onStart(request) + do { + for try await element in upstream.send(request) { + if case let .value(response, bytesReceived) = element { + await onSuccess(response.request, response, bytesReceived) } + continuation.yield(element) + } + continuation.finish() + } catch { + await onFailure(request, error) + continuation.finish(throwing: error) } + } } + } } diff --git a/Sources/Networking/Components/Metrics.swift b/Sources/Networking/Components/Metrics.swift index e46989de..13b677da 100644 --- a/Sources/Networking/Components/Metrics.swift +++ b/Sources/Networking/Components/Metrics.swift @@ -3,83 +3,83 @@ import Clocks import Foundation extension NetworkingComponent { - public func instrument() -> some NetworkingComponent { - networkEnvironment(\.instrument) { - @Dependency(\.continuousClock) var clock - return NetworkingInstrumentClient(NetworkingInstrument(clock: clock)) - } + public func instrument() -> some NetworkingComponent { + networkEnvironment(\.instrument) { + @Dependency(\.continuousClock) var clock + return NetworkingInstrumentClient(NetworkingInstrument(clock: clock)) } + } } private actor NetworkingInstrument { - - let clock: AnyClock - let start: ElapsedTimeMeasurement - var end: ElapsedTimeMeasurement? - var measurements: [ElapsedTimeMeasurement] = [] - - init(clock: any Clock) { - self.clock = AnyClock(clock) - self.start = .init(label: "start", duration: .zero, instant: AnyClock(clock).now) - } - - func measureElapsedTime(label: String) { - let now = clock.now - let previous = measurements.last?.instant ?? start.instant - let duration = previous.duration(to: now) - let measurement = ElapsedTimeMeasurement( - label: label, - duration: duration, - instant: now - ) - measurements.append(measurement) - - @NetworkEnvironment(\.logger) var logger - let total = measurements.total - logger?.info("⏱️ \(label) \(duration.description) total: \(total.description)") - - } + + let clock: AnyClock + let start: ElapsedTimeMeasurement + var end: ElapsedTimeMeasurement? + var measurements: [ElapsedTimeMeasurement] = [] + + init(clock: any Clock) { + self.clock = AnyClock(clock) + self.start = .init(label: "start", duration: .zero, instant: AnyClock(clock).now) + } + + func measureElapsedTime(label: String) { + let now = clock.now + let previous = measurements.last?.instant ?? start.instant + let duration = previous.duration(to: now) + let measurement = ElapsedTimeMeasurement( + label: label, + duration: duration, + instant: now + ) + measurements.append(measurement) + + @NetworkEnvironment(\.logger) var logger + let total = measurements.total + logger?.info("⏱️ \(label) \(duration.description) total: \(total.description)") + + } } public struct ElapsedTimeMeasurement: Equatable { - public let label: String - public let duration: Duration - let instant: AnyClock.Instant - - public init(label: String, duration: Duration, instant: AnyClock.Instant) { - self.label = label - self.duration = duration - self.instant = instant - } + public let label: String + public let duration: Duration + let instant: AnyClock.Instant + + public init(label: String, duration: Duration, instant: AnyClock.Instant) { + self.label = label + self.duration = duration + self.instant = instant + } } public struct NetworkingInstrumentClient { - public var elapsedTimeMeasurements: @Sendable () async -> [ElapsedTimeMeasurement] - public var measureElapsedTime: @Sendable (String) async -> Void + public var elapsedTimeMeasurements: @Sendable () async -> [ElapsedTimeMeasurement] + public var measureElapsedTime: @Sendable (String) async -> Void } extension [ElapsedTimeMeasurement] { - var total: Duration { - return map(\.duration).reduce(.zero, +) - } + var total: Duration { + return map(\.duration).reduce(.zero, +) + } } extension NetworkingInstrumentClient: NetworkEnvironmentKey { - fileprivate init(_ instrument: NetworkingInstrument) { - self.init( - elapsedTimeMeasurements: { - await instrument.measurements - }, - measureElapsedTime: { - await instrument.measureElapsedTime(label: $0) - } - ) - } + fileprivate init(_ instrument: NetworkingInstrument) { + self.init( + elapsedTimeMeasurements: { + await instrument.measurements + }, + measureElapsedTime: { + await instrument.measureElapsedTime(label: $0) + } + ) + } } extension NetworkEnvironmentValues { - public var instrument: NetworkingInstrumentClient? { - get { self[NetworkingInstrumentClient.self] } - set { self[NetworkingInstrumentClient.self] = newValue } - } + public var instrument: NetworkingInstrumentClient? { + get { self[NetworkingInstrumentClient.self] } + set { self[NetworkingInstrumentClient.self] = newValue } + } } diff --git a/Sources/Networking/Components/Numbered.swift b/Sources/Networking/Components/Numbered.swift index 4b0e02db..add6c3a3 100644 --- a/Sources/Networking/Components/Numbered.swift +++ b/Sources/Networking/Components/Numbered.swift @@ -1,40 +1,40 @@ extension NetworkingComponent { - public func numbered() -> some NetworkingComponent { - modified(Numbered()) - } + public func numbered() -> some NetworkingComponent { + modified(Numbered()) + } } public struct RequestSequence { - @TaskLocal - public static var number: Int = 0 + @TaskLocal + public static var number: Int = 0 } struct Numbered: NetworkingModifier { - private let sequence = SequenceNumber(value: 0) - - func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream { - ResponseStream { continuation in - Task { - await RequestSequence.$number.withValue(sequence.next()) { - await upstream.send(request).redirect(into: continuation) - } - } + private let sequence = SequenceNumber(value: 0) + + func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream { + ResponseStream { continuation in + Task { + await RequestSequence.$number.withValue(sequence.next()) { + await upstream.send(request).redirect(into: continuation) } + } } + } } actor SequenceNumber { - @TaskLocal - static var next: @Sendable (Int) -> Int = { $0 + 1 } - - private var value: Int - - init(value: Int) { - self.value = value - } - - func next() -> Int { - value = Self.next(value) - return value - } + @TaskLocal + static var next: @Sendable (Int) -> Int = { $0 + 1 } + + private var value: Int + + init(value: Int) { + self.value = value + } + + func next() -> Int { + value = Self.next(value) + return value + } } diff --git a/Sources/Networking/Components/Retry.swift b/Sources/Networking/Components/Retry.swift index 61705edc..88c0d61b 100644 --- a/Sources/Networking/Components/Retry.swift +++ b/Sources/Networking/Components/Retry.swift @@ -3,242 +3,242 @@ import Foundation import Helpers extension NetworkingComponent { - public func automaticRetry() -> some NetworkingComponent { - modified(Retry()) - } + public func automaticRetry() -> some NetworkingComponent { + modified(Retry()) + } } struct Retry: NetworkingModifier { - let data = RetryData() - - func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream { - guard request.supportsRetryingRequests else { - return upstream.send(request) - } - return ResponseStream { continuation in - Task { - await data - .send(upstream: upstream, request: request) - .redirect(into: continuation, onTermination: { - if await data.shouldMeasureElapsedTime { - @NetworkEnvironment(\.instrument) var instrument - await instrument?.measureElapsedTime("AutomaticRetry") - } - }) + let data = RetryData() + + func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream { + guard request.supportsRetryingRequests else { + return upstream.send(request) + } + return ResponseStream { continuation in + Task { + await data + .send(upstream: upstream, request: request) + .redirect(into: continuation, onTermination: { + if await data.shouldMeasureElapsedTime { + @NetworkEnvironment(\.instrument) var instrument + await instrument?.measureElapsedTime("AutomaticRetry") } - } + }) + } } + } } actor RetryData { - struct Attempt { - let request: HTTPRequestData - let result: Result - } - struct Value { - let original: HTTPRequestData - var attempts: [Attempt] = [] - } - - @Dependency(\.date) var date - @Dependency(\.calendar) var calendar - @Dependency(\.continuousClock) var clock - @NetworkEnvironment(\.logger) var logger - - private var data: [HTTPRequestData.ID: Value] = [:] - private(set) var shouldMeasureElapsedTime: Bool = false - - func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream { - return ResponseStream { continuation in - Task { - var progress = BytesReceived() - do { - for try await element in upstream.send(request).shared() { - progress = element.progress - continuation.yield(element) - } - cleanUp(request: request) - continuation.finish() - } catch { - do { - // Indicate that we should measure the elapsed time at the end - shouldMeasureElapsedTime = true - // Retry - try await retry( - request: request, - error: error, - upstream: upstream, - progress: progress - ) - .redirect(into: continuation) - } catch { - cleanUp(request: request) - continuation.finish(throwing: error) - } - } - } - } - } - - func retry( - request: HTTPRequestData, - error: Error, - upstream: NetworkingComponent, - progress: BytesReceived - ) async throws -> ResponseStream { - - // Check to see if the request has a retry strategy - guard let strategy = request.retryingStrategy else { - throw error - } - - // Figure out the original request id - let originalRequestID = request.retriedOriginalRequestID ?? request.id - - // Access the retry data - var data = self.data[originalRequestID] ?? Value(original: request, attempts: []) - - // Append a new Attempt value - let attempt = Attempt(request: request, result: .failure(error)) - data.attempts.append(attempt) - - // Saving the data back - self.data[originalRequestID] = data - - // Check to see that we have a delay - guard let delay = await strategy.retryDelay( - request: request, - after: data.attempts.map(\.result), - date: date(), - calendar: calendar - ) else { - throw error + struct Attempt { + let request: HTTPRequestData + let result: Result + } + struct Value { + let original: HTTPRequestData + var attempts: [Attempt] = [] + } + + @Dependency(\.date) var date + @Dependency(\.calendar) var calendar + @Dependency(\.continuousClock) var clock + @NetworkEnvironment(\.logger) var logger + + private var data: [HTTPRequestData.ID: Value] = [:] + private(set) var shouldMeasureElapsedTime: Bool = false + + func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream { + return ResponseStream { continuation in + Task { + var progress = BytesReceived() + do { + for try await element in upstream.send(request).shared() { + progress = element.progress + continuation.yield(element) + } + cleanUp(request: request) + continuation.finish() + } catch { + do { + // Indicate that we should measure the elapsed time at the end + shouldMeasureElapsedTime = true + // Retry + try await retry( + request: request, + error: error, + upstream: upstream, + progress: progress + ) + .redirect(into: continuation) + } catch { + cleanUp(request: request) + continuation.finish(throwing: error) + } } - - // Create a retry-copy of the request - let copy = request.retry() - - // Print some info to the logger - logger?.info("🤞 Retry \(originalRequestID) after \(String(describing: delay)).") - - // Return a new response stream - return ResponseStream { continuation in - Task { - // Update the progress to reset it - continuation.yield(.progress(BytesReceived(received: 0, expected: progress.expected))) - do { - // Delay sending the request - try await clock.sleep(for: delay) - } catch { - continuation.finish(throwing: error) - } - // Send the request - await self.send(upstream: upstream, request: copy) - .redirect(into: continuation) - } + } + } + } + + func retry( + request: HTTPRequestData, + error: Error, + upstream: NetworkingComponent, + progress: BytesReceived + ) async throws -> ResponseStream { + + // Check to see if the request has a retry strategy + guard let strategy = request.retryingStrategy else { + throw error + } + + // Figure out the original request id + let originalRequestID = request.retriedOriginalRequestID ?? request.id + + // Access the retry data + var data = self.data[originalRequestID] ?? Value(original: request, attempts: []) + + // Append a new Attempt value + let attempt = Attempt(request: request, result: .failure(error)) + data.attempts.append(attempt) + + // Saving the data back + self.data[originalRequestID] = data + + // Check to see that we have a delay + guard let delay = await strategy.retryDelay( + request: request, + after: data.attempts.map(\.result), + date: date(), + calendar: calendar + ) else { + throw error + } + + // Create a retry-copy of the request + let copy = request.retry() + + // Print some info to the logger + logger?.info("🤞 Retry \(originalRequestID) after \(String(describing: delay)).") + + // Return a new response stream + return ResponseStream { continuation in + Task { + // Update the progress to reset it + continuation.yield(.progress(BytesReceived(received: 0, expected: progress.expected))) + do { + // Delay sending the request + try await clock.sleep(for: delay) + } catch { + continuation.finish(throwing: error) } - } - - func cleanUp(request: HTTPRequestData) { - self.data.removeValue(forKey: request.retriedOriginalRequestID ?? request.id) - } + // Send the request + await self.send(upstream: upstream, request: copy) + .redirect(into: continuation) + } + } + } + + func cleanUp(request: HTTPRequestData) { + self.data.removeValue(forKey: request.retriedOriginalRequestID ?? request.id) + } } // MARK: Request Option public enum RetryingStrategyRequestOption: HTTPRequestDataOption { - public static var defaultOption: RetryingStrategy? = BackoffRetryStrategy - .constant(delay: .seconds(3), maxAttemptCount: 3) + public static var defaultOption: RetryingStrategy? = BackoffRetryStrategy + .constant(delay: .seconds(3), maxAttemptCount: 3) } struct RetriedOriginalRequestID: Equatable, HTTPRequestDataOption { - static var defaultOption: HTTPRequestData.ID? + static var defaultOption: HTTPRequestData.ID? } struct RetriedRequestID: Equatable, HTTPRequestDataOption { - static var defaultOption: HTTPRequestData.ID? + static var defaultOption: HTTPRequestData.ID? } extension HTTPRequestData { - public var retryingStrategy: RetryingStrategy? { - get { self[option: RetryingStrategyRequestOption.self] } - set { self[option: RetryingStrategyRequestOption.self] = newValue } - } - - var supportsRetryingRequests: Bool { - nil != retryingStrategy - } - - var retriedOriginalRequestID: HTTPRequestData.ID? { - get { self[option: RetriedOriginalRequestID.self] } - set { self[option: RetriedOriginalRequestID.self] = newValue } - } - - var retriedRequestID: HTTPRequestData.ID? { - get { self[option: RetriedRequestID.self] } - set { self[option: RetriedRequestID.self] = newValue } - } - - func retry() -> HTTPRequestData { - var retry = HTTPRequestData( - method: self.method, - scheme: self.scheme, - authority: self.authority, - path: self.path, - headerFields: self.headerFields, - body: body - ) - retry.copy(options: self.options) - retry.retriedOriginalRequestID = self.retriedOriginalRequestID ?? self.id - retry.retriedRequestID = self.id - return retry - } + public var retryingStrategy: RetryingStrategy? { + get { self[option: RetryingStrategyRequestOption.self] } + set { self[option: RetryingStrategyRequestOption.self] = newValue } + } + + var supportsRetryingRequests: Bool { + nil != retryingStrategy + } + + var retriedOriginalRequestID: HTTPRequestData.ID? { + get { self[option: RetriedOriginalRequestID.self] } + set { self[option: RetriedOriginalRequestID.self] = newValue } + } + + var retriedRequestID: HTTPRequestData.ID? { + get { self[option: RetriedRequestID.self] } + set { self[option: RetriedRequestID.self] = newValue } + } + + func retry() -> HTTPRequestData { + var retry = HTTPRequestData( + method: self.method, + scheme: self.scheme, + authority: self.authority, + path: self.path, + headerFields: self.headerFields, + body: body + ) + retry.copy(options: self.options) + retry.retriedOriginalRequestID = self.retriedOriginalRequestID ?? self.id + retry.retriedRequestID = self.id + return retry + } } // MARK: - Retrying Strategy public protocol RetryingStrategy { - func retryDelay( - request: HTTPRequestData, - after attempts: [Result], - date: Date, - calendar: Calendar - ) async -> Duration? + func retryDelay( + request: HTTPRequestData, + after attempts: [Result], + date: Date, + calendar: Calendar + ) async -> Duration? } public struct BackoffRetryStrategy: RetryingStrategy { - private var block: (HTTPRequestData, [Result], Date, Calendar) -> Duration? - public init(block: @escaping (HTTPRequestData, [Result], Date, Calendar) -> Duration?) { - self.block = block - } - - public static func constant(delay: Duration, maxAttemptCount: UInt) -> Self { - .init { _, attempts, _, _ in - guard attempts.count < maxAttemptCount else { return nil } - return delay - } - } - - public static func immediate(maxAttemptCount: UInt) -> Self { - constant(delay: .zero, maxAttemptCount: maxAttemptCount) - } - - public static func exponential(maxDelay: Duration = .seconds(300), maxAttemptCount: UInt) -> Self { - let interval: Double = 1 - let rate: Double = 2.0 - return .init { _, attempts, _, _ in - guard attempts.count < maxAttemptCount else { return nil } - let delay: Double = (interval * pow(rate, Double(attempts.count))) + .random(in: 0...0.001) - return min(.seconds(delay), maxDelay) - } - } - - public func retryDelay( - request: HTTPRequestData, - after attempts: [Result], - date: Date, - calendar: Calendar - ) async -> Duration? { - block(request, attempts, date, calendar) - } + private var block: (HTTPRequestData, [Result], Date, Calendar) -> Duration? + public init(block: @escaping (HTTPRequestData, [Result], Date, Calendar) -> Duration?) { + self.block = block + } + + public static func constant(delay: Duration, maxAttemptCount: UInt) -> Self { + .init { _, attempts, _, _ in + guard attempts.count < maxAttemptCount else { return nil } + return delay + } + } + + public static func immediate(maxAttemptCount: UInt) -> Self { + constant(delay: .zero, maxAttemptCount: maxAttemptCount) + } + + public static func exponential(maxDelay: Duration = .seconds(300), maxAttemptCount: UInt) -> Self { + let interval: Double = 1 + let rate: Double = 2.0 + return .init { _, attempts, _, _ in + guard attempts.count < maxAttemptCount else { return nil } + let delay: Double = (interval * pow(rate, Double(attempts.count))) + .random(in: 0...0.001) + return min(.seconds(delay), maxDelay) + } + } + + public func retryDelay( + request: HTTPRequestData, + after attempts: [Result], + date: Date, + calendar: Calendar + ) async -> Duration? { + block(request, attempts, date, calendar) + } } diff --git a/Sources/Networking/Components/Server.swift b/Sources/Networking/Components/Server.swift index 15183271..8974edbe 100644 --- a/Sources/Networking/Components/Server.swift +++ b/Sources/Networking/Components/Server.swift @@ -2,7 +2,7 @@ import HTTPTypes import os.log extension NetworkingComponent { - + public func server(authority: String?) -> some NetworkingComponent { server(mutate: \.authority) { _ in authority @@ -10,7 +10,7 @@ extension NetworkingComponent { logger?.info("💁 authority -> '\(authority ?? "no value")' \(request.debugDescription)") } } - + public func server(headerField name: HTTPField.Name, value: String?) -> some NetworkingComponent { server(mutate: \.headerFields) { headers in var copy = headers @@ -31,7 +31,7 @@ extension NetworkingComponent { // swiftlint:enable line_length } } - + public func server(prefixPath: String, delimiter: String = "/") -> some NetworkingComponent { server(mutate: \.path) { path in guard let path else { return prefixPath } @@ -40,7 +40,7 @@ extension NetworkingComponent { logger?.info("💁 prefix path -> '\(prefixPath)' \(request.debugDescription)") } } - + public func server( mutate keypath: WritableKeyPath, with transform: @escaping (Value) -> Value, @@ -52,7 +52,7 @@ extension NetworkingComponent { log(logger, request) } } - + public func server( _ mutateRequest: @escaping (inout HTTPRequestData) -> Void ) -> some NetworkingComponent { diff --git a/Sources/Networking/Components/Throttled.swift b/Sources/Networking/Components/Throttled.swift index 51b47361..8796e101 100644 --- a/Sources/Networking/Components/Throttled.swift +++ b/Sources/Networking/Components/Throttled.swift @@ -3,69 +3,69 @@ import Dependencies import Foundation public enum ThrottleOption: HTTPRequestDataOption { - public static var defaultOption: Self { .always } - case always, never + public static var defaultOption: Self { .always } + case always, never } extension HTTPRequestData { - public var throttle: ThrottleOption { - get { self[option: ThrottleOption.self] } - set { self[option: ThrottleOption.self] = newValue } - } + public var throttle: ThrottleOption { + get { self[option: ThrottleOption.self] } + set { self[option: ThrottleOption.self] = newValue } + } } extension NetworkingComponent { - public func throttled(max: UInt) -> some NetworkingComponent { - modified(Throttled(limit: max)) - } + public func throttled(max: UInt) -> some NetworkingComponent { + modified(Throttled(limit: max)) + } } struct Throttled: NetworkingModifier { - - let activeRequests = ActiveRequests() - let limit: UInt - - init(limit: UInt) { - self.limit = limit + + let activeRequests = ActiveRequests() + let limit: UInt + + init(limit: UInt) { + self.limit = limit + } + + func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream { + guard case .always = request.throttle else { + return upstream.send(request) } - - func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream { - guard case .always = request.throttle else { - return upstream.send(request) - } - return ResponseStream { continuation in - Task { - do { - try await activeRequests.send( - upstream: upstream, - request: request, - limit: self.limit - ) - .redirect(into: continuation) - } catch { - continuation.finish(throwing: error) - } - } + return ResponseStream { continuation in + Task { + do { + try await activeRequests.send( + upstream: upstream, + request: request, + limit: self.limit + ) + .redirect(into: continuation) + } catch { + continuation.finish(throwing: error) } + } } + } } extension ActiveRequests { - - fileprivate func send( - upstream: NetworkingComponent, - request: HTTPRequestData, - limit: UInt - ) async throws -> SharedStream { - guard active.count >= limit else { - return add(stream: upstream.send(request), for: request) - } - while active.count >= limit { - // Co-operative cancellation - try Task.checkCancellation() - // Yield - await Task.yield() - } - return add(stream: upstream.send(request), for: request) + + fileprivate func send( + upstream: NetworkingComponent, + request: HTTPRequestData, + limit: UInt + ) async throws -> SharedStream { + guard active.count >= limit else { + return add(stream: upstream.send(request), for: request) + } + while active.count >= limit { + // Co-operative cancellation + try Task.checkCancellation() + // Yield + await Task.yield() } + return add(stream: upstream.send(request), for: request) + } } diff --git a/Sources/Networking/Components/URLSession.swift b/Sources/Networking/Components/URLSession.swift index e7982ab6..2b447074 100644 --- a/Sources/Networking/Components/URLSession.swift +++ b/Sources/Networking/Components/URLSession.swift @@ -3,92 +3,92 @@ import Foundation import os.log extension URLSession: NetworkingComponent { - public func send(_ request: HTTPRequestData) -> ResponseStream { - ResponseStream { continuation in - Task { - @NetworkEnvironment(\.instrument) var instrument - guard let urlRequest = URLRequest(http: request) else { - continuation.finish(throwing: StackError.createURLRequestFailed(request)) - return - } - do { - await send(urlRequest) - .map { partial in - try partial.mapValue { data, response in - try HTTPResponseData(request: request, data: data, urlResponse: response) - } - } - .eraseToThrowingStream() - .redirect(into: continuation, onTermination: { - await instrument?.measureElapsedTime("URLSession") - }) - } catch { - await instrument?.measureElapsedTime("\(Self.self)") - continuation.finish(throwing: error) - } + public func send(_ request: HTTPRequestData) -> ResponseStream { + ResponseStream { continuation in + Task { + @NetworkEnvironment(\.instrument) var instrument + guard let urlRequest = URLRequest(http: request) else { + continuation.finish(throwing: StackError.createURLRequestFailed(request)) + return + } + do { + await send(urlRequest) + .map { partial in + try partial.mapValue { data, response in + try HTTPResponseData(request: request, data: data, urlResponse: response) + } } + .eraseToThrowingStream() + .redirect(into: continuation, onTermination: { + await instrument?.measureElapsedTime("URLSession") + }) + } catch { + await instrument?.measureElapsedTime("\(Self.self)") + continuation.finish(throwing: error) } + } } - - @Sendable func send(_ request: URLRequest) -> ResponseStream<(Data, URLResponse)> { - ResponseStream<(Data, URLResponse)> { continuation in - Task { - do { - // Define a buffer to download bytes into - let bufferSize = 16_384 // 16kB - var data = Data() - - // Get an AsyncBytes for the request - let (bytes, response) = try await self.bytes(for: request) - - // Co-operative cancellation - try Task.checkCancellation() - - // Track the progress of bytes received - var progress = BytesReceived(expected: response.expectedContentLength) - var bufferCount = 0 - - // Configure the buffer - data.reserveCapacity(min(bufferSize, Int(progress.expected))) - - // Iterate through the bytes - for try await byte in bytes { - - // Fill up the in-memory buffer with bytes - data.append(byte) - - // Count how many bytes have been received - progress.receiveBytes(count: 1) - bufferCount += 1 - - // Check to see if we've reached the buffer size - if bufferCount >= bufferSize { - - // Co-operative cancellation - try Task.checkCancellation() - - // Yield progress - continuation.yield(.progress(progress)) - - // Reset the buffer count - bufferCount = 0 - } - } // End of for-await-in bytes - - // Yield progress - continuation.yield(.progress(progress)) - - // Yield the value - continuation.yield( - .value((data, response), progress) - ) - - continuation.finish() - - } catch { - continuation.finish(throwing: error) - } + } + + @Sendable func send(_ request: URLRequest) -> ResponseStream<(Data, URLResponse)> { + ResponseStream<(Data, URLResponse)> { continuation in + Task { + do { + // Define a buffer to download bytes into + let bufferSize = 16_384 // 16kB + var data = Data() + + // Get an AsyncBytes for the request + let (bytes, response) = try await self.bytes(for: request) + + // Co-operative cancellation + try Task.checkCancellation() + + // Track the progress of bytes received + var progress = BytesReceived(expected: response.expectedContentLength) + var bufferCount = 0 + + // Configure the buffer + data.reserveCapacity(min(bufferSize, Int(progress.expected))) + + // Iterate through the bytes + for try await byte in bytes { + + // Fill up the in-memory buffer with bytes + data.append(byte) + + // Count how many bytes have been received + progress.receiveBytes(count: 1) + bufferCount += 1 + + // Check to see if we've reached the buffer size + if bufferCount >= bufferSize { + + // Co-operative cancellation + try Task.checkCancellation() + + // Yield progress + continuation.yield(.progress(progress)) + + // Reset the buffer count + bufferCount = 0 } + } // End of for-await-in bytes + + // Yield progress + continuation.yield(.progress(progress)) + + // Yield the value + continuation.yield( + .value((data, response), progress) + ) + + continuation.finish() + + } catch { + continuation.finish(throwing: error) } + } } + } } diff --git a/Sources/Networking/Core/ActiveRequests.swift b/Sources/Networking/Core/ActiveRequests.swift index 10c0b105..d66a19e2 100644 --- a/Sources/Networking/Core/ActiveRequests.swift +++ b/Sources/Networking/Core/ActiveRequests.swift @@ -2,38 +2,38 @@ import ConcurrencyExtras import Helpers public actor ActiveRequests { - public typealias SharedStream = SharedAsyncSequence> - public struct Key: Hashable { - public let id: HTTPRequestData.ID - public let number = RequestSequence.number - } - public struct Value { - public let request: HTTPRequestData - let stream: SharedStream - } - - public private(set) var active: [Key: Value] = [:] - public var shouldMeasureElapsedTime: Bool = false - public var count: Int { active.count } - - @discardableResult - public func add( - stream: ResponseStream, - for request: HTTPRequestData - ) -> SharedStream { - let shared = ResponseStream { continuation in - Task { - await stream.redirect(into: continuation, onTermination: { @Sendable in - await self.removeStream(for: request) - }) - } - }.shared() - - active[Key(id: request.id)] = Value(request: request, stream: shared) - return shared - } - - public func removeStream(for request: HTTPRequestData) { - active[Key(id: request.id)] = nil - } + public typealias SharedStream = SharedAsyncSequence> + public struct Key: Hashable { + public let id: HTTPRequestData.ID + public let number = RequestSequence.number + } + public struct Value { + public let request: HTTPRequestData + let stream: SharedStream + } + + public private(set) var active: [Key: Value] = [:] + public var shouldMeasureElapsedTime: Bool = false + public var count: Int { active.count } + + @discardableResult + public func add( + stream: ResponseStream, + for request: HTTPRequestData + ) -> SharedStream { + let shared = ResponseStream { continuation in + Task { + await stream.redirect(into: continuation, onTermination: { @Sendable in + await self.removeStream(for: request) + }) + } + }.shared() + + active[Key(id: request.id)] = Value(request: request, stream: shared) + return shared + } + + public func removeStream(for request: HTTPRequestData) { + active[Key(id: request.id)] = nil + } } diff --git a/Sources/Networking/Core/BytesReceived.swift b/Sources/Networking/Core/BytesReceived.swift index bb56773a..4d12f6c2 100644 --- a/Sources/Networking/Core/BytesReceived.swift +++ b/Sources/Networking/Core/BytesReceived.swift @@ -1,48 +1,48 @@ import Foundation public struct BytesReceived: Sendable, Hashable { - public internal(set) var received: Int64 - public internal(set) var expected: Int64 - - public var fractionCompleted: Double { - max(0.0, min(Double(received) / Double(expected), 1.0)) - } - - public init( - received: Int64 = 0, - expected: Int64 = 0 - ) { - self.received = received - self.expected = expected - } - - public init(data: Data) { - let count = Int64(data.count) - self.init(received: count, expected: count) - } - - public mutating func receiveBytes(count: Int64) { - received += count - } - - public func withExpectedContentLength(from request: HTTPRequestData) -> BytesReceived { - BytesReceived( - received: received, - expected: max(request.expectedContentLength ?? 0, expected) - ) - } - - public func withExpectedBytes(from response: HTTPResponseData) -> BytesReceived { - BytesReceived( - received: received, - expected: max(Int64(response.data.count), expected) - ) - } + public internal(set) var received: Int64 + public internal(set) var expected: Int64 + + public var fractionCompleted: Double { + max(0.0, min(Double(received) / Double(expected), 1.0)) + } + + public init( + received: Int64 = 0, + expected: Int64 = 0 + ) { + self.received = received + self.expected = expected + } + + public init(data: Data) { + let count = Int64(data.count) + self.init(received: count, expected: count) + } + + public mutating func receiveBytes(count: Int64) { + received += count + } + + public func withExpectedContentLength(from request: HTTPRequestData) -> BytesReceived { + BytesReceived( + received: received, + expected: max(request.expectedContentLength ?? 0, expected) + ) + } + + public func withExpectedBytes(from response: HTTPResponseData) -> BytesReceived { + BytesReceived( + received: received, + expected: max(Int64(response.data.count), expected) + ) + } } func + (lhs: BytesReceived, rhs: BytesReceived) -> BytesReceived { - BytesReceived( - received: lhs.received + rhs.received, - expected: lhs.expected + rhs.expected - ) + BytesReceived( + received: lhs.received + rhs.received, + expected: lhs.expected + rhs.expected + ) } diff --git a/Sources/Networking/Core/HTTPRequestBody.swift b/Sources/Networking/Core/HTTPRequestBody.swift index 7626f1f1..74a051b0 100644 --- a/Sources/Networking/Core/HTTPRequestBody.swift +++ b/Sources/Networking/Core/HTTPRequestBody.swift @@ -2,21 +2,21 @@ import Foundation import HTTPTypes public protocol HTTPRequestBody { - var isEmpty: Bool { get } - var additionalHeaders: HTTPFields { get } - func encode() throws -> Data + var isEmpty: Bool { get } + var additionalHeaders: HTTPFields { get } + func encode() throws -> Data } extension HTTPRequestBody { - public var isEmpty: Bool { false } - public var isNotEmpty: Bool { false == isEmpty } - public var additionalHeaders: HTTPFields { [:] } + public var isEmpty: Bool { false } + public var isNotEmpty: Bool { false == isEmpty } + public var additionalHeaders: HTTPFields { [:] } } extension HTTPFields { - mutating func append(_ other: Self) { - for field in other { - self[fields: field.name].append(field) - } + mutating func append(_ other: Self) { + for field in other { + self[fields: field.name].append(field) } + } } diff --git a/Sources/Networking/Core/HTTPRequestData.swift b/Sources/Networking/Core/HTTPRequestData.swift index 9abb9fde..548bbd3e 100644 --- a/Sources/Networking/Core/HTTPRequestData.swift +++ b/Sources/Networking/Core/HTTPRequestData.swift @@ -9,210 +9,210 @@ import ShortID @dynamicMemberLookup public struct HTTPRequestData: Sendable, Identifiable { - public typealias ID = Tagged - public let id: ID - public var body: Data? - - @Sanitized fileprivate var request: HTTPRequest - internal fileprivate(set) var options: [ObjectIdentifier: HTTPRequestDataOptionContainer] = [:] - - public var identifier: String { - id.rawValue - } - - public subscript( - dynamicMember dynamicMember: WritableKeyPath - ) -> Value { - get { $request[keyPath: dynamicMember] } - set { $request[keyPath: dynamicMember] = newValue } - } - - init( - id: ID, - method: HTTPRequest.Method = .get, - scheme: String? = "https", - authority: String? = nil, - path: String? = nil, - headerFields: HTTPFields = [:], - body: Data? = nil - ) { - self.id = id - self.body = body - self._request = .init(projectedValue: .init( - method: method, - scheme: scheme, - authority: authority, - path: path, - headerFields: headerFields - )) - } - - public init( - method: HTTPRequest.Method = .get, - scheme: String? = "https", - authority: String? = nil, - path: String? = nil, - headerFields: HTTPFields = [:], - body: Data? = nil - ) { - @Dependency(\.shortID) var shortID - self.init( - id: .init(shortID().description), - method: method, - scheme: scheme, - authority: authority, - path: path, - headerFields: headerFields, - body: body - ) - } - - public init( - method: HTTPRequest.Method = .get, - scheme: String? = "https", - authority: String? = nil, - path: String? = nil, - headerFields: HTTPFields = [:], - body: any HTTPRequestBody - ) throws { - var fields = headerFields - let data: Data? = try { - guard body.isNotEmpty else { - return nil - } - fields.append(body.additionalHeaders) - return try body.encode() - }() - self.init( - method: method, - scheme: scheme, - authority: authority, - path: path, - headerFields: fields, - body: data - ) - } + public typealias ID = Tagged + public let id: ID + public var body: Data? + + @Sanitized fileprivate var request: HTTPRequest + internal fileprivate(set) var options: [ObjectIdentifier: HTTPRequestDataOptionContainer] = [:] + + public var identifier: String { + id.rawValue + } + + public subscript( + dynamicMember dynamicMember: WritableKeyPath + ) -> Value { + get { $request[keyPath: dynamicMember] } + set { $request[keyPath: dynamicMember] = newValue } + } + + init( + id: ID, + method: HTTPRequest.Method = .get, + scheme: String? = "https", + authority: String? = nil, + path: String? = nil, + headerFields: HTTPFields = [:], + body: Data? = nil + ) { + self.id = id + self.body = body + self._request = .init(projectedValue: .init( + method: method, + scheme: scheme, + authority: authority, + path: path, + headerFields: headerFields + )) + } + + public init( + method: HTTPRequest.Method = .get, + scheme: String? = "https", + authority: String? = nil, + path: String? = nil, + headerFields: HTTPFields = [:], + body: Data? = nil + ) { + @Dependency(\.shortID) var shortID + self.init( + id: .init(shortID().description), + method: method, + scheme: scheme, + authority: authority, + path: path, + headerFields: headerFields, + body: body + ) + } + + public init( + method: HTTPRequest.Method = .get, + scheme: String? = "https", + authority: String? = nil, + path: String? = nil, + headerFields: HTTPFields = [:], + body: any HTTPRequestBody + ) throws { + var fields = headerFields + let data: Data? = try { + guard body.isNotEmpty else { + return nil + } + fields.append(body.additionalHeaders) + return try body.encode() + }() + self.init( + method: method, + scheme: scheme, + authority: authority, + path: path, + headerFields: fields, + body: data + ) + } } // MARK: - Options extension HTTPRequestData { - public subscript(option optionType: Option.Type) -> Option.Value { - get { - let id = ObjectIdentifier(optionType) - guard let container = options[id], let value = container.value as? Option.Value else { - return optionType.defaultOption - } - return value - } - set { - let id = ObjectIdentifier(optionType) - options[id] = HTTPRequestDataOptionContainer( - newValue, - isEqualTo: { other in - guard let other else { - return false == optionType.includeInEqualityEvaluation - } - return optionType.includeInEqualityEvaluation - ? _isEqual(newValue, other) - : true - }) - } - } - - internal mutating func copy(options other: [ObjectIdentifier: HTTPRequestDataOptionContainer]) { - self.options = other - } + public subscript(option optionType: Option.Type) -> Option.Value { + get { + let id = ObjectIdentifier(optionType) + guard let container = options[id], let value = container.value as? Option.Value else { + return optionType.defaultOption + } + return value + } + set { + let id = ObjectIdentifier(optionType) + options[id] = HTTPRequestDataOptionContainer( + newValue, + isEqualTo: { other in + guard let other else { + return false == optionType.includeInEqualityEvaluation + } + return optionType.includeInEqualityEvaluation + ? _isEqual(newValue, other) + : true + }) + } + } + + internal mutating func copy(options other: [ObjectIdentifier: HTTPRequestDataOptionContainer]) { + self.options = other + } } // MARK: - Conformances extension HTTPRequestData: Equatable { - public static func == (lhs: HTTPRequestData, rhs: HTTPRequestData) -> Bool { - lhs.id == rhs.id - && lhs.body == rhs.body - && lhs.request == rhs.request - && lhs.options.allSatisfy { key, lhs in - return lhs.isEqualTo(rhs.options[key]?.value) - } - && rhs.options.allSatisfy { key, rhs in - return rhs.isEqualTo(lhs.options[key]?.value) - } + public static func == (lhs: HTTPRequestData, rhs: HTTPRequestData) -> Bool { + lhs.id == rhs.id + && lhs.body == rhs.body + && lhs.request == rhs.request + && lhs.options.allSatisfy { key, lhs in + return lhs.isEqualTo(rhs.options[key]?.value) } + && rhs.options.allSatisfy { key, rhs in + return rhs.isEqualTo(lhs.options[key]?.value) + } + } } extension HTTPRequestData: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(body) - hasher.combine(request) - } + public func hash(into hasher: inout Hasher) { + hasher.combine(body) + hasher.combine(request) + } } extension HTTPRequestData: CustomDebugStringConvertible { - public var debugDescription: String { - "[\(RequestSequence.number):\(identifier)] \(request.debugDescription)" - } + public var debugDescription: String { + "[\(RequestSequence.number):\(identifier)] \(request.debugDescription)" + } } // MARK: - Pattern Match public func ~= (lhs: HTTPRequestData, rhs: HTTPRequestData) -> Bool { - lhs.body == rhs.body - && (lhs.request == rhs.request) - && lhs.options.allSatisfy { key, lhs in - return lhs.isEqualTo(rhs.options[key]?.value) - } - && rhs.options.allSatisfy { key, rhs in - return rhs.isEqualTo(lhs.options[key]?.value) - } + lhs.body == rhs.body + && (lhs.request == rhs.request) + && lhs.options.allSatisfy { key, lhs in + return lhs.isEqualTo(rhs.options[key]?.value) + } + && rhs.options.allSatisfy { key, rhs in + return rhs.isEqualTo(lhs.options[key]?.value) + } } // MARK: - Sanitize @propertyWrapper private struct Sanitized { - var projectedValue: HTTPRequest - var wrappedValue: HTTPRequest { - projectedValue.sanitized() - } + var projectedValue: HTTPRequest + var wrappedValue: HTTPRequest { + projectedValue.sanitized() + } } extension HTTPRequest { - fileprivate func sanitized() -> Self { - var copy = self - copy.sanitize() - return copy - } - - fileprivate mutating func sanitize() { - // Trim any trailing / from authority - authority = authority?.trimSlashSuffix() - // Ensure there is a single / on the path - if let trimmedPath = path?.trimSlashPrefix(), !trimmedPath.isEmpty { - path = "/" + trimmedPath - } - - if let path, path.isEmpty { - self.path = "/" - } else if nil == path { - self.path = "/" - } - } + fileprivate func sanitized() -> Self { + var copy = self + copy.sanitize() + return copy + } + + fileprivate mutating func sanitize() { + // Trim any trailing / from authority + authority = authority?.trimSlashSuffix() + // Ensure there is a single / on the path + if let trimmedPath = path?.trimSlashPrefix(), !trimmedPath.isEmpty { + path = "/" + trimmedPath + } + + if let path, path.isEmpty { + self.path = "/" + } else if nil == path { + self.path = "/" + } + } } extension String { - fileprivate mutating func trimSlashSuffix() -> String { - String(self.trimmingSuffix(while: { $0 == "/" })) - } - fileprivate mutating func trimSlashPrefix() -> String { - String(self.trimmingPrefix(while: { $0 == "/" })) - } + fileprivate mutating func trimSlashSuffix() -> String { + String(self.trimmingSuffix(while: { $0 == "/" })) + } + fileprivate mutating func trimSlashPrefix() -> String { + String(self.trimmingPrefix(while: { $0 == "/" })) + } } // MARK: - Foundation extension URLRequest { - public init?(http: HTTPRequestData) { - self.init(httpRequest: http.request) - } + public init?(http: HTTPRequestData) { + self.init(httpRequest: http.request) + } } diff --git a/Sources/Networking/Core/HTTPRequestDataOption.swift b/Sources/Networking/Core/HTTPRequestDataOption.swift index 248009de..734423a3 100644 --- a/Sources/Networking/Core/HTTPRequestDataOption.swift +++ b/Sources/Networking/Core/HTTPRequestDataOption.swift @@ -1,23 +1,23 @@ import Foundation public protocol HTTPRequestDataOption { - associatedtype Value - static var defaultOption: Value { get } - static var includeInEqualityEvaluation: Bool { get } + associatedtype Value + static var defaultOption: Value { get } + static var includeInEqualityEvaluation: Bool { get } } extension HTTPRequestDataOption { - public static var includeInEqualityEvaluation: Bool { - false - } + public static var includeInEqualityEvaluation: Bool { + false + } } struct HTTPRequestDataOptionContainer: @unchecked Sendable { - let value: Any - let isEqualTo: (Any?) -> Bool - - init(_ value: Any, isEqualTo: @escaping (Any?) -> Bool) { - self.value = value - self.isEqualTo = isEqualTo - } + let value: Any + let isEqualTo: (Any?) -> Bool + + init(_ value: Any, isEqualTo: @escaping (Any?) -> Bool) { + self.value = value + self.isEqualTo = isEqualTo + } } diff --git a/Sources/Networking/Core/HTTPResponseData.swift b/Sources/Networking/Core/HTTPResponseData.swift index 64d75229..4f12e8ec 100644 --- a/Sources/Networking/Core/HTTPResponseData.swift +++ b/Sources/Networking/Core/HTTPResponseData.swift @@ -6,129 +6,129 @@ import HTTPTypesFoundation @dynamicMemberLookup public struct HTTPResponseData: Sendable { - public let request: HTTPRequestData - public let data: Data - private let _response: HTTPResponse - - public subscript( - dynamicMember dynamicMemberLookup: KeyPath - ) -> Value { - _response[keyPath: dynamicMemberLookup] + public let request: HTTPRequestData + public let data: Data + private let _response: HTTPResponse + + public subscript( + dynamicMember dynamicMemberLookup: KeyPath + ) -> Value { + _response[keyPath: dynamicMemberLookup] + } + + internal fileprivate(set) var metadata: [ObjectIdentifier: HTTPResponseMetadataContainer] = [:] + + public init(request: HTTPRequestData, data: Data, response: HTTPResponse) { + self.request = request + self.data = data + self._response = response + } + + public init(request: HTTPRequestData, data: Data, urlResponse: URLResponse?) throws { + guard let response = (urlResponse as? HTTPURLResponse)?.httpResponse else { + throw StackError.invalidURLResponse(request, data, urlResponse) } - - internal fileprivate(set) var metadata: [ObjectIdentifier: HTTPResponseMetadataContainer] = [:] - - public init(request: HTTPRequestData, data: Data, response: HTTPResponse) { - self.request = request - self.data = data - self._response = response - } - - public init(request: HTTPRequestData, data: Data, urlResponse: URLResponse?) throws { - guard let response = (urlResponse as? HTTPURLResponse)?.httpResponse else { - throw StackError.invalidURLResponse(request, data, urlResponse) - } - self.init(request: request, data: data, response: response) - } - - func decode( - as payloadType: Payload.Type, - decoder: Decoder, - transform: @Sendable (Payload, Self) throws -> Body - ) throws -> Body where Decoder.Input == Data { - do { - let payload = try decoder.decode(payloadType, from: data) - let body = try transform(payload, self) - return body - } catch let error as DecodingError { - throw StackError.decodeResponse(self, error) - } + self.init(request: request, data: data, response: response) + } + + func decode( + as payloadType: Payload.Type, + decoder: Decoder, + transform: @Sendable (Payload, Self) throws -> Body + ) throws -> Body where Decoder.Input == Data { + do { + let payload = try decoder.decode(payloadType, from: data) + let body = try transform(payload, self) + return body + } catch let error as DecodingError { + throw StackError.decodeResponse(self, error) } + } } // MARK: - Metadata extension HTTPResponseData { - public subscript(metadata metadataType: Metadata.Type) -> Metadata.Value { - get { - let id = ObjectIdentifier(metadataType) - guard let container = metadata[id], let value = container.value as? Metadata.Value else { - return metadataType.defaultMetadata - } - return value - } - set { - let id = ObjectIdentifier(metadataType) - metadata[id] = HTTPResponseMetadataContainer( - newValue, - isEqualTo: { other in - guard let other else { - return false == metadataType.includeInEqualityEvaluation - } - return metadataType.includeInEqualityEvaluation - ? _isEqual(newValue, other) - : true - }) - } + public subscript(metadata metadataType: Metadata.Type) -> Metadata.Value { + get { + let id = ObjectIdentifier(metadataType) + guard let container = metadata[id], let value = container.value as? Metadata.Value else { + return metadataType.defaultMetadata + } + return value } - - internal mutating func copy(metadata other: [ObjectIdentifier: HTTPResponseMetadataContainer]) { - self.metadata = other + set { + let id = ObjectIdentifier(metadataType) + metadata[id] = HTTPResponseMetadataContainer( + newValue, + isEqualTo: { other in + guard let other else { + return false == metadataType.includeInEqualityEvaluation + } + return metadataType.includeInEqualityEvaluation + ? _isEqual(newValue, other) + : true + }) } + } + + internal mutating func copy(metadata other: [ObjectIdentifier: HTTPResponseMetadataContainer]) { + self.metadata = other + } } // MARK: - Conformances extension HTTPResponseData: Equatable { - public static func == (lhs: HTTPResponseData, rhs: HTTPResponseData) -> Bool { - lhs.request == rhs.request - && lhs.data == rhs.data - && lhs._response == rhs._response - && lhs.metadata.allSatisfy { key, lhs in - return lhs.isEqualTo(rhs.metadata[key]?.value) - } - && rhs.metadata.allSatisfy { key, rhs in - return rhs.isEqualTo(lhs.metadata[key]?.value) - } + public static func == (lhs: HTTPResponseData, rhs: HTTPResponseData) -> Bool { + lhs.request == rhs.request + && lhs.data == rhs.data + && lhs._response == rhs._response + && lhs.metadata.allSatisfy { key, lhs in + return lhs.isEqualTo(rhs.metadata[key]?.value) + } + && rhs.metadata.allSatisfy { key, rhs in + return rhs.isEqualTo(lhs.metadata[key]?.value) } + } } extension HTTPResponseData: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(request) - hasher.combine(data) - hasher.combine(_response) - } + public func hash(into hasher: inout Hasher) { + hasher.combine(request) + hasher.combine(data) + hasher.combine(_response) + } } extension HTTPResponseData: CustomDebugStringConvertible { - public var debugDescription: String { - var debugDescription = "\(self.status.description)" - if data.isEmpty { - debugDescription += " No Data" - } else { - if let contentType = self.headerFields[.contentType] { - debugDescription += "\(contentType.description)" - #if hasFeature(BareSlashRegexLiterals) - let regex = /(json)/ - #else - let regex = #/(json)/# - #endif - if contentType.contains(regex) { - let dataDescription = String(decoding: data, as: UTF8.self) - debugDescription += "\n\(dataDescription)" - } - } + public var debugDescription: String { + var debugDescription = "\(self.status.description)" + if data.isEmpty { + debugDescription += " No Data" + } else { + if let contentType = self.headerFields[.contentType] { + debugDescription += "\(contentType.description)" +#if hasFeature(BareSlashRegexLiterals) + let regex = /(json)/ +#else + let regex = #/(json)/# +#endif + if contentType.contains(regex) { + let dataDescription = String(decoding: data, as: UTF8.self) + debugDescription += "\n\(dataDescription)" } - return debugDescription + } } + return debugDescription + } } // MARK: - Conveniences extension HTTPResponse.Status { - public var isFailure: Bool { - Self.badRequest.code <= code - } + public var isFailure: Bool { + Self.badRequest.code <= code + } } diff --git a/Sources/Networking/Core/HTTPResponseMetadata.swift b/Sources/Networking/Core/HTTPResponseMetadata.swift index 68701e58..d04e17c9 100644 --- a/Sources/Networking/Core/HTTPResponseMetadata.swift +++ b/Sources/Networking/Core/HTTPResponseMetadata.swift @@ -1,23 +1,23 @@ import Foundation public protocol HTTPResponseMetadata { - associatedtype Value - static var defaultMetadata: Value { get } - static var includeInEqualityEvaluation: Bool { get } + associatedtype Value + static var defaultMetadata: Value { get } + static var includeInEqualityEvaluation: Bool { get } } extension HTTPResponseMetadata { - public static var includeInEqualityEvaluation: Bool { - false - } + public static var includeInEqualityEvaluation: Bool { + false + } } struct HTTPResponseMetadataContainer: @unchecked Sendable { - let value: Any - let isEqualTo: (Any?) -> Bool - - init(_ value: Any, isEqualTo: @escaping (Any?) -> Bool) { - self.value = value - self.isEqualTo = isEqualTo - } + let value: Any + let isEqualTo: (Any?) -> Bool + + init(_ value: Any, isEqualTo: @escaping (Any?) -> Bool) { + self.value = value + self.isEqualTo = isEqualTo + } } diff --git a/Sources/Networking/Core/NetworkEnvironment.swift b/Sources/Networking/Core/NetworkEnvironment.swift index 21fcd480..b73dab95 100644 --- a/Sources/Networking/Core/NetworkEnvironment.swift +++ b/Sources/Networking/Core/NetworkEnvironment.swift @@ -1,81 +1,81 @@ import Foundation extension NetworkingComponent { - func networkEnvironment( - _ keyPath: WritableKeyPath, - _ value: @escaping () -> Value - ) -> some NetworkingComponent { - modified(NetworkEnvironmentWritingModifier(keyPath: keyPath, value: value)) - } + func networkEnvironment( + _ keyPath: WritableKeyPath, + _ value: @escaping () -> Value + ) -> some NetworkingComponent { + modified(NetworkEnvironmentWritingModifier(keyPath: keyPath, value: value)) + } } private struct NetworkEnvironmentWritingModifier< - Value: Sendable + Value: Sendable >: NetworkingModifier { - let keyPath: WritableKeyPath - let value: () -> Value - init( - keyPath: WritableKeyPath, - value: @escaping () -> Value - ) { - self.keyPath = keyPath - self.value = value - } - func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream { - var values = NetworkEnvironmentValues.environmentValues - values[keyPath: keyPath] = value() - return NetworkEnvironmentValues.$environmentValues.withValue(values) { - upstream.send(request) - } + let keyPath: WritableKeyPath + let value: () -> Value + init( + keyPath: WritableKeyPath, + value: @escaping () -> Value + ) { + self.keyPath = keyPath + self.value = value + } + func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream { + var values = NetworkEnvironmentValues.environmentValues + values[keyPath: keyPath] = value() + return NetworkEnvironmentValues.$environmentValues.withValue(values) { + upstream.send(request) } + } } public protocol NetworkEnvironmentKey { - associatedtype Value: Sendable = Self + associatedtype Value: Sendable = Self } @propertyWrapper public struct NetworkEnvironment: @unchecked Sendable { - private let keyPath: KeyPath - - public var wrappedValue: Value { - NetworkEnvironmentValues.environmentValues[keyPath: keyPath] - } - - public init( - _ keyPath: KeyPath - ) { - self.keyPath = keyPath - } + private let keyPath: KeyPath + + public var wrappedValue: Value { + NetworkEnvironmentValues.environmentValues[keyPath: keyPath] + } + + public init( + _ keyPath: KeyPath + ) { + self.keyPath = keyPath + } } public struct NetworkEnvironmentValues: Sendable { - @TaskLocal public static var environmentValues = Self() - private var storage: [ObjectIdentifier: AnySendable] = [:] - - public subscript( - key: Key.Type - ) -> Key.Value? where Key.Value: Sendable { - get { - guard - let base = self.storage[ObjectIdentifier(key)]?.base, - let value = base as? Key.Value - else { - return nil - } - return value - } - set { - self.storage[ObjectIdentifier(key)] = AnySendable(newValue) - } + @TaskLocal public static var environmentValues = Self() + private var storage: [ObjectIdentifier: AnySendable] = [:] + + public subscript( + key: Key.Type + ) -> Key.Value? where Key.Value: Sendable { + get { + guard + let base = self.storage[ObjectIdentifier(key)]?.base, + let value = base as? Key.Value + else { + return nil + } + return value } + set { + self.storage[ObjectIdentifier(key)] = AnySendable(newValue) + } + } } private struct AnySendable: @unchecked Sendable { - let base: Any - @inlinable - init(_ base: Base) { - self.base = base - } + let base: Any + @inlinable + init(_ base: Base) { + self.base = base + } } diff --git a/Sources/Networking/Core/NetworkingComponent.swift b/Sources/Networking/Core/NetworkingComponent.swift index 6ec145ff..3b0314b5 100644 --- a/Sources/Networking/Core/NetworkingComponent.swift +++ b/Sources/Networking/Core/NetworkingComponent.swift @@ -8,74 +8,74 @@ import Helpers /// /// It has a single requirement for sending a networking request, and receiving a stream of events back. public protocol NetworkingComponent { - func send(_ request: HTTPRequestData) -> ResponseStream + func send(_ request: HTTPRequestData) -> ResponseStream } public typealias ResponseStream = AsyncThrowingStream, Error> extension NetworkingComponent { - @discardableResult - public func data( - _ request: HTTPRequestData, - progress updateProgress: @escaping @Sendable (BytesReceived) async -> Void = { _ in }, - timeout duration: Duration, - using clock: @autoclosure () -> any Clock - ) async throws -> HTTPResponseData { - do { - try Task.checkCancellation() - return try await send(request) - .compactMap { element in - await updateProgress(element.progress) - return element.value - } - .first(beforeTimeout: duration, using: clock()) - } catch is TimeoutError { - throw StackError.timeout(request) + @discardableResult + public func data( + _ request: HTTPRequestData, + progress updateProgress: @escaping @Sendable (BytesReceived) async -> Void = { _ in }, + timeout duration: Duration, + using clock: @autoclosure () -> any Clock + ) async throws -> HTTPResponseData { + do { + try Task.checkCancellation() + return try await send(request) + .compactMap { element in + await updateProgress(element.progress) + return element.value } + .first(beforeTimeout: duration, using: clock()) + } catch is TimeoutError { + throw StackError.timeout(request) } + } - @discardableResult - public func data( - _ request: HTTPRequestData, - progress updateProgress: @escaping @Sendable (BytesReceived) async -> Void = { _ in }, - timeout duration: Duration - ) async throws -> HTTPResponseData { - try await data( - request, - progress: updateProgress, - timeout: duration, - using: Dependency(\.continuousClock).wrappedValue - ) - } + @discardableResult + public func data( + _ request: HTTPRequestData, + progress updateProgress: @escaping @Sendable (BytesReceived) async -> Void = { _ in }, + timeout duration: Duration + ) async throws -> HTTPResponseData { + try await data( + request, + progress: updateProgress, + timeout: duration, + using: Dependency(\.continuousClock).wrappedValue + ) + } - @discardableResult - public func data( - _ request: HTTPRequestData, - progress updateProgress: @escaping @Sendable (BytesReceived) async -> Void = { _ in } - ) async throws -> HTTPResponseData { - try await data(request, progress: updateProgress, timeout: .seconds(request.requestTimeoutInSeconds)) - } + @discardableResult + public func data( + _ request: HTTPRequestData, + progress updateProgress: @escaping @Sendable (BytesReceived) async -> Void = { _ in } + ) async throws -> HTTPResponseData { + try await data(request, progress: updateProgress, timeout: .seconds(request.requestTimeoutInSeconds)) + } } // MARK: - Codable Support extension NetworkingComponent { - public func value( - _ request: HTTPRequestData, - as bodyType: Body.Type, - decoder specializedDecoder: Decoder - ) async throws -> (body: Body, response: HTTPResponseData) where Decoder.Input == Data { - try await value(Request(http: request, decoder: specializedDecoder)) - } + public func value( + _ request: HTTPRequestData, + as bodyType: Body.Type, + decoder specializedDecoder: Decoder + ) async throws -> (body: Body, response: HTTPResponseData) where Decoder.Input == Data { + try await value(Request(http: request, decoder: specializedDecoder)) + } - public func value( - _ request: Request - ) async throws -> (body: Body, response: HTTPResponseData) { - let response = try await data(request.http) - try Task.checkCancellation() - let body = try request.decode(response) - return (body, response) - } + public func value( + _ request: Request + ) async throws -> (body: Body, response: HTTPResponseData) { + let response = try await data(request.http) + try Task.checkCancellation() + let body = try request.decode(response) + return (body, response) + } } diff --git a/Sources/Networking/Core/NetworkingModifier.swift b/Sources/Networking/Core/NetworkingModifier.swift index 934fbf19..f02bad1d 100644 --- a/Sources/Networking/Core/NetworkingModifier.swift +++ b/Sources/Networking/Core/NetworkingModifier.swift @@ -3,23 +3,23 @@ import Foundation import Helpers public protocol NetworkingModifier { - func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream + func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream } extension NetworkingComponent { - public func modified(_ modifier: some NetworkingModifier) -> some NetworkingComponent { - Modified(upstream: self, modifier: modifier) - } + public func modified(_ modifier: some NetworkingModifier) -> some NetworkingComponent { + Modified(upstream: self, modifier: modifier) + } } private struct Modified: NetworkingComponent { - let upstream: Upstream - let modifier: Modifier - init(upstream: Upstream, modifier: Modifier) { - self.upstream = upstream - self.modifier = modifier - } - func send(_ request: HTTPRequestData) -> ResponseStream { - modifier.send(upstream: upstream, request: request) - } + let upstream: Upstream + let modifier: Modifier + init(upstream: Upstream, modifier: Modifier) { + self.upstream = upstream + self.modifier = modifier + } + func send(_ request: HTTPRequestData) -> ResponseStream { + modifier.send(upstream: upstream, request: request) + } } diff --git a/Sources/Networking/Core/Partial.swift b/Sources/Networking/Core/Partial.swift index 7abe1517..882f79f7 100644 --- a/Sources/Networking/Core/Partial.swift +++ b/Sources/Networking/Core/Partial.swift @@ -1,51 +1,51 @@ public enum Partial { - case progress(Progress) - case value(Value, Progress) - - public var value: Value? { - guard case let .value(value, _) = self else { - return nil - } - return value + case progress(Progress) + case value(Value, Progress) + + public var value: Value? { + guard case let .value(value, _) = self else { + return nil } - - public var progress: Progress { - switch self { - case .progress(let progress), .value(_, let progress): - return progress - } + return value + } + + public var progress: Progress { + switch self { + case .progress(let progress), .value(_, let progress): + return progress } - - public func onValue( - perform block: (Value) throws -> Void - ) rethrows -> Partial { - if case let .value(value, _) = self { - try block(value) - } - return self + } + + public func onValue( + perform block: (Value) throws -> Void + ) rethrows -> Partial { + if case let .value(value, _) = self { + try block(value) } - - public func mapValue( - transform: (Value) throws -> NewValue - ) rethrows -> Partial { - switch self { - case let .progress(progess): - return .progress(progess) - case let .value(value, progress): - return try .value(transform(value), progress) - } + return self + } + + public func mapValue( + transform: (Value) throws -> NewValue + ) rethrows -> Partial { + switch self { + case let .progress(progess): + return .progress(progess) + case let .value(value, progress): + return try .value(transform(value), progress) } - - public func mapProgress( - transform: (Progress) throws -> NewProgress - ) rethrows -> Partial { - switch self { - case let .progress(progress): - return try .progress(transform(progress)) - case let .value(value, progress): - return try .value(value, transform(progress)) - } + } + + public func mapProgress( + transform: (Progress) throws -> NewProgress + ) rethrows -> Partial { + switch self { + case let .progress(progress): + return try .progress(transform(progress)) + case let .value(value, progress): + return try .value(value, transform(progress)) } + } } extension Partial: Equatable where Value: Equatable, Progress: Equatable { } diff --git a/Sources/Networking/Core/ProgressTracker.swift b/Sources/Networking/Core/ProgressTracker.swift index 5910b6d3..ac83ca5b 100644 --- a/Sources/Networking/Core/ProgressTracker.swift +++ b/Sources/Networking/Core/ProgressTracker.swift @@ -1,19 +1,19 @@ public actor ProgressTracker: Sendable { - private var tasks: [AnyHashable: BytesReceived] = [:] - - func set(id: some Hashable & Sendable, bytesReceived: BytesReceived) { - tasks[id] = bytesReceived - } - - func remove(id: AnyHashable) { - tasks[id] = nil - } - - public func overall() -> BytesReceived { - tasks.values.reduce(BytesReceived(), +) - } - - public func fractionCompleted() -> Double { - overall().fractionCompleted - } + private var tasks: [AnyHashable: BytesReceived] = [:] + + func set(id: some Hashable & Sendable, bytesReceived: BytesReceived) { + tasks[id] = bytesReceived + } + + func remove(id: AnyHashable) { + tasks[id] = nil + } + + public func overall() -> BytesReceived { + tasks.values.reduce(BytesReceived(), +) + } + + public func fractionCompleted() -> Double { + overall().fractionCompleted + } } diff --git a/Sources/Networking/Core/Request.swift b/Sources/Networking/Core/Request.swift index f53498cd..7d4f09b7 100644 --- a/Sources/Networking/Core/Request.swift +++ b/Sources/Networking/Core/Request.swift @@ -3,48 +3,48 @@ import Combine import HTTPTypes public struct Request: Sendable { - public let http: HTTPRequestData - public let decode: @Sendable (HTTPResponseData) throws -> Body - - public init(http: HTTPRequestData, decode: @escaping @Sendable (HTTPResponseData) throws -> Body) { - self.http = http - self.decode = decode - } + public let http: HTTPRequestData + public let decode: @Sendable (HTTPResponseData) throws -> Body + + public init(http: HTTPRequestData, decode: @escaping @Sendable (HTTPResponseData) throws -> Body) { + self.http = http + self.decode = decode + } } extension Request { - public init( - http: HTTPRequestData, - as payloadType: Payload.Type, - decoder: Decoder, - transform: @escaping @Sendable (Payload, HTTPResponseData) throws -> Body - ) where Decoder.Input == Data { - self.init(http: http) { response in - try response.decode(as: payloadType, decoder: decoder, transform: transform) - } - } - - public init( - http: HTTPRequestData, - as payloadType: Payload.Type, - transform: @escaping @Sendable (Payload, HTTPResponseData) throws -> Body - ) { - self.init(http: http, as: payloadType, decoder: JSONDecoder(), transform: transform) + public init( + http: HTTPRequestData, + as payloadType: Payload.Type, + decoder: Decoder, + transform: @escaping @Sendable (Payload, HTTPResponseData) throws -> Body + ) where Decoder.Input == Data { + self.init(http: http) { response in + try response.decode(as: payloadType, decoder: decoder, transform: transform) } + } + + public init( + http: HTTPRequestData, + as payloadType: Payload.Type, + transform: @escaping @Sendable (Payload, HTTPResponseData) throws -> Body + ) { + self.init(http: http, as: payloadType, decoder: JSONDecoder(), transform: transform) + } } extension Request where Body: Decodable { - - public init( - http: HTTPRequestData, - decoder: Decoder - ) where Decoder.Input == Data { - self.init(http: http, as: Body.self, decoder: decoder) { payload, _ in - payload - } - } - - public init(http: HTTPRequestData) { - self.init(http: http, decoder: JSONDecoder()) + + public init( + http: HTTPRequestData, + decoder: Decoder + ) where Decoder.Input == Data { + self.init(http: http, as: Body.self, decoder: decoder) { payload, _ in + payload } + } + + public init(http: HTTPRequestData) { + self.init(http: http, decoder: JSONDecoder()) + } } diff --git a/Sources/Networking/Core/Errors.swift b/Sources/Networking/Errors/Errors.swift similarity index 100% rename from Sources/Networking/Core/Errors.swift rename to Sources/Networking/Errors/Errors.swift diff --git a/Sources/Networking/Errors/NetworkingError+Decoding.swift b/Sources/Networking/Errors/NetworkingError+Decoding.swift index 94fcbb54..20305e6b 100644 --- a/Sources/Networking/Errors/NetworkingError+Decoding.swift +++ b/Sources/Networking/Errors/NetworkingError+Decoding.swift @@ -2,33 +2,33 @@ import Combine import Foundation extension NetworkingError { - - public func decodeResponseBody( - as errorMessageType: ErrorMessage.Type, - using decoder: Decoder - ) -> ErrorMessage? where Decoder.Input == Data { - guard let response, false == response.data.isEmpty else { - return nil - } - do { - let message = try decoder.decode(errorMessageType, from: response.data) - return message - } catch { - @NetworkEnvironment(\.logger) var logger - if let logger { - let stringRepresentation = String(decoding: response.data, as: UTF8.self) - let privateLogMessage = "Decoding \(String(describing: response))" - + " into \(String(describing: errorMessageType))," - + " but received: \(stringRepresentation)" - logger.error("Failed to decode error message. \(privateLogMessage, privacy: .private)") - } - return nil - } + + public func decodeResponseBody( + as errorMessageType: ErrorMessage.Type, + using decoder: Decoder + ) -> ErrorMessage? where Decoder.Input == Data { + guard let response, false == response.data.isEmpty else { + return nil } - - public func decodeResponseBodyIntoJSON( - as errorMessageType: ErrorMessage.Type - ) -> ErrorMessage? { - decodeResponseBody(as: errorMessageType, using: JSONDecoder()) + do { + let message = try decoder.decode(errorMessageType, from: response.data) + return message + } catch { + @NetworkEnvironment(\.logger) var logger + if let logger { + let stringRepresentation = String(decoding: response.data, as: UTF8.self) + let privateLogMessage = "Decoding \(String(describing: response))" + + " into \(String(describing: errorMessageType))," + + " but received: \(stringRepresentation)" + logger.error("Failed to decode error message. \(privateLogMessage, privacy: .private)") + } + return nil } + } + + public func decodeResponseBodyIntoJSON( + as errorMessageType: ErrorMessage.Type + ) -> ErrorMessage? { + decodeResponseBody(as: errorMessageType, using: JSONDecoder()) + } } diff --git a/Sources/Networking/Errors/NetworkingError.swift b/Sources/Networking/Errors/NetworkingError.swift index a6754163..5a5266c0 100644 --- a/Sources/Networking/Errors/NetworkingError.swift +++ b/Sources/Networking/Errors/NetworkingError.swift @@ -1,6 +1,6 @@ import Foundation public protocol NetworkingError: Error { - var request: HTTPRequestData { get } - var response: HTTPResponseData? { get } + var request: HTTPRequestData { get } + var response: HTTPResponseData? { get } } diff --git a/Sources/Networking/Errors/StackError.swift b/Sources/Networking/Errors/StackError.swift index 1e6143ba..014734f3 100644 --- a/Sources/Networking/Errors/StackError.swift +++ b/Sources/Networking/Errors/StackError.swift @@ -2,55 +2,55 @@ import Foundation import Helpers enum StackError: Error { - enum ProgrammingError: Equatable, Sendable { + enum ProgrammingError: Equatable, Sendable { + // TBD + } - } - - case createURLRequestFailed(HTTPRequestData) - case decodeResponse(HTTPResponseData, Error) - case invalidURLResponse(HTTPRequestData, Data, URLResponse?) - case statusCode(HTTPResponseData) - case timeout(HTTPRequestData) - case unauthorized(HTTPResponseData) + case createURLRequestFailed(HTTPRequestData) + case decodeResponse(HTTPResponseData, Error) + case invalidURLResponse(HTTPRequestData, Data, URLResponse?) + case statusCode(HTTPResponseData) + case timeout(HTTPRequestData) + case unauthorized(HTTPResponseData) } extension StackError: NetworkingError { - var request: HTTPRequestData { - switch self { - case .createURLRequestFailed(let request), .invalidURLResponse(let request, _, _), .timeout(let request): - return request - case .unauthorized(let response), .decodeResponse(let response, _), .statusCode(let response): - return response.request - } + var request: HTTPRequestData { + switch self { + case .createURLRequestFailed(let request), .invalidURLResponse(let request, _, _), .timeout(let request): + return request + case .unauthorized(let response), .decodeResponse(let response, _), .statusCode(let response): + return response.request } + } - var response: HTTPResponseData? { - switch self { - case .createURLRequestFailed, .invalidURLResponse, .timeout: - return nil - case .unauthorized(let response), .decodeResponse(let response, _), .statusCode(let response): - return response - } + var response: HTTPResponseData? { + switch self { + case .createURLRequestFailed, .invalidURLResponse, .timeout: + return nil + case .unauthorized(let response), .decodeResponse(let response, _), .statusCode(let response): + return response } + } } extension StackError: Equatable { - static func == (lhs: StackError, rhs: StackError) -> Bool { - switch (lhs, rhs) { - case let (.createURLRequestFailed(lhs), .createURLRequestFailed(rhs)): - return lhs == rhs - case let (.decodeResponse(lhs, lhsE), .decodeResponse(rhs, rhsE)): - return lhs == rhs && _isEqual(lhsE, rhsE) - case let (.invalidURLResponse(lhs, lhsD, lhsR), .invalidURLResponse(rhs, rhsD, rhsR)): - return lhs == rhs && _isEqual(lhsD, rhsD) && lhsR == rhsR - case let (.statusCode(lhs), .statusCode(rhs)): - return lhs == rhs - case let (.timeout(lhs), .timeout(rhs)): - return lhs == rhs - case let (.unauthorized(lhs), .unauthorized(rhs)): - return lhs == rhs - default: - return false - } + static func == (lhs: StackError, rhs: StackError) -> Bool { + switch (lhs, rhs) { + case let (.createURLRequestFailed(lhs), .createURLRequestFailed(rhs)): + return lhs == rhs + case let (.decodeResponse(lhs, lhsE), .decodeResponse(rhs, rhsE)): + return lhs == rhs && _isEqual(lhsE, rhsE) + case let (.invalidURLResponse(lhs, lhsD, lhsR), .invalidURLResponse(rhs, rhsD, rhsR)): + return lhs == rhs && _isEqual(lhsD, rhsD) && lhsR == rhsR + case let (.statusCode(lhs), .statusCode(rhs)): + return lhs == rhs + case let (.timeout(lhs), .timeout(rhs)): + return lhs == rhs + case let (.unauthorized(lhs), .unauthorized(rhs)): + return lhs == rhs + default: + return false } + } } diff --git a/Sources/Networking/Options/ExpectedContentLength.swift b/Sources/Networking/Options/ExpectedContentLength.swift index d4dabb6b..acf10157 100644 --- a/Sources/Networking/Options/ExpectedContentLength.swift +++ b/Sources/Networking/Options/ExpectedContentLength.swift @@ -1,10 +1,10 @@ private enum ExpectedContentLength: HTTPRequestDataOption { - static var defaultOption: Int64? + static var defaultOption: Int64? } extension HTTPRequestData { - public var expectedContentLength: Int64? { - get { self[option: ExpectedContentLength.self] } - set { self[option: ExpectedContentLength.self] = newValue } - } + public var expectedContentLength: Int64? { + get { self[option: ExpectedContentLength.self] } + set { self[option: ExpectedContentLength.self] = newValue } + } } diff --git a/Sources/Networking/Options/RequestTimeout.swift b/Sources/Networking/Options/RequestTimeout.swift index ffb18dfd..8dc90e78 100644 --- a/Sources/Networking/Options/RequestTimeout.swift +++ b/Sources/Networking/Options/RequestTimeout.swift @@ -1,12 +1,12 @@ import Foundation enum RequestTimeoutInSeconds: HTTPRequestDataOption { - static var defaultOption: Int64 = 60 + static var defaultOption: Int64 = 60 } extension HTTPRequestData { - public var requestTimeoutInSeconds: Int64 { - get { self[option: RequestTimeoutInSeconds.self] } - set { self[option: RequestTimeoutInSeconds.self] = newValue } - } + public var requestTimeoutInSeconds: Int64 { + get { self[option: RequestTimeoutInSeconds.self] } + set { self[option: RequestTimeoutInSeconds.self] = newValue } + } } diff --git a/Sources/TestSupport/BytesReceived+.swift b/Sources/TestSupport/BytesReceived+.swift index 963da553..cd613f5f 100644 --- a/Sources/TestSupport/BytesReceived+.swift +++ b/Sources/TestSupport/BytesReceived+.swift @@ -1,7 +1,7 @@ import Networking extension BytesReceived { - public init(response: HTTPResponseData) { - self.init(data: response.data) - } + public init(response: HTTPResponseData) { + self.init(data: response.data) + } } diff --git a/Sources/TestSupport/Logger+.swift b/Sources/TestSupport/Logger+.swift index 2457f7e6..258c40bf 100644 --- a/Sources/TestSupport/Logger+.swift +++ b/Sources/TestSupport/Logger+.swift @@ -1,5 +1,5 @@ import os.log extension Logger { - public static let test: Self = .init(subsystem: "works.dan.danthorpe-networking", category: "Tests") + public static let test: Self = .init(subsystem: "works.dan.danthorpe-networking", category: "Tests") } diff --git a/Sources/TestSupport/Mocked.swift b/Sources/TestSupport/Mocked.swift index 65131bbc..437caaea 100644 --- a/Sources/TestSupport/Mocked.swift +++ b/Sources/TestSupport/Mocked.swift @@ -7,7 +7,7 @@ extension NetworkingComponent { ) -> some NetworkingComponent { modified(Mocked(mock: check, with: stub)) } - + public func mocked( _ request: HTTPRequestData, stub: StubbedResponseStream @@ -19,14 +19,14 @@ extension NetworkingComponent { struct Mocked: NetworkingModifier { let mock: (HTTPRequestData) -> Bool let stub: StubbedResponseStream - + @NetworkEnvironment(\.instrument) var instrument - + init(mock: @escaping (HTTPRequestData) -> Bool, with stubbedResponse: StubbedResponseStream) { self.mock = mock self.stub = stubbedResponse } - + func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream { guard mock(request) else { return upstream.send(request) @@ -50,7 +50,7 @@ extension NetworkingComponent { struct CustomMocked: NetworkingModifier { let block: @Sendable (NetworkingComponent, HTTPRequestData) -> ResponseStream - + func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream { block(upstream, request) } diff --git a/Sources/TestSupport/NetworkEnvironmentReporter.swift b/Sources/TestSupport/NetworkEnvironmentReporter.swift index 5ac6844e..cb361eeb 100644 --- a/Sources/TestSupport/NetworkEnvironmentReporter.swift +++ b/Sources/TestSupport/NetworkEnvironmentReporter.swift @@ -2,19 +2,19 @@ import Networking import Helpers public actor NetworkEnvironmentReporter: NetworkReportingComponent { - let keyPath: KeyPath - public private(set) var start: Value? - public private(set) var finish: Value? - - public init(keyPath: KeyPath) { - self.keyPath = keyPath - } - - public func didStart(request: Networking.HTTPRequestData) { - self.start = NetworkEnvironmentValues.environmentValues[keyPath: keyPath] - } - - public func didFinish(request: HTTPRequestData) { - self.finish = NetworkEnvironmentValues.environmentValues[keyPath: keyPath] - } + let keyPath: KeyPath + public private(set) var start: Value? + public private(set) var finish: Value? + + public init(keyPath: KeyPath) { + self.keyPath = keyPath + } + + public func didStart(request: Networking.HTTPRequestData) { + self.start = NetworkEnvironmentValues.environmentValues[keyPath: keyPath] + } + + public func didFinish(request: HTTPRequestData) { + self.finish = NetworkEnvironmentValues.environmentValues[keyPath: keyPath] + } } diff --git a/Sources/TestSupport/Reported.swift b/Sources/TestSupport/Reported.swift index a426dd27..9b9138e3 100644 --- a/Sources/TestSupport/Reported.swift +++ b/Sources/TestSupport/Reported.swift @@ -2,59 +2,59 @@ import Networking import Helpers public protocol NetworkReportingComponent: Actor { - func didStart(request: HTTPRequestData) - func didFinish(request: HTTPRequestData) + func didStart(request: HTTPRequestData) + func didFinish(request: HTTPRequestData) } extension NetworkReportingComponent { - public func didFinish(request: HTTPRequestData) { } + public func didFinish(request: HTTPRequestData) { } } extension NetworkingComponent { - public func reported(by testReporter: any NetworkReportingComponent) -> some NetworkingComponent { - modified(Reported(reporter: testReporter)) - } + public func reported(by testReporter: any NetworkReportingComponent) -> some NetworkingComponent { + modified(Reported(reporter: testReporter)) + } } struct Reported: NetworkingModifier { - let reporter: any NetworkReportingComponent - func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream { - ResponseStream { continuation in - Task { - await reporter.didStart(request: request) - await upstream.send(request) - .redirect(into: continuation, onTermination: { - await reporter.didFinish(request: request) - }) - } - } + let reporter: any NetworkReportingComponent + func send(upstream: NetworkingComponent, request: HTTPRequestData) -> ResponseStream { + ResponseStream { continuation in + Task { + await reporter.didStart(request: request) + await upstream.send(request) + .redirect(into: continuation, onTermination: { + await reporter.didFinish(request: request) + }) + } } + } } // MARK: - Test Reporter public actor TestReporter: NetworkReportingComponent { - public var requests: [HTTPRequestData] = [] - public var activeRequests: [HTTPRequestData] { - didSet { - peakActiveRequests = max(peakActiveRequests, activeRequests.count) - } - } - public var peakActiveRequests: Int = 0 - public init( - requests: [HTTPRequestData] = [], - activeRequests: [HTTPRequestData] = [] - ) { - self.requests = requests - self.activeRequests = requests - } - - public func didStart(request: HTTPRequestData) { - self.requests.append(request) - self.activeRequests.append(request) - } - - public func didFinish(request: HTTPRequestData) { - self.activeRequests.removeAll { $0.id == request.id } + public var requests: [HTTPRequestData] = [] + public var activeRequests: [HTTPRequestData] { + didSet { + peakActiveRequests = max(peakActiveRequests, activeRequests.count) } + } + public var peakActiveRequests: Int = 0 + public init( + requests: [HTTPRequestData] = [], + activeRequests: [HTTPRequestData] = [] + ) { + self.requests = requests + self.activeRequests = requests + } + + public func didStart(request: HTTPRequestData) { + self.requests.append(request) + self.activeRequests.append(request) + } + + public func didFinish(request: HTTPRequestData) { + self.activeRequests.removeAll { $0.id == request.id } + } } diff --git a/Sources/TestSupport/StubbedResponse.swift b/Sources/TestSupport/StubbedResponse.swift index b13e67b4..b70cd721 100644 --- a/Sources/TestSupport/StubbedResponse.swift +++ b/Sources/TestSupport/StubbedResponse.swift @@ -4,89 +4,89 @@ import Networking import HTTPTypes public struct StubbedError: Hashable, Error { - public let request: HTTPRequestData - public init(request: HTTPRequestData) { - self.request = request - } + public let request: HTTPRequestData + public init(request: HTTPRequestData) { + self.request = request + } } public struct StubbedResponseStream: Equatable, Sendable { - public enum Configuration: Equatable, Sendable { - case immediate - case uniform(steps: Int = 4, interval: Duration = .seconds(2)) - case throwing - - public static let `default`: Self = .uniform() - } - - public let configuration: Configuration - public let data: Data - public let response: HTTPResponse - - public init( - _ configuration: Configuration = .default, - data: Data = Data(), - response: HTTPResponse - ) { - self.configuration = configuration - self.data = data - self.response = response - } - - public func callAsFunction(_ request: HTTPRequestData) -> ResponseStream { - ResponseStream { continuation in - let responseData = expectedResponse(request) - Task { - let clock = TestClock() - var bytes = BytesReceived().withExpectedBytes(from: responseData) - switch configuration { - case .immediate: - bytes.receiveBytes(count: Int64(responseData.data.count)) - continuation.yield(.value(responseData, bytes)) - continuation.finish() - case .throwing: - let bytesReceived = bytes.expected / 4 - for _ in 0..<2 { - await clock.advance(by: .seconds(2)) - bytes.receiveBytes(count: bytesReceived) - continuation.yield(.progress(bytes)) - } - continuation.finish(throwing: StubbedError(request: request)) - - case let .uniform(steps: steps, interval: interval): - let bytesReceived = bytes.expected / Int64(steps) - for _ in 0.. ResponseStream { + ResponseStream { continuation in + let responseData = expectedResponse(request) + Task { + let clock = TestClock() + var bytes = BytesReceived().withExpectedBytes(from: responseData) + switch configuration { + case .immediate: + bytes.receiveBytes(count: Int64(responseData.data.count)) + continuation.yield(.value(responseData, bytes)) + continuation.finish() + case .throwing: + let bytesReceived = bytes.expected / 4 + for _ in 0..<2 { + await clock.advance(by: .seconds(2)) + bytes.receiveBytes(count: bytesReceived) + continuation.yield(.progress(bytes)) + } + continuation.finish(throwing: StubbedError(request: request)) + + case let .uniform(steps: steps, interval: interval): + let bytesReceived = bytes.expected / Int64(steps) + for _ in 0.. HTTPResponseData { - HTTPResponseData(request: request, data: data, response: response) - } + } + + public func expectedResponse(_ request: HTTPRequestData) -> HTTPResponseData { + HTTPResponseData(request: request, data: data, response: response) + } } extension StubbedResponseStream { - public static func ok( - _ configuration: Configuration = .default, - data: Data = Data(), - headerFields: HTTPFields = [:] - ) -> Self { - .status(.ok, configuration, data: data, headerFields: headerFields) - } - - public static func status( - _ status: HTTPResponse.Status, - _ configuration: Configuration = .default, - data: Data = Data(), - headerFields: HTTPFields = [:] - ) -> Self { - .init(configuration, data: data, response: .init(status: status, headerFields: headerFields)) - } + public static func ok( + _ configuration: Configuration = .default, + data: Data = Data(), + headerFields: HTTPFields = [:] + ) -> Self { + .status(.ok, configuration, data: data, headerFields: headerFields) + } + + public static func status( + _ status: HTTPResponse.Status, + _ configuration: Configuration = .default, + data: Data = Data(), + headerFields: HTTPFields = [:] + ) -> Self { + .init(configuration, data: data, response: .init(status: status, headerFields: headerFields)) + } } diff --git a/Sources/TestSupport/TerminalNetworkingComponent.swift b/Sources/TestSupport/TerminalNetworkingComponent.swift index eb13c94b..0fac524b 100644 --- a/Sources/TestSupport/TerminalNetworkingComponent.swift +++ b/Sources/TestSupport/TerminalNetworkingComponent.swift @@ -2,25 +2,25 @@ import Networking import XCTestDynamicOverlay public struct TerminalNetworkingComponent: NetworkingComponent { - public struct TestFailure: Equatable, Error { - public let request: HTTPRequestData - public init(request: HTTPRequestData) { - self.request = request - } + public struct TestFailure: Equatable, Error { + public let request: HTTPRequestData + public init(request: HTTPRequestData) { + self.request = request } - let isFailingTerminal: Bool - public init( - isFailingTerminal: Bool = true - ) { - self.isFailingTerminal = isFailingTerminal - } - public func send(_ request: HTTPRequestData) -> ResponseStream { - ResponseStream { continuation in - if isFailingTerminal { - continuation.finish(throwing: TestFailure(request: request)) - } else { - continuation.finish() - } - } + } + let isFailingTerminal: Bool + public init( + isFailingTerminal: Bool = true + ) { + self.isFailingTerminal = isFailingTerminal + } + public func send(_ request: HTTPRequestData) -> ResponseStream { + ResponseStream { continuation in + if isFailingTerminal { + continuation.finish(throwing: TestFailure(request: request)) + } else { + continuation.finish() + } } + } } diff --git a/Sources/TestSupport/TestAuthenticationDelegate.swift b/Sources/TestSupport/TestAuthenticationDelegate.swift index 4749cac5..ccabbcb3 100644 --- a/Sources/TestSupport/TestAuthenticationDelegate.swift +++ b/Sources/TestSupport/TestAuthenticationDelegate.swift @@ -5,13 +5,13 @@ import XCTestDynamicOverlay public final class TestAuthenticationDelegate: @unchecked Sendable { public typealias Fetch = @Sendable (HTTPRequestData) async throws -> Credentials public typealias Refresh = @Sendable (Credentials, HTTPResponseData) async throws -> Credentials - + @Protected public var fetchCount: Int = 0 @Protected public var refreshCount: Int = 0 - + public var fetch: Fetch public var refresh: Refresh - + public init( fetch: @escaping Fetch = unimplemented("TestAuthenticationDelegate.fetch"), refresh: @escaping Refresh = unimplemented("TestAuthenticationDelegate.refresh") @@ -26,7 +26,7 @@ extension TestAuthenticationDelegate: AuthenticationDelegate { fetchCount += 1 return try await fetch(request) } - + public func refresh( unauthorized credentials: Credentials, from response: HTTPResponseData diff --git a/Tests/NetworkingTests/Components/Authentication/AuthenticationTests.swift b/Tests/NetworkingTests/Components/Authentication/AuthenticationTests.swift index c7dc3638..47bf5f83 100644 --- a/Tests/NetworkingTests/Components/Authentication/AuthenticationTests.swift +++ b/Tests/NetworkingTests/Components/Authentication/AuthenticationTests.swift @@ -8,7 +8,7 @@ import TestSupport import XCTest final class AuthenticationTests: XCTestCase { - + override func invokeTest() { withDependencies { $0.shortID = .incrementing @@ -17,7 +17,7 @@ final class AuthenticationTests: XCTestCase { super.invokeTest() } } - + func test__authentication() async throws { let reporter = TestReporter() let delegate = TestAuthenticationDelegate( @@ -25,27 +25,27 @@ final class AuthenticationTests: XCTestCase { BearerCredentials(token: "token") } ) - + let bearerAuthentication = BearerAuthentication(delegate: delegate) - + var request = HTTPRequestData(authority: "example.com") request.authenticationMethod = .bearer let copy = request - + let network = TerminalNetworkingComponent() .mocked(.ok(), check: { _ in true }) .reported(by: reporter) .authenticated(with: bearerAuthentication) - + try await withMainSerialExecutor { try await withThrowingTaskGroup(of: HTTPResponseData.self) { group in - + for _ in 0..<4 { group.addTask { return try await network.data(copy) } } - + var responses: [HTTPResponseData] = [] for try await response in group { responses.append(response) @@ -54,42 +54,42 @@ final class AuthenticationTests: XCTestCase { $0.request.headerFields[.authorization] == "Bearer token" }) } - + let reportedRequests = await reporter.requests XCTAssertEqual(reportedRequests.count, 4) XCTAssertTrue(reportedRequests.allSatisfy { $0.headerFields[.authorization] == "Bearer token" }) - + XCTAssertEqual(delegate.fetchCount, 1) } } - + func test__authentication__when_delegate_throws_error_on_fetch() async throws { struct CustomError: Error, Hashable { } - + let delegate = TestAuthenticationDelegate( fetch: { _ in throw CustomError() } ) - + let bearerAuthentication = BearerAuthentication(delegate: delegate) - + var request = HTTPRequestData(authority: "example.com") request.authenticationMethod = .bearer - + let network = TerminalNetworkingComponent() .authenticated(with: bearerAuthentication) - + await XCTAssertThrowsError( try await network.data(request), matches: AuthenticationError.fetchCredentialsFailed(request, .bearer, CustomError()) ) } - + func test__authentication__refresh_token() async throws { - + var isUnauthorized = true let reporter = TestReporter() let delegate = TestAuthenticationDelegate( @@ -100,12 +100,12 @@ final class AuthenticationTests: XCTestCase { BearerCredentials(token: "refreshed token") } ) - + let bearerAuthentication = BearerAuthentication(delegate: delegate) - + var request = HTTPRequestData(authority: "example.com") request.authenticationMethod = .bearer - + let network = TerminalNetworkingComponent() .mocked(.ok(), check: { _ in true }) .mocked(.status(.unauthorized), check: { _ in @@ -114,9 +114,9 @@ final class AuthenticationTests: XCTestCase { }) .reported(by: reporter) .authenticated(with: bearerAuthentication) - + try await network.data(request) - + let reportedRequests = await reporter.requests XCTAssertEqual(reportedRequests.count, 2) XCTAssertTrue(reportedRequests[0].headerFields[.authorization] == "Bearer token") diff --git a/Tests/NetworkingTests/Components/Authentication/BasicCredentialsTests.swift b/Tests/NetworkingTests/Components/Authentication/BasicCredentialsTests.swift index 63cbeb23..0563fd86 100644 --- a/Tests/NetworkingTests/Components/Authentication/BasicCredentialsTests.swift +++ b/Tests/NetworkingTests/Components/Authentication/BasicCredentialsTests.swift @@ -13,7 +13,7 @@ final class BasicCredentialsTests: XCTestCase { let request = credentials.apply(to: HTTPRequestData(id: "1")) XCTAssertEqual(request.headerFields[.authorization], "Basic YmxvYjpzdXBlciEkM2NyZXQ=") } - + func test__provide_credentials() { var request = HTTPRequestData(id: "1") request.basicCredentials = BasicCredentials(user: "blob", password: "super!$3cret") diff --git a/Tests/NetworkingTests/Components/Authentication/BearerCredentialsTests.swift b/Tests/NetworkingTests/Components/Authentication/BearerCredentialsTests.swift index 91e6e4d4..90ef8ae9 100644 --- a/Tests/NetworkingTests/Components/Authentication/BearerCredentialsTests.swift +++ b/Tests/NetworkingTests/Components/Authentication/BearerCredentialsTests.swift @@ -13,7 +13,7 @@ final class BearerCredentialsTests: XCTestCase { let request = credentials.apply(to: HTTPRequestData(id: "1")) XCTAssertEqual(request.headerFields[.authorization], "Bearer super!$3cret") } - + func test__provide_credentials() { var request = HTTPRequestData(id: "1") request.bearerCredentials = BearerCredentials(token: "super!$3cret") diff --git a/Tests/NetworkingTests/Components/Authentication/HeaderBasedAuthenticationTests.swift b/Tests/NetworkingTests/Components/Authentication/HeaderBasedAuthenticationTests.swift index d6d28197..c5bfa2f5 100644 --- a/Tests/NetworkingTests/Components/Authentication/HeaderBasedAuthenticationTests.swift +++ b/Tests/NetworkingTests/Components/Authentication/HeaderBasedAuthenticationTests.swift @@ -8,62 +8,62 @@ import XCTest @testable import Networking final class HeaderBasedAuthenticationTests: XCTestCase { - + func test__given_requires_credentials__delegate_is_triggered() async throws { var request = HTTPRequestData(id: "1") request.authenticationMethod = .bearer - + let delelgate = TestAuthenticationDelegate( fetch: { [request] in XCTAssertEqual(request, $0) return BearerCredentials(token: "some token") } ) - + let authenticator = HeaderBasedAuthentication(delegate: delelgate) - + let newRequest = try await authenticator.fetch(for: request).apply(to: request) - + XCTAssertEqual(newRequest.headerFields[.authorization], "Bearer some token") XCTAssertEqual(delelgate.fetchCount, 1) } - + func test__given_delegate_throws_error() async throws { struct CustomError: Error, Equatable { } - + var request = HTTPRequestData(id: "1") request.authenticationMethod = .bearer - + let delelgate = TestAuthenticationDelegate( fetch: { _ in throw CustomError() } ) - + let authenticator = HeaderBasedAuthentication(delegate: delelgate) - + await XCTAssertThrowsError( try await authenticator.fetch(for: request), matches: AuthenticationError.fetchCredentialsFailed(request, .bearer, CustomError()) ) } - + func test__requests_are_queued_until_delegate_responds() async throws { - + let delelgate = TestAuthenticationDelegate( fetch: { _ in return BearerCredentials(token: "some token") } ) - + let authenticator = HeaderBasedAuthentication(delegate: delelgate) - + @Sendable func check(authority: String) async throws -> HTTPRequestData { var request = HTTPRequestData(authority: "example.com") request.authenticationMethod = .bearer return try await authenticator.fetch(for: request).apply(to: request) } - + try await withDependencies { $0.shortID = .incrementing } operation: { @@ -81,14 +81,14 @@ final class HeaderBasedAuthenticationTests: XCTestCase { group.addTask { try await check(authority: "example.com") } - + var requests: [HTTPRequestData] = [] for try await request in group { requests.append(request) } return requests } - + let authorization = Set(requests.compactMap(\.headerFields[.authorization])) XCTAssertEqual(authorization, ["Bearer some token"]) XCTAssertEqual(delelgate.fetchCount, 1) diff --git a/Tests/NetworkingTests/Components/CheckedStatusCodeTests.swift b/Tests/NetworkingTests/Components/CheckedStatusCodeTests.swift index b84b0808..d1ae1973 100644 --- a/Tests/NetworkingTests/Components/CheckedStatusCodeTests.swift +++ b/Tests/NetworkingTests/Components/CheckedStatusCodeTests.swift @@ -8,53 +8,53 @@ import XCTest @testable import Networking final class CheckedStatusCodeTests: XCTestCase { - - func configureNetwork( - for status: HTTPResponse.Status - ) -> (network: some NetworkingComponent, response: HTTPResponseData) { - let request = HTTPRequestData(authority: "example.com") - let stubbed: StubbedResponseStream = .status(status) - let network = TerminalNetworkingComponent() - .mocked(request, stub: stubbed) - .checkedStatusCode() - - return (network, stubbed.expectedResponse(request)) + + func configureNetwork( + for status: HTTPResponse.Status + ) -> (network: some NetworkingComponent, response: HTTPResponseData) { + let request = HTTPRequestData(authority: "example.com") + let stubbed: StubbedResponseStream = .status(status) + let network = TerminalNetworkingComponent() + .mocked(request, stub: stubbed) + .checkedStatusCode() + + return (network, stubbed.expectedResponse(request)) + } + + func test__ok() async throws { + try await withDependencies { + $0.shortID = .incrementing + $0.continuousClock = TestClock() + } operation: { + let (network, expectedResponse) = configureNetwork(for: .ok) + try await network.data(expectedResponse.request) } - - func test__ok() async throws { - try await withDependencies { - $0.shortID = .incrementing - $0.continuousClock = TestClock() - } operation: { - let (network, expectedResponse) = configureNetwork(for: .ok) - try await network.data(expectedResponse.request) - } + } + + func test__internal_server_error() async throws { + try await withDependencies { + $0.shortID = .incrementing + $0.continuousClock = TestClock() + } operation: { + let (network, expectedResponse) = configureNetwork(for: .internalServerError) + await XCTAssertThrowsError( + try await network.data(expectedResponse.request), + matches: StackError.statusCode(expectedResponse) + ) } - - func test__internal_server_error() async throws { - try await withDependencies { - $0.shortID = .incrementing - $0.continuousClock = TestClock() - } operation: { - let (network, expectedResponse) = configureNetwork(for: .internalServerError) - await XCTAssertThrowsError( - try await network.data(expectedResponse.request), - matches: StackError.statusCode(expectedResponse) - ) - } + } + + func test__unauthorized() async throws { + try await withDependencies { + $0.shortID = .incrementing + $0.continuousClock = TestClock() + } operation: { + let (network, expectedResponse) = configureNetwork(for: .unauthorized) + await XCTAssertThrowsError( + try await network.data(expectedResponse.request), + matches: StackError.unauthorized(expectedResponse) + ) } - - func test__unauthorized() async throws { - try await withDependencies { - $0.shortID = .incrementing - $0.continuousClock = TestClock() - } operation: { - let (network, expectedResponse) = configureNetwork(for: .unauthorized) - await XCTAssertThrowsError( - try await network.data(expectedResponse.request), - matches: StackError.unauthorized(expectedResponse) - ) - } - } - + } + } diff --git a/Tests/NetworkingTests/Components/DuplicatesRemovedTests.swift b/Tests/NetworkingTests/Components/DuplicatesRemovedTests.swift index 6da54e62..94b67e37 100644 --- a/Tests/NetworkingTests/Components/DuplicatesRemovedTests.swift +++ b/Tests/NetworkingTests/Components/DuplicatesRemovedTests.swift @@ -6,58 +6,58 @@ import TestSupport import XCTest final class DuplicatesRemovedTests: XCTestCase { - - func test__duplicates_removed() async throws { - let data1 = try XCTUnwrap("Hello".data(using: .utf8)) - let data2 = try XCTUnwrap("World".data(using: .utf8)) - let data3 = try XCTUnwrap("Whoops".data(using: .utf8)) - - let reporter = TestReporter() - - try await withDependencies { - $0.shortID = .incrementing - $0.continuousClock = TestClock() - } operation: { - try await withMainSerialExecutor { - let request1 = HTTPRequestData(authority: "example.com") - let request2 = HTTPRequestData(authority: "example.co.uk") - let request3 = HTTPRequestData(authority: "example.com", path: "/error") - let request4 = HTTPRequestData(authority: "example.com") // actually the same endpoint as request 1 - - let network = TerminalNetworkingComponent() - .mocked(request1, stub: .ok(data: data1)) - .mocked(request2, stub: .ok(data: data2)) - .mocked(request3, stub: .ok(data: data3)) - .mocked(request4, stub: .ok(data: data1)) - .reported(by: reporter) - .duplicatesRemoved() - - try await withThrowingTaskGroup(of: HTTPResponseData.self) { group in - for _ in 0..<4 { - group.addTask { - try await network.data(request1) - } - group.addTask { - try await network.data(request2) - } - group.addTask { - try await network.data(request3) - } - group.addTask { - try await network.data(request4) - } - } - - var responses: [HTTPResponseData] = [] - for try await response in group { - responses.append(response) - } - XCTAssertEqual(responses.count, 16) - } - - let reportedRequests = await reporter.requests - XCTAssertEqual(reportedRequests.count, 3) + + func test__duplicates_removed() async throws { + let data1 = try XCTUnwrap("Hello".data(using: .utf8)) + let data2 = try XCTUnwrap("World".data(using: .utf8)) + let data3 = try XCTUnwrap("Whoops".data(using: .utf8)) + + let reporter = TestReporter() + + try await withDependencies { + $0.shortID = .incrementing + $0.continuousClock = TestClock() + } operation: { + try await withMainSerialExecutor { + let request1 = HTTPRequestData(authority: "example.com") + let request2 = HTTPRequestData(authority: "example.co.uk") + let request3 = HTTPRequestData(authority: "example.com", path: "/error") + let request4 = HTTPRequestData(authority: "example.com") // actually the same endpoint as request 1 + + let network = TerminalNetworkingComponent() + .mocked(request1, stub: .ok(data: data1)) + .mocked(request2, stub: .ok(data: data2)) + .mocked(request3, stub: .ok(data: data3)) + .mocked(request4, stub: .ok(data: data1)) + .reported(by: reporter) + .duplicatesRemoved() + + try await withThrowingTaskGroup(of: HTTPResponseData.self) { group in + for _ in 0..<4 { + group.addTask { + try await network.data(request1) + } + group.addTask { + try await network.data(request2) + } + group.addTask { + try await network.data(request3) + } + group.addTask { + try await network.data(request4) } + } + + var responses: [HTTPResponseData] = [] + for try await response in group { + responses.append(response) + } + XCTAssertEqual(responses.count, 16) } + + let reportedRequests = await reporter.requests + XCTAssertEqual(reportedRequests.count, 3) + } } + } } diff --git a/Tests/NetworkingTests/Components/LoggedTests.swift b/Tests/NetworkingTests/Components/LoggedTests.swift index c0e7e7fa..a5e48acc 100644 --- a/Tests/NetworkingTests/Components/LoggedTests.swift +++ b/Tests/NetworkingTests/Components/LoggedTests.swift @@ -6,76 +6,76 @@ import TestSupport import XCTest final class LoggedTests: XCTestCase { - - struct OnSuccess: Equatable { - let request: HTTPRequestData - let response: HTTPResponseData - let bytes: BytesReceived + + struct OnSuccess: Equatable { + let request: HTTPRequestData + let response: HTTPResponseData + let bytes: BytesReceived + } + + struct OnFailure { + let request: HTTPRequestData + let error: Error + } + + actor LoggedTester { + var onSend: [HTTPRequestData] = [] + var onSuccess: [OnSuccess] = [] + var onFailure: [OnFailure] = [] + + func appendSend(_ value: HTTPRequestData) { + onSend.append(value) } - - struct OnFailure { - let request: HTTPRequestData - let error: Error + func appendSuccess(_ value: OnSuccess) { + onSuccess.append(value) } - - actor LoggedTester { - var onSend: [HTTPRequestData] = [] - var onSuccess: [OnSuccess] = [] - var onFailure: [OnFailure] = [] - - func appendSend(_ value: HTTPRequestData) { - onSend.append(value) - } - func appendSuccess(_ value: OnSuccess) { - onSuccess.append(value) - } - func appendFailure(_ value: OnFailure) { - onFailure.append(value) - } + func appendFailure(_ value: OnFailure) { + onFailure.append(value) } - - func test__logged_receives_lifecycle() async throws { - - let tester = LoggedTester() - let data1 = try XCTUnwrap("Hello".data(using: .utf8)) - let data2 = try XCTUnwrap("World".data(using: .utf8)) - let data3 = try XCTUnwrap("Whoops".data(using: .utf8)) - - try await withDependencies { - $0.shortID = .incrementing - $0.continuousClock = TestClock() - } operation: { - let request1 = HTTPRequestData(authority: "example.com") - let request2 = HTTPRequestData(authority: "example.co.uk") - let request3 = HTTPRequestData(authority: "example.com", path: "error") - let network = TerminalNetworkingComponent(isFailingTerminal: true) - .mocked(request1, stub: .ok(data: data1)) - .mocked(request2, stub: .ok(data: data2)) - .mocked(request3, stub: .ok(.throwing, data: data3)) - .logged(using: .test) { [tester] in - await tester.appendSend($0) - } onFailure: { [tester] in - await tester.appendFailure(OnFailure(request: $0, error: $1)) - } onSuccess: { [tester] in - await tester.appendSuccess(OnSuccess(request: $0, response: $1, bytes: $2)) - } - - try await network.data(request1) - try await network.data(request2) - try await network.data(request1) - try await XCTAssertThrowsError( - await network.data(request3), - matches: StubbedError(request: request3) - ) - - let requests = await tester.onSend - XCTAssertEqual(requests, [request1, request2, request1, request3]) - let datas = await tester.onSuccess.map(\.response.data) - XCTAssertEqual(datas, [data1, data2, data1]) - let failureRequests = await tester.onFailure.map(\.request) - XCTAssertEqual(failureRequests, [request3]) - let failureErrors = await tester.onFailure.map(\.error) - XCTAssertEqual(failureErrors.compactMap { $0 as? StubbedError }, [StubbedError(request: request3)]) + } + + func test__logged_receives_lifecycle() async throws { + + let tester = LoggedTester() + let data1 = try XCTUnwrap("Hello".data(using: .utf8)) + let data2 = try XCTUnwrap("World".data(using: .utf8)) + let data3 = try XCTUnwrap("Whoops".data(using: .utf8)) + + try await withDependencies { + $0.shortID = .incrementing + $0.continuousClock = TestClock() + } operation: { + let request1 = HTTPRequestData(authority: "example.com") + let request2 = HTTPRequestData(authority: "example.co.uk") + let request3 = HTTPRequestData(authority: "example.com", path: "error") + let network = TerminalNetworkingComponent(isFailingTerminal: true) + .mocked(request1, stub: .ok(data: data1)) + .mocked(request2, stub: .ok(data: data2)) + .mocked(request3, stub: .ok(.throwing, data: data3)) + .logged(using: .test) { [tester] in + await tester.appendSend($0) + } onFailure: { [tester] in + await tester.appendFailure(OnFailure(request: $0, error: $1)) + } onSuccess: { [tester] in + await tester.appendSuccess(OnSuccess(request: $0, response: $1, bytes: $2)) } + + try await network.data(request1) + try await network.data(request2) + try await network.data(request1) + try await XCTAssertThrowsError( + await network.data(request3), + matches: StubbedError(request: request3) + ) + + let requests = await tester.onSend + XCTAssertEqual(requests, [request1, request2, request1, request3]) + let datas = await tester.onSuccess.map(\.response.data) + XCTAssertEqual(datas, [data1, data2, data1]) + let failureRequests = await tester.onFailure.map(\.request) + XCTAssertEqual(failureRequests, [request3]) + let failureErrors = await tester.onFailure.map(\.error) + XCTAssertEqual(failureErrors.compactMap { $0 as? StubbedError }, [StubbedError(request: request3)]) } + } } diff --git a/Tests/NetworkingTests/Components/MetricsTests.swift b/Tests/NetworkingTests/Components/MetricsTests.swift index a4f4cbc1..b3cd3b7c 100644 --- a/Tests/NetworkingTests/Components/MetricsTests.swift +++ b/Tests/NetworkingTests/Components/MetricsTests.swift @@ -7,34 +7,34 @@ import TestSupport import XCTest final class MetricsTests: XCTestCase { - func test__basics() async throws { - let clock = TestClock() - let data = try XCTUnwrap("Hello".data(using: .utf8)) - let reporter = NetworkEnvironmentReporter(keyPath: \.instrument) - try await withDependencies { - $0.shortID = .incrementing - $0.continuousClock = clock - } operation: { - let request1 = HTTPRequestData(authority: "example.com") - let network = TerminalNetworkingComponent() - .mocked(request1, stub: .ok(data: data)) - .delayed(by: .seconds(3)) - .reported(by: reporter) - .instrument() - - async let response = network.data(request1) - await clock.advance(by: .seconds(3)) - let receivedData = try await response.data - - XCTAssertEqual(receivedData, data) - - guard let measurements = await reporter.finish??.elapsedTimeMeasurements() else { - XCTFail("Expected to have elapsed time measurements") - return - } - - XCTAssertNoDifference(measurements.map(\.label), ["Delay", "Mocked"]) - XCTAssertNoDifference(measurements.map(\.duration), [.zero, .seconds(3)]) - } + func test__basics() async throws { + let clock = TestClock() + let data = try XCTUnwrap("Hello".data(using: .utf8)) + let reporter = NetworkEnvironmentReporter(keyPath: \.instrument) + try await withDependencies { + $0.shortID = .incrementing + $0.continuousClock = clock + } operation: { + let request1 = HTTPRequestData(authority: "example.com") + let network = TerminalNetworkingComponent() + .mocked(request1, stub: .ok(data: data)) + .delayed(by: .seconds(3)) + .reported(by: reporter) + .instrument() + + async let response = network.data(request1) + await clock.advance(by: .seconds(3)) + let receivedData = try await response.data + + XCTAssertEqual(receivedData, data) + + guard let measurements = await reporter.finish??.elapsedTimeMeasurements() else { + XCTFail("Expected to have elapsed time measurements") + return + } + + XCTAssertNoDifference(measurements.map(\.label), ["Delay", "Mocked"]) + XCTAssertNoDifference(measurements.map(\.duration), [.zero, .seconds(3)]) } + } } diff --git a/Tests/NetworkingTests/Components/NumberedTests.swift b/Tests/NetworkingTests/Components/NumberedTests.swift index 49405939..948d2211 100644 --- a/Tests/NetworkingTests/Components/NumberedTests.swift +++ b/Tests/NetworkingTests/Components/NumberedTests.swift @@ -5,41 +5,41 @@ import TestSupport import XCTest final class NumberedTests: XCTestCase { - - actor RequestSequenceReporter: NetworkReportingComponent { - var numbers: [(identifier: String, number: Int)] = [] - func didStart(request: HTTPRequestData) { - numbers.append((request.identifier, RequestSequence.number)) - } + + actor RequestSequenceReporter: NetworkReportingComponent { + var numbers: [(identifier: String, number: Int)] = [] + func didStart(request: HTTPRequestData) { + numbers.append((request.identifier, RequestSequence.number)) } - - func test__requests_get_incrementing_sequence_numbers() async throws { - let reporter = RequestSequenceReporter() - - try await withDependencies { - $0.shortID = .incrementing - $0.continuousClock = TestClock() - } operation: { - let request1 = HTTPRequestData(authority: "example.com") - let request2 = HTTPRequestData(authority: "example.co.uk") - let network = TerminalNetworkingComponent(isFailingTerminal: true) - .mocked(request2, stub: .ok()) - .mocked(request1, stub: .ok()) - .reported(by: reporter) - .numbered() - - try await network.data(request1) - try await network.data(request2) - try await network.data(request1) - - let numbers = await reporter.numbers - - XCTAssertEqual(numbers.map(\.identifier), [ - request1.identifier, - request2.identifier, - request1.identifier - ]) - XCTAssertEqual(numbers.map(\.number), [1, 2, 3]) - } + } + + func test__requests_get_incrementing_sequence_numbers() async throws { + let reporter = RequestSequenceReporter() + + try await withDependencies { + $0.shortID = .incrementing + $0.continuousClock = TestClock() + } operation: { + let request1 = HTTPRequestData(authority: "example.com") + let request2 = HTTPRequestData(authority: "example.co.uk") + let network = TerminalNetworkingComponent(isFailingTerminal: true) + .mocked(request2, stub: .ok()) + .mocked(request1, stub: .ok()) + .reported(by: reporter) + .numbered() + + try await network.data(request1) + try await network.data(request2) + try await network.data(request1) + + let numbers = await reporter.numbers + + XCTAssertEqual(numbers.map(\.identifier), [ + request1.identifier, + request2.identifier, + request1.identifier + ]) + XCTAssertEqual(numbers.map(\.number), [1, 2, 3]) } + } } diff --git a/Tests/NetworkingTests/Components/RetryTests.swift b/Tests/NetworkingTests/Components/RetryTests.swift index 64466498..66fa635f 100644 --- a/Tests/NetworkingTests/Components/RetryTests.swift +++ b/Tests/NetworkingTests/Components/RetryTests.swift @@ -8,197 +8,197 @@ import XCTest @testable import Networking final class RetryTests: XCTestCase { - - final class RetryingMock { - var stubs: [StubbedResponseStream] - init(stubs: [StubbedResponseStream]) { - self.stubs = stubs.reversed() - } - - func send(upstream: NetworkingComponent, request: HTTPRequestData) throws -> ResponseStream { - guard let stub = stubs.popLast() else { - throw "Exhausted supply of stub responses" - } - return stub(request) - } + + final class RetryingMock { + var stubs: [StubbedResponseStream] + init(stubs: [StubbedResponseStream]) { + self.stubs = stubs.reversed() } - - func test__basic_retry() async throws { - let clock = ImmediateClock() - let data = try XCTUnwrap("Hello".data(using: .utf8)) - let retryingMock = RetryingMock( - stubs: [ - .init(.throwing, response: .init(status: .badGateway)), - .init(.throwing, response: .init(status: .badGateway)), - .ok(data: data) - ] - ) - - try await withDependencies { - $0.date = .constant(Date()) - $0.calendar = .current - $0.shortID = .incrementing - $0.continuousClock = clock - } operation: { - let request = HTTPRequestData(authority: "example.com") - - let network = TerminalNetworkingComponent() - .mocked { upstream, request in - do { - return try retryingMock.send(upstream: upstream, request: request) - } catch { - XCTFail(String(describing: error)) - return .finished(throwing: error) - } - } - .automaticRetry() - .logged(using: .test) - - let response = try await network.data(request, timeout: .seconds(60), using: TestClock()) - - XCTAssertEqual(response.data, data) - XCTAssertTrue(retryingMock.stubs.isEmpty) - } + + func send(upstream: NetworkingComponent, request: HTTPRequestData) throws -> ResponseStream { + guard let stub = stubs.popLast() else { + throw "Exhausted supply of stub responses" + } + return stub(request) } - - func test__given_no_retry_strategy() async { - withDependencies { - $0.shortID = .incrementing - $0.continuousClock = TestClock() - } operation: { - var request = HTTPRequestData(authority: "example.com") - request.retryingStrategy = nil + } + + func test__basic_retry() async throws { + let clock = ImmediateClock() + let data = try XCTUnwrap("Hello".data(using: .utf8)) + let retryingMock = RetryingMock( + stubs: [ + .init(.throwing, response: .init(status: .badGateway)), + .init(.throwing, response: .init(status: .badGateway)), + .ok(data: data) + ] + ) + + try await withDependencies { + $0.date = .constant(Date()) + $0.calendar = .current + $0.shortID = .incrementing + $0.continuousClock = clock + } operation: { + let request = HTTPRequestData(authority: "example.com") + + let network = TerminalNetworkingComponent() + .mocked { upstream, request in + do { + return try retryingMock.send(upstream: upstream, request: request) + } catch { + XCTFail(String(describing: error)) + return .finished(throwing: error) + } } + .automaticRetry() + .logged(using: .test) + + let response = try await network.data(request, timeout: .seconds(60), using: TestClock()) + + XCTAssertEqual(response.data, data) + XCTAssertTrue(retryingMock.stubs.isEmpty) } - - func test__default_behaviour__constant_backoff() async { - let request = HTTPRequestData(id: .init("1"), authority: "example.com") - XCTAssertTrue(request.supportsRetryingRequests) - let strategy = request.retryingStrategy - var delay = await strategy?.retryDelay( - request: request, - after: [.failure("Some Error")], - date: Date(), - calendar: .current - ) - XCTAssertEqual(delay, .seconds(3)) - delay = await strategy?.retryDelay( - request: request, - after: [.failure("Some Error"), .failure("Some Error")], - date: Date(), - calendar: .current - ) - XCTAssertEqual(delay, .seconds(3)) - delay = await strategy?.retryDelay( - request: request, - after: [.failure("Some Error"), .failure("Some Error"), .failure("Some Error")], - date: Date(), - calendar: .current - ) - XCTAssertNil(delay) + } + + func test__given_no_retry_strategy() async { + withDependencies { + $0.shortID = .incrementing + $0.continuousClock = TestClock() + } operation: { + var request = HTTPRequestData(authority: "example.com") + request.retryingStrategy = nil } + } + + func test__default_behaviour__constant_backoff() async { + let request = HTTPRequestData(id: .init("1"), authority: "example.com") + XCTAssertTrue(request.supportsRetryingRequests) + let strategy = request.retryingStrategy + var delay = await strategy?.retryDelay( + request: request, + after: [.failure("Some Error")], + date: Date(), + calendar: .current + ) + XCTAssertEqual(delay, .seconds(3)) + delay = await strategy?.retryDelay( + request: request, + after: [.failure("Some Error"), .failure("Some Error")], + date: Date(), + calendar: .current + ) + XCTAssertEqual(delay, .seconds(3)) + delay = await strategy?.retryDelay( + request: request, + after: [.failure("Some Error"), .failure("Some Error"), .failure("Some Error")], + date: Date(), + calendar: .current + ) + XCTAssertNil(delay) + } } final class RetryStrategyTests: XCTestCase { - func test_constant_backoff() async { - let request = HTTPRequestData(id: .init("1"), authority: "example.com") - let strategy = BackoffRetryStrategy.constant(delay: .seconds(1), maxAttemptCount: 3) - var delay = await strategy.retryDelay( - request: request, - after: [.failure("Some Error")], - date: Date(), - calendar: .current - ) - XCTAssertEqual(delay, .seconds(1)) - delay = await strategy.retryDelay( - request: request, - after: [.failure("Some Error"), .failure("Some Error")], - date: Date(), - calendar: .current - ) - XCTAssertEqual(delay, .seconds(1)) - delay = await strategy.retryDelay( - request: request, - after: [.failure("Some Error"), .failure("Some Error"), .failure("Some Error")], - date: Date(), - calendar: .current - ) - XCTAssertNil(delay) - } - - func test_exponential_backoff() async { - let request = HTTPRequestData(id: .init("1"), authority: "example.com") - let strategy = BackoffRetryStrategy.exponential(maxDelay: .seconds(20), maxAttemptCount: 6) - var delay = await strategy.retryDelay( - request: request, - after: [.failure("Some Error")], - date: Date(), - calendar: .current - ) - XCTAssertEqual(delay?.components.seconds, 2) - delay = await strategy.retryDelay( - request: request, - after: [.failure("Some Error"), .failure("Some Error")], - date: Date(), - calendar: .current - ) - XCTAssertEqual(delay?.components.seconds, 4) - delay = await strategy.retryDelay( - request: request, - after: [.failure("Some Error"), .failure("Some Error"), .failure("Some Error")], - date: Date(), - calendar: .current - ) - XCTAssertEqual(delay?.components.seconds, 8) - delay = await strategy.retryDelay( - request: request, - after: [.failure("Some Error"), .failure("Some Error"), .failure("Some Error"), .failure("Some Error")], - date: Date(), - calendar: .current - ) - XCTAssertEqual(delay?.components.seconds, 16) - delay = await strategy.retryDelay( - request: request, - after: [.failure("Some Error"), .failure("Some Error"), .failure("Some Error"), .failure("Some Error"), - .failure("Some Error")], - date: Date(), - calendar: .current - ) - XCTAssertEqual(delay?.components.seconds, 20) - delay = await strategy.retryDelay( - request: request, - after: [.failure("Some Error"), .failure("Some Error"), .failure("Some Error"), .failure("Some Error"), - .failure("Some Error"), .failure("Some Error")], - date: Date(), - calendar: .current - ) - XCTAssertNil(delay) - } - - func test_immediate_backoff() async { - let request = HTTPRequestData(id: .init("1"), authority: "example.com") - let strategy = BackoffRetryStrategy.immediate(maxAttemptCount: 3) - var delay = await strategy.retryDelay( - request: request, - after: [.failure("Some Error")], - date: Date(), - calendar: .current - ) - XCTAssertEqual(delay, .zero) - delay = await strategy.retryDelay( - request: request, - after: [.failure("Some Error"), .failure("Some Error")], - date: Date(), - calendar: .current - ) - XCTAssertEqual(delay, .zero) - delay = await strategy.retryDelay( - request: request, - after: [.failure("Some Error"), .failure("Some Error"), .failure("Some Error")], - date: Date(), - calendar: .current - ) - XCTAssertNil(delay) - - } + func test_constant_backoff() async { + let request = HTTPRequestData(id: .init("1"), authority: "example.com") + let strategy = BackoffRetryStrategy.constant(delay: .seconds(1), maxAttemptCount: 3) + var delay = await strategy.retryDelay( + request: request, + after: [.failure("Some Error")], + date: Date(), + calendar: .current + ) + XCTAssertEqual(delay, .seconds(1)) + delay = await strategy.retryDelay( + request: request, + after: [.failure("Some Error"), .failure("Some Error")], + date: Date(), + calendar: .current + ) + XCTAssertEqual(delay, .seconds(1)) + delay = await strategy.retryDelay( + request: request, + after: [.failure("Some Error"), .failure("Some Error"), .failure("Some Error")], + date: Date(), + calendar: .current + ) + XCTAssertNil(delay) + } + + func test_exponential_backoff() async { + let request = HTTPRequestData(id: .init("1"), authority: "example.com") + let strategy = BackoffRetryStrategy.exponential(maxDelay: .seconds(20), maxAttemptCount: 6) + var delay = await strategy.retryDelay( + request: request, + after: [.failure("Some Error")], + date: Date(), + calendar: .current + ) + XCTAssertEqual(delay?.components.seconds, 2) + delay = await strategy.retryDelay( + request: request, + after: [.failure("Some Error"), .failure("Some Error")], + date: Date(), + calendar: .current + ) + XCTAssertEqual(delay?.components.seconds, 4) + delay = await strategy.retryDelay( + request: request, + after: [.failure("Some Error"), .failure("Some Error"), .failure("Some Error")], + date: Date(), + calendar: .current + ) + XCTAssertEqual(delay?.components.seconds, 8) + delay = await strategy.retryDelay( + request: request, + after: [.failure("Some Error"), .failure("Some Error"), .failure("Some Error"), .failure("Some Error")], + date: Date(), + calendar: .current + ) + XCTAssertEqual(delay?.components.seconds, 16) + delay = await strategy.retryDelay( + request: request, + after: [.failure("Some Error"), .failure("Some Error"), .failure("Some Error"), .failure("Some Error"), + .failure("Some Error")], + date: Date(), + calendar: .current + ) + XCTAssertEqual(delay?.components.seconds, 20) + delay = await strategy.retryDelay( + request: request, + after: [.failure("Some Error"), .failure("Some Error"), .failure("Some Error"), .failure("Some Error"), + .failure("Some Error"), .failure("Some Error")], + date: Date(), + calendar: .current + ) + XCTAssertNil(delay) + } + + func test_immediate_backoff() async { + let request = HTTPRequestData(id: .init("1"), authority: "example.com") + let strategy = BackoffRetryStrategy.immediate(maxAttemptCount: 3) + var delay = await strategy.retryDelay( + request: request, + after: [.failure("Some Error")], + date: Date(), + calendar: .current + ) + XCTAssertEqual(delay, .zero) + delay = await strategy.retryDelay( + request: request, + after: [.failure("Some Error"), .failure("Some Error")], + date: Date(), + calendar: .current + ) + XCTAssertEqual(delay, .zero) + delay = await strategy.retryDelay( + request: request, + after: [.failure("Some Error"), .failure("Some Error"), .failure("Some Error")], + date: Date(), + calendar: .current + ) + XCTAssertNil(delay) + + } } diff --git a/Tests/NetworkingTests/Components/ServerTests.swift b/Tests/NetworkingTests/Components/ServerTests.swift index 331b7dee..7ddb13c0 100644 --- a/Tests/NetworkingTests/Components/ServerTests.swift +++ b/Tests/NetworkingTests/Components/ServerTests.swift @@ -9,101 +9,101 @@ import XCTest @testable import Networking final class ServerTests: XCTestCase { - - func test__set_authority_on_all_requests() async throws { - let reporter = TestReporter() - try await withDependencies { - $0.shortID = .incrementing - $0.continuousClock = TestClock() - } operation: { - var headerFields = HTTPFields() - headerFields[.contentType] = "application/json" - headerFields[.cookie] = "cookie" - - let network = TerminalNetworkingComponent() - .mocked(HTTPRequestData(authority: "example.com"), stub: .ok()) - .reported(by: reporter) - .server(authority: "example.com") - .logged(using: Logger()) - - try await network.data(HTTPRequestData()) - - let sentRequests = await reporter.requests - XCTAssertEqual(sentRequests.map(\.authority), ["example.com"]) - } + + func test__set_authority_on_all_requests() async throws { + let reporter = TestReporter() + try await withDependencies { + $0.shortID = .incrementing + $0.continuousClock = TestClock() + } operation: { + var headerFields = HTTPFields() + headerFields[.contentType] = "application/json" + headerFields[.cookie] = "cookie" + + let network = TerminalNetworkingComponent() + .mocked(HTTPRequestData(authority: "example.com"), stub: .ok()) + .reported(by: reporter) + .server(authority: "example.com") + .logged(using: Logger()) + + try await network.data(HTTPRequestData()) + + let sentRequests = await reporter.requests + XCTAssertEqual(sentRequests.map(\.authority), ["example.com"]) } - - func test__set_path_prefix_on_all_requests() async throws { - let reporter = TestReporter() - try await withDependencies { - $0.shortID = .incrementing - $0.continuousClock = TestClock() - } operation: { - var headerFields = HTTPFields() - headerFields[.contentType] = "application/json" - headerFields[.cookie] = "cookie" - - let network = TerminalNetworkingComponent() - .mocked(HTTPRequestData(authority: "example.com", path: "v1/hello"), stub: .ok()) - .reported(by: reporter) - .server(prefixPath: "v1") - .server(authority: "example.com") - .logged(using: Logger()) - - try await network.data(HTTPRequestData(path: "hello")) - - let sentRequests = await reporter.requests - XCTAssertEqual(sentRequests.map(\.authority), ["example.com"]) - XCTAssertEqual(sentRequests.map(\.path), ["v1/hello"]) - } + } + + func test__set_path_prefix_on_all_requests() async throws { + let reporter = TestReporter() + try await withDependencies { + $0.shortID = .incrementing + $0.continuousClock = TestClock() + } operation: { + var headerFields = HTTPFields() + headerFields[.contentType] = "application/json" + headerFields[.cookie] = "cookie" + + let network = TerminalNetworkingComponent() + .mocked(HTTPRequestData(authority: "example.com", path: "v1/hello"), stub: .ok()) + .reported(by: reporter) + .server(prefixPath: "v1") + .server(authority: "example.com") + .logged(using: Logger()) + + try await network.data(HTTPRequestData(path: "hello")) + + let sentRequests = await reporter.requests + XCTAssertEqual(sentRequests.map(\.authority), ["example.com"]) + XCTAssertEqual(sentRequests.map(\.path), ["v1/hello"]) } - - func test__set_path_prefix_on_all_requests__with_empty_path() async throws { - let reporter = TestReporter() - try await withDependencies { - $0.shortID = .incrementing - $0.continuousClock = TestClock() - } operation: { - var headerFields = HTTPFields() - headerFields[.contentType] = "application/json" - headerFields[.cookie] = "cookie" - - let network = TerminalNetworkingComponent() - .mocked(HTTPRequestData(authority: "example.com", path: "v1"), stub: .ok()) - .reported(by: reporter) - .server(prefixPath: "v1") - .server(authority: "example.com") - .logged(using: Logger()) - - try await network.data(HTTPRequestData()) - - let sentRequests = await reporter.requests - XCTAssertEqual(sentRequests.map(\.authority), ["example.com"]) - XCTAssertEqual(sentRequests.map(\.path), ["v1"]) - } + } + + func test__set_path_prefix_on_all_requests__with_empty_path() async throws { + let reporter = TestReporter() + try await withDependencies { + $0.shortID = .incrementing + $0.continuousClock = TestClock() + } operation: { + var headerFields = HTTPFields() + headerFields[.contentType] = "application/json" + headerFields[.cookie] = "cookie" + + let network = TerminalNetworkingComponent() + .mocked(HTTPRequestData(authority: "example.com", path: "v1"), stub: .ok()) + .reported(by: reporter) + .server(prefixPath: "v1") + .server(authority: "example.com") + .logged(using: Logger()) + + try await network.data(HTTPRequestData()) + + let sentRequests = await reporter.requests + XCTAssertEqual(sentRequests.map(\.authority), ["example.com"]) + XCTAssertEqual(sentRequests.map(\.path), ["v1"]) } - - func test__set_default_headers() async throws { - let reporter = TestReporter() - try await withDependencies { - $0.shortID = .incrementing - $0.continuousClock = TestClock() - } operation: { - var headerFields = HTTPFields() - headerFields[.contentType] = "application/json" - - let network = TerminalNetworkingComponent() - .mocked(HTTPRequestData(authority: "example.com", headerFields: headerFields), stub: .ok()) - .reported(by: reporter) - .server(headerField: .contentType, value: "application/json") - .server(authority: "example.com") - .logged(using: Logger()) - - try await network.data(HTTPRequestData()) - - let sentRequests = await reporter.requests - XCTAssertEqual(sentRequests.map(\.authority), ["example.com"]) - XCTAssertEqual(sentRequests.map(\.headerFields).compactMap { $0[.contentType] }, ["application/json"]) - } + } + + func test__set_default_headers() async throws { + let reporter = TestReporter() + try await withDependencies { + $0.shortID = .incrementing + $0.continuousClock = TestClock() + } operation: { + var headerFields = HTTPFields() + headerFields[.contentType] = "application/json" + + let network = TerminalNetworkingComponent() + .mocked(HTTPRequestData(authority: "example.com", headerFields: headerFields), stub: .ok()) + .reported(by: reporter) + .server(headerField: .contentType, value: "application/json") + .server(authority: "example.com") + .logged(using: Logger()) + + try await network.data(HTTPRequestData()) + + let sentRequests = await reporter.requests + XCTAssertEqual(sentRequests.map(\.authority), ["example.com"]) + XCTAssertEqual(sentRequests.map(\.headerFields).compactMap { $0[.contentType] }, ["application/json"]) } + } } diff --git a/Tests/NetworkingTests/Components/ThrottledTests.swift b/Tests/NetworkingTests/Components/ThrottledTests.swift index 93206c60..888be1e3 100644 --- a/Tests/NetworkingTests/Components/ThrottledTests.swift +++ b/Tests/NetworkingTests/Components/ThrottledTests.swift @@ -6,39 +6,39 @@ import TestSupport import XCTest final class ThrottledTests: XCTestCase { - - func test__basics() async throws { - let data = try XCTUnwrap("Hello".data(using: .utf8)) - let reporter = TestReporter() - - try await withDependencies { - $0.shortID = .incrementing - $0.continuousClock = TestClock() - } operation: { - try await withMainSerialExecutor { - let request = HTTPRequestData(authority: "example.com") - let network = TerminalNetworkingComponent() - .mocked(request, stub: .ok(data: data)) - .reported(by: reporter) - .throttled(max: 5) - - try await withThrowingTaskGroup(of: HTTPResponseData.self) { group in - for _ in 0..<100 { - group.addTask { - try await network.data(HTTPRequestData(authority: "example.com")) - } - } - - var responses: [HTTPResponseData] = [] - for try await response in group { - responses.append(response) - } - XCTAssertEqual(responses.count, 100) - } - - let peakActiveRequests = await reporter.peakActiveRequests - XCTAssertEqual(peakActiveRequests, 5) + + func test__basics() async throws { + let data = try XCTUnwrap("Hello".data(using: .utf8)) + let reporter = TestReporter() + + try await withDependencies { + $0.shortID = .incrementing + $0.continuousClock = TestClock() + } operation: { + try await withMainSerialExecutor { + let request = HTTPRequestData(authority: "example.com") + let network = TerminalNetworkingComponent() + .mocked(request, stub: .ok(data: data)) + .reported(by: reporter) + .throttled(max: 5) + + try await withThrowingTaskGroup(of: HTTPResponseData.self) { group in + for _ in 0..<100 { + group.addTask { + try await network.data(HTTPRequestData(authority: "example.com")) } + } + + var responses: [HTTPResponseData] = [] + for try await response in group { + responses.append(response) + } + XCTAssertEqual(responses.count, 100) } + + let peakActiveRequests = await reporter.peakActiveRequests + XCTAssertEqual(peakActiveRequests, 5) + } } + } } diff --git a/Tests/NetworkingTests/Core/BytesReceivedTests.swift b/Tests/NetworkingTests/Core/BytesReceivedTests.swift index 7c57afcb..bd82ae35 100644 --- a/Tests/NetworkingTests/Core/BytesReceivedTests.swift +++ b/Tests/NetworkingTests/Core/BytesReceivedTests.swift @@ -7,48 +7,48 @@ import XCTest @testable import Networking final class BytesReceivedTests: XCTestCase { - func test__init_data() throws { - let data = try XCTUnwrap("Hello World".data(using: .utf8)) - let bytes = BytesReceived(data: data) - XCTAssertEqual(bytes.received, 11) - XCTAssertEqual(bytes.expected, 11) - } - - func test__receive_bytes() { - var bytes = BytesReceived(expected: 256) - bytes.receiveBytes(count: 64) - bytes.receiveBytes(count: 64) - XCTAssertEqual(bytes.received, 128) - XCTAssertEqual(bytes.expected, 256) - } - - func test__add() { - let lhs = BytesReceived(received: 10, expected: 10) - let rhs = BytesReceived(received: 20, expected: 20) - XCTAssertEqual(lhs + rhs, BytesReceived(received: 30, expected: 30)) - } - - func test__fraction_completed() { - var bytes = BytesReceived(received: 10, expected: 100) - XCTAssertEqual(bytes.fractionCompleted, 0.1, accuracy: 0.00001) - bytes.receiveBytes(count: 50) - XCTAssertEqual(bytes.fractionCompleted, 0.6, accuracy: 0.00001) - } - - func test__with_expected_bytes_from_request__default() throws { - let request = HTTPRequestData(id: .init("1"), authority: "example.com") - let data = try XCTUnwrap("Hello World".data(using: .utf8)) - let bytes = BytesReceived(data: data).withExpectedContentLength(from: request) - XCTAssertEqual(bytes.received, 11) - XCTAssertEqual(bytes.expected, 11) - } - - func test__with_expected_bytes_from_request() throws { - var request = HTTPRequestData(id: .init("1"), authority: "example.com") - request.expectedContentLength = 100 - let data = try XCTUnwrap("Hello World".data(using: .utf8)) - let bytes = BytesReceived(data: data).withExpectedContentLength(from: request) - XCTAssertEqual(bytes.received, 11) - XCTAssertEqual(bytes.expected, 100) - } + func test__init_data() throws { + let data = try XCTUnwrap("Hello World".data(using: .utf8)) + let bytes = BytesReceived(data: data) + XCTAssertEqual(bytes.received, 11) + XCTAssertEqual(bytes.expected, 11) + } + + func test__receive_bytes() { + var bytes = BytesReceived(expected: 256) + bytes.receiveBytes(count: 64) + bytes.receiveBytes(count: 64) + XCTAssertEqual(bytes.received, 128) + XCTAssertEqual(bytes.expected, 256) + } + + func test__add() { + let lhs = BytesReceived(received: 10, expected: 10) + let rhs = BytesReceived(received: 20, expected: 20) + XCTAssertEqual(lhs + rhs, BytesReceived(received: 30, expected: 30)) + } + + func test__fraction_completed() { + var bytes = BytesReceived(received: 10, expected: 100) + XCTAssertEqual(bytes.fractionCompleted, 0.1, accuracy: 0.00001) + bytes.receiveBytes(count: 50) + XCTAssertEqual(bytes.fractionCompleted, 0.6, accuracy: 0.00001) + } + + func test__with_expected_bytes_from_request__default() throws { + let request = HTTPRequestData(id: .init("1"), authority: "example.com") + let data = try XCTUnwrap("Hello World".data(using: .utf8)) + let bytes = BytesReceived(data: data).withExpectedContentLength(from: request) + XCTAssertEqual(bytes.received, 11) + XCTAssertEqual(bytes.expected, 11) + } + + func test__with_expected_bytes_from_request() throws { + var request = HTTPRequestData(id: .init("1"), authority: "example.com") + request.expectedContentLength = 100 + let data = try XCTUnwrap("Hello World".data(using: .utf8)) + let bytes = BytesReceived(data: data).withExpectedContentLength(from: request) + XCTAssertEqual(bytes.received, 11) + XCTAssertEqual(bytes.expected, 100) + } } diff --git a/Tests/NetworkingTests/Core/ExpectedContentLengthOptionTests.swift b/Tests/NetworkingTests/Core/ExpectedContentLengthOptionTests.swift index 55422f63..287286d4 100644 --- a/Tests/NetworkingTests/Core/ExpectedContentLengthOptionTests.swift +++ b/Tests/NetworkingTests/Core/ExpectedContentLengthOptionTests.swift @@ -6,10 +6,10 @@ import XCTest @testable import Networking final class ExpectedContentLengthOptionTests: XCTestCase { - func test__expected_content_length_option() { - var request = HTTPRequestData(id: .init("1"), authority: "example.com") - XCTAssertNil(request.expectedContentLength) - request.expectedContentLength = 100 - XCTAssertEqual(request.expectedContentLength, 100) - } + func test__expected_content_length_option() { + var request = HTTPRequestData(id: .init("1"), authority: "example.com") + XCTAssertNil(request.expectedContentLength) + request.expectedContentLength = 100 + XCTAssertEqual(request.expectedContentLength, 100) + } } diff --git a/Tests/NetworkingTests/Core/HTTPRequestDataTests.swift b/Tests/NetworkingTests/Core/HTTPRequestDataTests.swift index f82f640f..0a149275 100644 --- a/Tests/NetworkingTests/Core/HTTPRequestDataTests.swift +++ b/Tests/NetworkingTests/Core/HTTPRequestDataTests.swift @@ -6,153 +6,153 @@ import XCTest @testable import Networking final class HTTPRequestDataTests: XCTestCase { - override func invokeTest() { - withDependencies { - $0.shortID = .incrementing - } operation: { - super.invokeTest() - } - } - - func test__basics() { - var request = HTTPRequestData( - id: .init("some id"), - method: .get, - scheme: "https", - authority: "example.com", - path: "example", - headerFields: [:], - body: nil - ) - - XCTAssertEqual(request.id, Tagged(rawValue: "some id")) - XCTAssertEqual(request.method, .get) - XCTAssertEqual(request.scheme, "https") - XCTAssertEqual(request.authority, "example.com") - XCTAssertEqual(request.path, "example") - XCTAssertEqual(request.headerFields, [:]) - XCTAssertNil(request.body) - - request.method = .post - XCTAssertEqual(request.method, .post) - - request.scheme = "abc" - XCTAssertEqual(request.scheme, "abc") - - request.authority = "example.co.uk" - XCTAssertEqual(request.authority, "example.co.uk") - - request.path = "example/another" - XCTAssertEqual(request.path, "example/another") - - request.headerFields = [ - .contentType: "application/json", - .accept: "application/json", - .cacheControl: "no-cache" - ] - XCTAssertEqual(request.headerFields, [ - .contentType: "application/json", - .accept: "application/json", - .cacheControl: "no-cache" - ]) - XCTAssertNil(request.body) - } - - func test__short_id() { - let request = HTTPRequestData( - authority: "example.com" - ) - XCTAssertEqual(request.identifier, "000001") - } - - func test__options() { - var request1 = HTTPRequestData( - id: .init("some id"), - authority: "example.com" - ) - - XCTAssertEqual(request1.testOption, "Hello World") - request1.testOption = "Goodbye" - XCTAssertEqual(request1.testOption, "Goodbye") - - var request2 = HTTPRequestData( - id: .init("some id"), - authority: "example.com" - ) - - // By default request options are not considered when - // evaluating equality - XCTAssertEqual(request1, request2) - - // Request options can override this behaviour, and signal that - // they should be considered for equality - request2.testEqualOption = "Goodbye" - XCTAssertNotEqual(request1, request2) - - request1.testEqualOption = "Hello Again" - XCTAssertNotEqual(request1, request2) - - request1.testEqualOption = "Goodbye" - XCTAssertEqual(request1, request2) - } - - func test__description() { - var request = HTTPRequestData( - id: .init("some id"), - authority: "example.com" - ) - XCTAssertEqual(request.debugDescription, "[0:some id] (GET) https://example.com/") - - request.scheme = "abc" - request.method = .post - request.path = "/hello" - XCTAssertEqual(request.debugDescription, "[0:some id] (POST) abc://example.com/hello") - } - - func test__foundation_url_request_with_path() throws { - let request = HTTPRequestData( - id: .init("some id"), - method: .get, - scheme: "https", - authority: "example.com", - path: "example", - headerFields: [:], - body: nil - ) - - let urlRequest = try XCTUnwrap(URLRequest(http: request)) - XCTAssertEqual(urlRequest.url?.absoluteString, "https://example.com/example") - } - - func test__foundation_url_request_minimum_arguments() throws { - let request = HTTPRequestData( - authority: "example.com" - ) - - let urlRequest = try XCTUnwrap(URLRequest(http: request)) - XCTAssertEqual(urlRequest.url?.absoluteString, "https://example.com/") + override func invokeTest() { + withDependencies { + $0.shortID = .incrementing + } operation: { + super.invokeTest() } + } + + func test__basics() { + var request = HTTPRequestData( + id: .init("some id"), + method: .get, + scheme: "https", + authority: "example.com", + path: "example", + headerFields: [:], + body: nil + ) + + XCTAssertEqual(request.id, Tagged(rawValue: "some id")) + XCTAssertEqual(request.method, .get) + XCTAssertEqual(request.scheme, "https") + XCTAssertEqual(request.authority, "example.com") + XCTAssertEqual(request.path, "example") + XCTAssertEqual(request.headerFields, [:]) + XCTAssertNil(request.body) + + request.method = .post + XCTAssertEqual(request.method, .post) + + request.scheme = "abc" + XCTAssertEqual(request.scheme, "abc") + + request.authority = "example.co.uk" + XCTAssertEqual(request.authority, "example.co.uk") + + request.path = "example/another" + XCTAssertEqual(request.path, "example/another") + + request.headerFields = [ + .contentType: "application/json", + .accept: "application/json", + .cacheControl: "no-cache" + ] + XCTAssertEqual(request.headerFields, [ + .contentType: "application/json", + .accept: "application/json", + .cacheControl: "no-cache" + ]) + XCTAssertNil(request.body) + } + + func test__short_id() { + let request = HTTPRequestData( + authority: "example.com" + ) + XCTAssertEqual(request.identifier, "000001") + } + + func test__options() { + var request1 = HTTPRequestData( + id: .init("some id"), + authority: "example.com" + ) + + XCTAssertEqual(request1.testOption, "Hello World") + request1.testOption = "Goodbye" + XCTAssertEqual(request1.testOption, "Goodbye") + + var request2 = HTTPRequestData( + id: .init("some id"), + authority: "example.com" + ) + + // By default request options are not considered when + // evaluating equality + XCTAssertEqual(request1, request2) + + // Request options can override this behaviour, and signal that + // they should be considered for equality + request2.testEqualOption = "Goodbye" + XCTAssertNotEqual(request1, request2) + + request1.testEqualOption = "Hello Again" + XCTAssertNotEqual(request1, request2) + + request1.testEqualOption = "Goodbye" + XCTAssertEqual(request1, request2) + } + + func test__description() { + var request = HTTPRequestData( + id: .init("some id"), + authority: "example.com" + ) + XCTAssertEqual(request.debugDescription, "[0:some id] (GET) https://example.com/") + + request.scheme = "abc" + request.method = .post + request.path = "/hello" + XCTAssertEqual(request.debugDescription, "[0:some id] (POST) abc://example.com/hello") + } + + func test__foundation_url_request_with_path() throws { + let request = HTTPRequestData( + id: .init("some id"), + method: .get, + scheme: "https", + authority: "example.com", + path: "example", + headerFields: [:], + body: nil + ) + + let urlRequest = try XCTUnwrap(URLRequest(http: request)) + XCTAssertEqual(urlRequest.url?.absoluteString, "https://example.com/example") + } + + func test__foundation_url_request_minimum_arguments() throws { + let request = HTTPRequestData( + authority: "example.com" + ) + + let urlRequest = try XCTUnwrap(URLRequest(http: request)) + XCTAssertEqual(urlRequest.url?.absoluteString, "https://example.com/") + } } private struct TestOption: HTTPRequestDataOption { - static var defaultOption: String = "Hello World" + static var defaultOption: String = "Hello World" } private extension HTTPRequestData { - var testOption: TestOption.Value { - get { self[option: TestOption.self] } - set { self[option: TestOption.self] = newValue } - } + var testOption: TestOption.Value { + get { self[option: TestOption.self] } + set { self[option: TestOption.self] = newValue } + } } private struct TestEqualOption: HTTPRequestDataOption { - static var defaultOption: String = "Hello World" - static var includeInEqualityEvaluation: Bool { true } + static var defaultOption: String = "Hello World" + static var includeInEqualityEvaluation: Bool { true } } private extension HTTPRequestData { - var testEqualOption: TestEqualOption.Value { - get { self[option: TestEqualOption.self] } - set { self[option: TestEqualOption.self] = newValue } - } + var testEqualOption: TestEqualOption.Value { + get { self[option: TestEqualOption.self] } + set { self[option: TestEqualOption.self] = newValue } + } } diff --git a/Tests/NetworkingTests/Core/NetworkingComponent+DataTests.swift b/Tests/NetworkingTests/Core/NetworkingComponent+DataTests.swift index 1242f108..32010519 100644 --- a/Tests/NetworkingTests/Core/NetworkingComponent+DataTests.swift +++ b/Tests/NetworkingTests/Core/NetworkingComponent+DataTests.swift @@ -9,83 +9,83 @@ import ShortID @testable import Networking final class NetworkingComponentDataTests: XCTestCase { - - func test__basic_data() async throws { - try await withDependencies { - $0.shortID = .incrementing - $0.continuousClock = TestClock() - } operation: { - let request = HTTPRequestData(authority: "example.com") - let data = try XCTUnwrap("Hello World".data(using: .utf8)) - let network = TerminalNetworkingComponent() - .mocked(request, stub: .ok(data: data)) - - let response = try await network.data(request) - - XCTAssertEqual(response.data, data) - } + + func test__basic_data() async throws { + try await withDependencies { + $0.shortID = .incrementing + $0.continuousClock = TestClock() + } operation: { + let request = HTTPRequestData(authority: "example.com") + let data = try XCTUnwrap("Hello World".data(using: .utf8)) + let network = TerminalNetworkingComponent() + .mocked(request, stub: .ok(data: data)) + + let response = try await network.data(request) + + XCTAssertEqual(response.data, data) } - - func test__basic_data__timeout() async throws { - let clock = TestClock() - try await withDependencies { - $0.shortID = .incrementing - $0.continuousClock = clock - } operation: { - let request = HTTPRequestData(authority: "example.com") - let data = try XCTUnwrap("Hello World".data(using: .utf8)) - let network = TerminalNetworkingComponent() - .mocked(request, stub: .ok(data: data)) - - async let response = network.data(request, timeout: .seconds(2)) - await clock.advance(by: .seconds(3)) - do { - _ = try await response.data - XCTFail("Expected an error to be thrown.") - } catch let error as StackError { - XCTAssertEqual(error, StackError.timeout(request)) - } catch { - XCTFail("Unexpected error \(error)") - } - } + } + + func test__basic_data__timeout() async throws { + let clock = TestClock() + try await withDependencies { + $0.shortID = .incrementing + $0.continuousClock = clock + } operation: { + let request = HTTPRequestData(authority: "example.com") + let data = try XCTUnwrap("Hello World".data(using: .utf8)) + let network = TerminalNetworkingComponent() + .mocked(request, stub: .ok(data: data)) + + async let response = network.data(request, timeout: .seconds(2)) + await clock.advance(by: .seconds(3)) + do { + _ = try await response.data + XCTFail("Expected an error to be thrown.") + } catch let error as StackError { + XCTAssertEqual(error, StackError.timeout(request)) + } catch { + XCTFail("Unexpected error \(error)") + } } - - func test__basic_data_progress() async throws { - actor UpdateProgress { - var bytesReceived: [BytesReceived] = [] - func update(_ bytesReceived: BytesReceived) { - self.bytesReceived.append(bytesReceived) - } - } - let progress = UpdateProgress() - let progressExpectation = expectation(description: "Update progress") - progressExpectation.assertForOverFulfill = true - progressExpectation.expectedFulfillmentCount = 5 - - try await withDependencies { - $0.shortID = .incrementing - $0.continuousClock = TestClock() - } operation: { - let request = HTTPRequestData(authority: "example.com") - let data = try XCTUnwrap("Hello World".data(using: .utf8)) - let network = TerminalNetworkingComponent() - .mocked(request, stub: .ok(data: data)) - - let response = try await network.data(request) { bytesReceived in - progressExpectation.fulfill() - await progress.update(bytesReceived) - } - XCTAssertEqual(response.data, data) - await fulfillment(of: [progressExpectation]) - let bytesReceived = await progress.bytesReceived - XCTAssertEqual(bytesReceived, [ - BytesReceived(received: 2, expected: 11), - BytesReceived(received: 4, expected: 11), - BytesReceived(received: 6, expected: 11), - BytesReceived(received: 8, expected: 11), - BytesReceived(received: 11, expected: 11) - ]) - } - + } + + func test__basic_data_progress() async throws { + actor UpdateProgress { + var bytesReceived: [BytesReceived] = [] + func update(_ bytesReceived: BytesReceived) { + self.bytesReceived.append(bytesReceived) + } + } + let progress = UpdateProgress() + let progressExpectation = expectation(description: "Update progress") + progressExpectation.assertForOverFulfill = true + progressExpectation.expectedFulfillmentCount = 5 + + try await withDependencies { + $0.shortID = .incrementing + $0.continuousClock = TestClock() + } operation: { + let request = HTTPRequestData(authority: "example.com") + let data = try XCTUnwrap("Hello World".data(using: .utf8)) + let network = TerminalNetworkingComponent() + .mocked(request, stub: .ok(data: data)) + + let response = try await network.data(request) { bytesReceived in + progressExpectation.fulfill() + await progress.update(bytesReceived) + } + XCTAssertEqual(response.data, data) + await fulfillment(of: [progressExpectation]) + let bytesReceived = await progress.bytesReceived + XCTAssertEqual(bytesReceived, [ + BytesReceived(received: 2, expected: 11), + BytesReceived(received: 4, expected: 11), + BytesReceived(received: 6, expected: 11), + BytesReceived(received: 8, expected: 11), + BytesReceived(received: 11, expected: 11) + ]) } + + } } diff --git a/Tests/NetworkingTests/Core/PartialTests.swift b/Tests/NetworkingTests/Core/PartialTests.swift index 317a86d9..2501f471 100644 --- a/Tests/NetworkingTests/Core/PartialTests.swift +++ b/Tests/NetworkingTests/Core/PartialTests.swift @@ -7,58 +7,58 @@ import XCTest @testable import Networking final class PartialTests: XCTestCase { - - func test__value() { - XCTAssertNil(Partial.progress(0.1).value) - XCTAssertEqual(Partial.value("Hello", 1.0).value, "Hello") - } - - func test__progress() { - XCTAssertEqual(Partial.progress(0.1).progress, 0.1) - XCTAssertEqual(Partial.value("Hello", 1.0).progress, 1.0) - } - - func test__on_value() { - var didPerformBlock: String? - var partial: Partial = .value("Hello", 1.0) - var onValue = partial.onValue { didPerformBlock = $0 } - XCTAssertEqual(didPerformBlock, "Hello") - XCTAssertEqual(partial, onValue) - - didPerformBlock = nil - partial = .progress(0.1) - onValue = partial.onValue { didPerformBlock = $0 } - XCTAssertNil(didPerformBlock) - XCTAssertEqual(partial, onValue) + + func test__value() { + XCTAssertNil(Partial.progress(0.1).value) + XCTAssertEqual(Partial.value("Hello", 1.0).value, "Hello") + } + + func test__progress() { + XCTAssertEqual(Partial.progress(0.1).progress, 0.1) + XCTAssertEqual(Partial.value("Hello", 1.0).progress, 1.0) + } + + func test__on_value() { + var didPerformBlock: String? + var partial: Partial = .value("Hello", 1.0) + var onValue = partial.onValue { didPerformBlock = $0 } + XCTAssertEqual(didPerformBlock, "Hello") + XCTAssertEqual(partial, onValue) + + didPerformBlock = nil + partial = .progress(0.1) + onValue = partial.onValue { didPerformBlock = $0 } + XCTAssertNil(didPerformBlock) + XCTAssertEqual(partial, onValue) + } + + func test__map_value() { + var partial: Partial = .value("Hello", 1.0) + var mapValue = partial.mapValue { $0.count } + XCTAssertEqual(mapValue, .value(5, 1.0)) + XCTAssertEqual(partial, .value("Hello", 1.0)) + + partial = .progress(0.1) + mapValue = partial.mapValue { $0.count } + XCTAssertEqual(mapValue, .progress(0.1)) + XCTAssertEqual(partial, .progress(0.1)) + } + + func test__map_progress() { + var partial: Partial = .value("Hello", 1.0) + var mapProgress = partial.mapProgress { + BytesReceived(received: Int64(100 * $0), expected: 100) } - - func test__map_value() { - var partial: Partial = .value("Hello", 1.0) - var mapValue = partial.mapValue { $0.count } - XCTAssertEqual(mapValue, .value(5, 1.0)) - XCTAssertEqual(partial, .value("Hello", 1.0)) - - partial = .progress(0.1) - mapValue = partial.mapValue { $0.count } - XCTAssertEqual(mapValue, .progress(0.1)) - XCTAssertEqual(partial, .progress(0.1)) - } - - func test__map_progress() { - var partial: Partial = .value("Hello", 1.0) - var mapProgress = partial.mapProgress { - BytesReceived(received: Int64(100 * $0), expected: 100) - } - XCTAssertEqual(mapProgress, - .value("Hello", BytesReceived(received: 100, expected: 100))) - XCTAssertEqual(partial, .value("Hello", 1.0)) - - partial = .progress(0.1) - mapProgress = partial.mapProgress { - BytesReceived(received: Int64(100 * $0), expected: 100) - } - XCTAssertEqual(mapProgress, - .progress(BytesReceived(received: 10, expected: 100))) - XCTAssertEqual(partial, .progress(0.1)) + XCTAssertEqual(mapProgress, + .value("Hello", BytesReceived(received: 100, expected: 100))) + XCTAssertEqual(partial, .value("Hello", 1.0)) + + partial = .progress(0.1) + mapProgress = partial.mapProgress { + BytesReceived(received: Int64(100 * $0), expected: 100) } + XCTAssertEqual(mapProgress, + .progress(BytesReceived(received: 10, expected: 100))) + XCTAssertEqual(partial, .progress(0.1)) + } } diff --git a/Tests/NetworkingTests/Core/ProgessTrackerTests.swift b/Tests/NetworkingTests/Core/ProgessTrackerTests.swift index 2eccf787..0132cff5 100644 --- a/Tests/NetworkingTests/Core/ProgessTrackerTests.swift +++ b/Tests/NetworkingTests/Core/ProgessTrackerTests.swift @@ -7,40 +7,40 @@ import XCTest @testable import Networking final class ProgressTrackerTests: XCTestCase { - - func test__basics() async { - let tracker = ProgressTracker() - - await tracker.set(id: "Hello", bytesReceived: BytesReceived(received: 10, expected: 100)) - var fraction = await tracker.fractionCompleted() - XCTAssertEqual(fraction, 0.1, accuracy: 0.00001) - - await tracker.set(id: "Hello", bytesReceived: BytesReceived(received: 85, expected: 100)) - fraction = await tracker.fractionCompleted() - XCTAssertEqual(fraction, 0.85, accuracy: 0.00001) - - await tracker.set(id: "World", bytesReceived: BytesReceived(received: 20, expected: 100)) - fraction = await tracker.fractionCompleted() - XCTAssertEqual(fraction, 0.525, accuracy: 0.00001) - - await tracker.set(id: "Hello", bytesReceived: BytesReceived(received: 100, expected: 100)) - fraction = await tracker.fractionCompleted() - XCTAssertEqual(fraction, 0.60, accuracy: 0.00001) - - await tracker.set(id: "World", bytesReceived: BytesReceived(received: 40, expected: 100)) - fraction = await tracker.fractionCompleted() - XCTAssertEqual(fraction, 0.70, accuracy: 0.00001) - - await tracker.remove(id: "Hello") - fraction = await tracker.fractionCompleted() - XCTAssertEqual(fraction, 0.40, accuracy: 0.00001) - - await tracker.set(id: "World", bytesReceived: BytesReceived(received: 80, expected: 100)) - fraction = await tracker.fractionCompleted() - XCTAssertEqual(fraction, 0.80, accuracy: 0.00001) - - await tracker.remove(id: "World") - fraction = await tracker.fractionCompleted() - XCTAssertEqual(fraction, 0.0, accuracy: 0.00001) - } + + func test__basics() async { + let tracker = ProgressTracker() + + await tracker.set(id: "Hello", bytesReceived: BytesReceived(received: 10, expected: 100)) + var fraction = await tracker.fractionCompleted() + XCTAssertEqual(fraction, 0.1, accuracy: 0.00001) + + await tracker.set(id: "Hello", bytesReceived: BytesReceived(received: 85, expected: 100)) + fraction = await tracker.fractionCompleted() + XCTAssertEqual(fraction, 0.85, accuracy: 0.00001) + + await tracker.set(id: "World", bytesReceived: BytesReceived(received: 20, expected: 100)) + fraction = await tracker.fractionCompleted() + XCTAssertEqual(fraction, 0.525, accuracy: 0.00001) + + await tracker.set(id: "Hello", bytesReceived: BytesReceived(received: 100, expected: 100)) + fraction = await tracker.fractionCompleted() + XCTAssertEqual(fraction, 0.60, accuracy: 0.00001) + + await tracker.set(id: "World", bytesReceived: BytesReceived(received: 40, expected: 100)) + fraction = await tracker.fractionCompleted() + XCTAssertEqual(fraction, 0.70, accuracy: 0.00001) + + await tracker.remove(id: "Hello") + fraction = await tracker.fractionCompleted() + XCTAssertEqual(fraction, 0.40, accuracy: 0.00001) + + await tracker.set(id: "World", bytesReceived: BytesReceived(received: 80, expected: 100)) + fraction = await tracker.fractionCompleted() + XCTAssertEqual(fraction, 0.80, accuracy: 0.00001) + + await tracker.remove(id: "World") + fraction = await tracker.fractionCompleted() + XCTAssertEqual(fraction, 0.0, accuracy: 0.00001) + } } diff --git a/Tests/NetworkingTests/Core/RequestTests.swift b/Tests/NetworkingTests/Core/RequestTests.swift index ca47425f..651ca5fd 100644 --- a/Tests/NetworkingTests/Core/RequestTests.swift +++ b/Tests/NetworkingTests/Core/RequestTests.swift @@ -6,33 +6,33 @@ import TestSupport import XCTest final class RequestTests: XCTestCase { - - func test__decoder_basics() async throws { - let json = + + func test__decoder_basics() async throws { + let json = """ {"value":"Hello World"} """ - let data = try XCTUnwrap(json.data(using: .utf8)) - - try await withDependencies { - $0.shortID = .incrementing - $0.continuousClock = TestClock() - } operation: { - let http = HTTPRequestData(authority: "example.com") - let network = TerminalNetworkingComponent() - .mocked(http, stub: .ok(data: data)) - - var (message, response) = try await network.value(Request(http: http)) - XCTAssertEqual(message.value, "Hello World") - XCTAssertEqual(response.status, .ok) - - (message, response) = try await network.value(http, as: Message.self, decoder: JSONDecoder()) - XCTAssertEqual(message.value, "Hello World") - XCTAssertEqual(response.status, .ok) - } + let data = try XCTUnwrap(json.data(using: .utf8)) + + try await withDependencies { + $0.shortID = .incrementing + $0.continuousClock = TestClock() + } operation: { + let http = HTTPRequestData(authority: "example.com") + let network = TerminalNetworkingComponent() + .mocked(http, stub: .ok(data: data)) + + var (message, response) = try await network.value(Request(http: http)) + XCTAssertEqual(message.value, "Hello World") + XCTAssertEqual(response.status, .ok) + + (message, response) = try await network.value(http, as: Message.self, decoder: JSONDecoder()) + XCTAssertEqual(message.value, "Hello World") + XCTAssertEqual(response.status, .ok) } + } } private struct Message: Decodable, Equatable { - let value: String + let value: String } diff --git a/Tests/NetworkingTests/Core/RequestTimeoutOptionTests.swift b/Tests/NetworkingTests/Core/RequestTimeoutOptionTests.swift index 164d1c46..d3946137 100644 --- a/Tests/NetworkingTests/Core/RequestTimeoutOptionTests.swift +++ b/Tests/NetworkingTests/Core/RequestTimeoutOptionTests.swift @@ -6,10 +6,10 @@ import XCTest @testable import Networking final class RequestTimeoutOptionTests: XCTestCase { - func test__request_timeout_option() { - var request = HTTPRequestData(id: .init("1"), authority: "example.com") - XCTAssertEqual(request.requestTimeoutInSeconds, 60) - request.requestTimeoutInSeconds = 100 - XCTAssertEqual(request.requestTimeoutInSeconds, 100) - } + func test__request_timeout_option() { + var request = HTTPRequestData(id: .init("1"), authority: "example.com") + XCTAssertEqual(request.requestTimeoutInSeconds, 60) + request.requestTimeoutInSeconds = 100 + XCTAssertEqual(request.requestTimeoutInSeconds, 100) + } }