diff --git a/NetworkingSampleApp/.swiftlint.yml b/.swiftlint.yml similarity index 97% rename from NetworkingSampleApp/.swiftlint.yml rename to .swiftlint.yml index 434c97a1..e459d116 100755 --- a/NetworkingSampleApp/.swiftlint.yml +++ b/.swiftlint.yml @@ -4,6 +4,8 @@ # Feel free to send pull request or suggest improvements! # +analyzer_rules: + - unused_import # # Rule identifiers to exclude from running. @@ -30,7 +32,6 @@ opt_in_rules: - contains_over_first_not_nil - convenience_type - fallthrough - - unused_import - unavailable_function - strict_fileprivate - explicit_init @@ -42,9 +43,9 @@ opt_in_rules: # Paths to include during linting. `--path` is ignored if present. # included: - - ./ - - ../Sources - - ../Tests + - NetworkingSampleApp + - Sources + - Tests # # Paths to ignore during linting. Takes precedence over `included`. @@ -52,7 +53,6 @@ included: excluded: - Carthage - Pods - - Tests - Scripts - vendor - fastlane diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Networking.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Networking.xcscheme index fb598c66..a5adf7f8 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/Networking.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/Networking.xcscheme @@ -1,6 +1,6 @@ + + + + diff --git a/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/xcshareddata/xcschemes/NetworkingSampleApp.xcscheme b/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/xcshareddata/xcschemes/NetworkingSampleApp.xcscheme index bff24b04..abdb86ec 100644 --- a/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/xcshareddata/xcschemes/NetworkingSampleApp.xcscheme +++ b/NetworkingSampleApp/NetworkingSampleApp.xcodeproj/xcshareddata/xcschemes/NetworkingSampleApp.xcscheme @@ -1,6 +1,6 @@ Void) -> some View { - Button( - action: action, - label: { - Image(systemName: symbol) - .symbolVariant(.circle.fill) - .font(.title2) - .symbolRenderingMode(.hierarchical) - .foregroundStyle(color) - } - ) - .buttonStyle(.plain) - .contentShape(Circle()) - } -} diff --git a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Users/User.swift b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Users/User.swift new file mode 100644 index 00000000..f58b7bab --- /dev/null +++ b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Users/User.swift @@ -0,0 +1,25 @@ +// +// SampleUserResponse.swift +// Networking sample app +// +// Created by Tomas Cejka on 07.04.2021. +// + +import Foundation + +/// Data structure of sample API user response +struct User: Codable, Identifiable { + enum CodingKeys: String, CodingKey { + case id + case email + case firstName = "first_name" + case lastName = "last_name" + case avatarURL = "avatar" + } + + let id: Int + let email: String + let firstName: String + let lastName: String + let avatarURL: URL +} diff --git a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Users/UsersView.swift b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Users/UsersView.swift new file mode 100644 index 00000000..64d4da1f --- /dev/null +++ b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Users/UsersView.swift @@ -0,0 +1,120 @@ +// +// UsersView.swift +// NetworkingSampleApp +// +// Created by Matej Molnár on 07.12.2023. +// + +import SwiftUI + +struct UsersView: View { + @StateObject private var viewModel = UsersViewModel() + + @State private var fromUserID: Int = 1 + @State private var toUserID: Int = 3 + @State private var parallelise = false + @State private var userName: String = "" + @State private var userJob: String = "" + + var body: some View { + Form { + getUserView + + createUserView + } + .navigationTitle("Users") + } +} + +private extension UsersView { + var getUserView: some View { + Group { + Section { + HStack { + Text("From:") + + TextField("From user ID", value: $fromUserID, formatter: NumberFormatter()) + } + + HStack { + Text("To:") + + TextField("To user ID", value: $toUserID, formatter: NumberFormatter()) + } + + Toggle("Parallelise", isOn: $parallelise) + } header: { + Text("Get User by ID") + } footer: { + Button("Get Users") { + viewModel.getUsers( + in: fromUserID...toUserID, + parallelFetch: parallelise + ) + } + .buttonStyle(.borderedProminent) + .frame(maxWidth: .infinity) + } + + if !viewModel.users.isEmpty { + Section("Users") { + ForEach(viewModel.users) { user in + userCell(user) + } + } + } + } + } + + var createUserView: some View { + Group { + Section { + TextField("Name", text: $userName) + TextField("Job", text: $userJob) + } header: { + Text("Create User with parameters") + } footer: { + Button("Create User") { + viewModel.createUser(name: userName, job: userJob) + } + .buttonStyle(.borderedProminent) + .frame(maxWidth: .infinity) + } + + if let createdUser = viewModel.createdUser { + Section("Created User") { + Text("ID: \(createdUser.id)") + Text("Name: \(createdUser.name)") + Text("Job: \(createdUser.job)") + Text("Created at: \(createdUser.createdAt.formatted())") + } + } + } + } + + func userCell(_ user: User) -> some View { + HStack(alignment: .center) { + AsyncImage(url: user.avatarURL) { image in + image + .resizable() + } placeholder: { + Color.gray + } + .frame(width: 70, height: 70) + .clipShape(Circle()) + + VStack(alignment: .leading) { + Text(user.firstName + " " + user.lastName) + .font(.subheadline) + + Text(user.email) + .font(.footnote) + .foregroundStyle(.gray) + } + } + } +} + +#Preview { + UsersView() +} diff --git a/NetworkingSampleApp/NetworkingSampleApp/Scenes/Users/UsersViewModel.swift b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Users/UsersViewModel.swift new file mode 100644 index 00000000..4d3e020f --- /dev/null +++ b/NetworkingSampleApp/NetworkingSampleApp/Scenes/Users/UsersViewModel.swift @@ -0,0 +1,82 @@ +// +// UsersViewModel.swift +// NetworkingSampleApp +// +// Created by Matej Molnár on 07.12.2023. +// + +import Foundation +import Networking + +@MainActor +final class UsersViewModel: ObservableObject { + @Published var users = [User]() + @Published var createdUser: SampleCreateUserResponse? + + /// Custom decoder needed for decoding `createdAt` parameter of SampleCreateUserResponse. + private let responseDecoder: JSONDecoder = { + let decoder = JSONDecoder() + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + decoder.dateDecodingStrategy = .formatted(dateFormatter) + return decoder + }() + + private lazy var apiManager: APIManager = { + var responseProcessors: [ResponseProcessing] = [ + LoggingInterceptor.shared, + StatusCodeProcessor.shared + ] + var errorProcessors: [ErrorProcessing] = [LoggingInterceptor.shared] + +#if DEBUG + responseProcessors.append(EndpointRequestStorageProcessor.shared) + errorProcessors.append(EndpointRequestStorageProcessor.shared) +#endif + + return APIManager( + requestAdapters: [LoggingInterceptor.shared], + responseProcessors: responseProcessors, + errorProcessors: errorProcessors + ) + }() +} + +extension UsersViewModel { + func getUsers(in range: ClosedRange, parallelFetch: Bool) { + Task { + users = [] + + if parallelFetch { + // Fire all user requests parallelly in a group, assign it to users array after all of them are completed. + users = try await withThrowingTaskGroup(of: User.self) { group in + for id in range { + group.addTask { + let response: SampleUserResponse = try await self.apiManager.request(SampleUserRouter.user(userId: id)) + return response.data + } + } + + return try await group.reduce(into: [User]()) { $0.append($1) } + } + } else { + // Fetch user add it to users array and wait for 0.5 seconds, before fetching the next one. + for id in range { + let response: SampleUserResponse = try await apiManager.request(SampleUserRouter.user(userId: id)) + users.append(response.data) + try await Task.sleep(for: .seconds(0.5)) + } + } + } + } + + func createUser(name: String, job: String) { + Task { + createdUser = try await self.apiManager.request( + SampleUserRouter.createUser(user: .init(name: name, job: job)), + decoder: responseDecoder, + retryConfiguration: .default + ) + } + } +} diff --git a/NetworkingSampleApp/NetworkingSampleApp/UIElements/TaskButton.swift b/NetworkingSampleApp/NetworkingSampleApp/UIElements/TaskButton.swift new file mode 100644 index 00000000..a92b5d92 --- /dev/null +++ b/NetworkingSampleApp/NetworkingSampleApp/UIElements/TaskButton.swift @@ -0,0 +1,55 @@ +// +// TaskButton.swift +// NetworkingSampleApp +// +// Created by Matej Molnár on 11.12.2023. +// + +import SwiftUI + +struct TaskButton: View { + enum Config { + case play, pause, cancel, retry + + var imageName: String { + switch self { + case .play: "play" + case .pause: "pause" + case .retry: "repeat" + case .cancel: "x" + } + } + + var color: Color { + switch self { + case .play, .pause, .retry: + .blue + case .cancel: + .red + } + } + } + + private let config: Config + private let action: () -> Void + + init(config: Config, action: @escaping () -> Void) { + self.config = config + self.action = action + } + + var body: some View { + Button( + action: action, + label: { + Image(systemName: config.imageName) + .symbolVariant(.circle.fill) + .font(.title2) + .symbolRenderingMode(.hierarchical) + .foregroundStyle(config.color) + } + ) + .buttonStyle(.plain) + .contentShape(Circle()) + } +} diff --git a/NetworkingSampleApp/NetworkingSampleAppTests/Info.plist b/NetworkingSampleApp/NetworkingSampleAppTests/Info.plist deleted file mode 100644 index 64d65ca4..00000000 --- a/NetworkingSampleApp/NetworkingSampleAppTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/Package.swift b/Package.swift index f536ae87..152471e9 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.7 +// swift-tools-version:5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "Networking", platforms: [ - .iOS(SupportedPlatform.IOSVersion.v15), + .iOS(SupportedPlatform.IOSVersion.v14), .macOS(SupportedPlatform.MacOSVersion.v12), .watchOS(SupportedPlatform.WatchOSVersion.v9) ], @@ -17,16 +17,19 @@ let package = Package( targets: ["Networking"] ) ], + dependencies: [.package(url: "https://github.com/realm/SwiftLint.git", exact: "0.53.0")], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( - name: "Networking" + name: "Networking", + plugins: [.plugin(name: "SwiftLintPlugin", package: "SwiftLint")] ), .testTarget( name: "NetworkingTests", dependencies: ["Networking"], - resources: [.process("Resources")] + resources: [.process("Resources")], + plugins: [.plugin(name: "SwiftLintPlugin", package: "SwiftLint")] ) ] ) diff --git a/README.md b/README.md index ce74f99d..a61cefc2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,492 @@ # Networking -The streamlined library for efficient API call management. This lightweight solution leverages the power of Swift concurrency by building upon URL sessions. -The library is thoughtfully documented using the DocC documentation format, ensuring comprehensive and accessible documentation for developers. +![Coverage](https://img.shields.io/badge/Coverage-100%25-darkgreen?style=flat-square) +![Platforms](https://img.shields.io/badge/Platforms-iOS_iPadOS_macOS_watchOS-lightgrey?style=flat-square) +![Swift](https://img.shields.io/badge/Swift-5.9+-blue?style=flat-square) -## Supported features -TBD +A networking layer using native `URLSession` and Swift concurrency. + +- [Requirements](#requirements) +- [Installation](#installation) +- [Overview](#overview) +- [Basics](#basics) + - [Making requests](#making-requests) + - [Downloading files](#downloading-files) + - [Uploading files](#uploading-files) + - [Request authorization](#request-authorization) +- [Requestable](#requestable) +- [APIManager](#apimanager) +- [DownloadAPIManager](#downloadapimanager) +- [UploadAPIManager](#uploadapimanager) +- [Modifiers](#modifiers) +- [Interceptors](#interceptors) + - [LoggingInterceptor](#logginginterceptor) + - [AuthorizationTokenInterceptor](#authorizationtokeninterceptor) +- [Processors](#processors) + - [StatusCodeProcessor](#statuscodeprocessor) + - [EndpointRequestStorageProcessor](#endpointrequeststorageprocessor) +- [Associated array query parameters](#associated-array-query-parameters) + +## Requirements + +- iOS/iPadOS 15.0+, macOS 12.0+, watchOS 9.0+ +- Xcode 14+ +- Swift 5.9+ + +## Installation + +You can install the library with [Swift Package Manager](https://swift.org/package-manager/). Once you have your Swift package set up, adding Dependency Injection as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`. + +```swift +dependencies: [ + .package(url: "https://github.com/strvcom/ios-networking.git", .upToNextMajor(from: "0.0.4")) +] +``` + +## Overview +Heavily inspired by Moya, the networking layer's philosophy is focused on creating individual endpoint routers, transforming them into a valid URLRequest objects and applying optional adapters and processors in the network call pipeline utilising native `URLSession` under the hood. + +## Basics + +### Making requests +There is no 1 line way of making a request from scratch in order to ensure consistency and better structure. First we need to define a Router by conforming to [Requestable](#requestable) protocol. Which in the simplest form can look like this: +```swift +enum UserRouter: Requestable { + case getUser + + var baseURL: URL { + URL(string: "https://reqres.in/api")! + } + + var path: String { + switch self { + case .getUser: "/user" + } + } + + var method: HTTPMethod { + switch self { + case .getUser: .get + } + } +} +``` + +Then we can make a request on an [APIManager](#apimanager) instance, which is responsible for handling the whole request flow. +```swift +let response = try await APIManager().request(UserRouter.getUser) +``` +If you specify object type, the [APIManager](#apimanager) will automatically perform the decoding (given the received JSON correctly maps to the decodable). You can also specify a custom json decoder. + +```swift +let userResponse: UserResponse = try await apiManager.request(UserRouter.getUser) +``` + +### Downloading files +Downloads are being handled by a designated [DownloadAPIManager](#downloadapimanager). Here is an example of a basic form of file download from a `URL`. It returns a tuple of `URLSessionDownloadTask` and `Response` (result for the HTTP handshake). +```swift +let (task, response) = try await DownloadAPIManager().request(url: URL) +``` + +You can then observe the download progress for a given `URLSessionDownloadTask` +```swift +for try await downloadState in downloadAPIManager.shared.progressStream(for: task) { + ... +} +``` + +In case you need to provide some specific info in the request, you can define a type conforming to [Requestable](#requestable) protocol and pass that to the [DownloadAPIManager](#downloadapimanager) instead of the `URL`. + +### Uploading files +Uploads are being handled by a designated [UploadAPIManager](#uploadapimanager). Here is an example of a basic form of file upload to a `URL`. It returns an `UploadTask` which is a struct that represents + manages a `URLSessionUploadTask` and provides its state. +```swift +let uploadTask = try await uploadManager.upload(.file(fileUrl), to: "https://upload.com/file") +``` + +You can then observe the upload progress for a given `UploadTask` +```swift +for await uploadState in await uploadManager.stateStream(for: task.id) { +... +} +``` + +In case you need to provide some specific info in the request, you can define a type conforming to [Requestable](#requestable) protocol and pass that to the [UploadAPIManager](#uploadapimanager) instead of the upload `URL`. + +### Request authorization +Networking provides a default authorization handling for OAuth scenarios. In order to utilise this we +have to first create our own implementation of `AuthorizationManaging` which we inject into to [AuthorizationTokenInterceptor](#authorizationtokeninterceptor) and then pass +it to the [APIManager](#apimanager) as both adapter and processor. + +```swift +let authManager = AuthorizationManager() +let authorizationInterceptor = AuthorizationTokenInterceptor(authorizationManager: authManager) +let apiManager = APIManager( + requestAdapters: [authorizationInterceptor], + responseProcessors: [authorizationInterceptor] + ) +``` + +After login we have to save the `AuthorizationData` to the `AuthorizationManager`. + +``` +let response: UserAuthResponse = try await apiManager.request( + UserRouter.loginUser(request) +) +try await authManager.storage.saveData(response.authData) +``` + +Then we can simply define which request should be authorised via `isAuthenticationRequired` property of [Requestable](#requestable) protocol. + +```swift +extension UserRouter: Requestable { + ... + var isAuthenticationRequired: Bool { + switch self { + case .getUser, .updateUser: + return true + } + } +} +``` + +## Requestable +By conforming to the ``Requestable`` protocol, you can define endpoint definitions containing the elementary HTTP request components necessary to create valid HTTP requests. +
**Recommendation:** Follow the `Router` naming convention to explicitly indicate the usage of a router pattern. + +### Example +```swift +enum UserRouter { + case getUser + case updateUser(UpdateUserRequest) +} + +extension UserRouter: Requestable { + // The base URL address used for the HTTP call. + var baseURL: URL { + URL(string: "https://reqres.in/api")! + } + + // Path will be appended to the base URL. + var path: String { + switch self { + case .getUser, .updateUser: + return "/user" + } + } + + // HTTPMethod used for each endpoint. + var method: HTTPMethod { + switch self { + case .getUser: + return .get + case .updateUser: + return .post + } + } + + // Optional body data encoded in JSON by default. + var dataType: RequestDataType? { + switch self { + case .getUser: + return nil + case let .updateUser(data): + return .encodable(data) + } + } + + // Optional authentication requirement if AuthorizationInterceptor is used. + var isAuthenticationRequired: Bool { + switch self { + case .getUser, .updateUser: + return true + } + } +} +``` + +Some of the properties have default implementations defined in the `Requestable+Convenience` extension. + +## APIManager +APIManager is responsible for the creation and management of a network call. It conforms to the ``APIManaging`` protocol which allows you to define your own custom APIManager if needed. + +There are two ways to initialise the ``APIManager`` object: +1. Using URLSession as the response provider. +```swift +init( + urlSession: URLSession = .init(configuration: .default), + requestAdapters: [RequestAdapting] = [], + responseProcessors: [ResponseProcessing] = [StatusCodeProcessor.shared], + errorProcessors: [ErrorProcessing] = [] +) +``` + +2. Using custom response provider by conforming to ``ResponseProviding``. An example of a custom provider is ``MockResponseProvider``, which can be used for UI tests to interact with mocked data saved through "EndpointRequestStorageProcessor". To utilize them, simply move the stored session folder into the Asset catalogue. + +```swift +init( + responseProvider: ResponseProviding, + requestAdapters: [RequestAdapting] = [], + responseProcessors: [ResponseProcessing] = [StatusCodeProcessor.shared], + errorProcessors: [ErrorProcessing] = [] +) +``` + +Adapters and processors are passed during initialisation and cannot be changed afterwards. + +There are two methods provided by the ``APIManaging`` protocol: + +1. Result is URLSession's default (data, response) tuple. +```swift +func request( + _ endpoint: Requestable, + retryConfiguration: RetryConfiguration? +) async throws -> Response +``` +2. Result is custom decodable object. +```swift +func request( + _ endpoint: Requestable, + decoder: JSONDecoder, + retryConfiguration: RetryConfiguration? +) async throws -> DecodableResponse +``` + +### Example + + +Provide a custom after failure ``RetryConfiguration``, specifying the count of retries, delay and a handler that determines whether the request should be tried again. Otherwise, ``RetryConfiguration/default`` configuration is used. + +```swift +let retryConfiguration = RetryConfiguration(retries: 2, delay: .constant(1)) { error in + // custom logic here +} +let userResponse: UserResponse = try await apiManager.request( + UserRouter.getUser, + retryConfiguration: retryConfiguration +) +``` + +## DownloadAPIManager +DownloadAPIManager is responsible for the creation and management of a network file download. It conforms to the ``DownloadAPIManaging`` protocol which allows you to define your own custom DownloadAPIManager if needed. Multiple parallel downloads are supported. + +The initialisation is equivalent to APIManager, except the session is created for the user based on a given `URLSessionConfiguration`: +```swift +init( + urlSessionConfiguration: URLSessionConfiguration = .default, + requestAdapters: [RequestAdapting] = [], + responseProcessors: [ResponseProcessing] = [StatusCodeProcessor.shared], + errorProcessors: [ErrorProcessing] = [] +) +``` + +Adapters and processors are passed during initialisation and cannot be changed afterwards. + +The DownloadAPIManager contains a public property that enables you to keep track of current tasks in progress. +```swift +var allTasks: [URLSessionDownloadTask] { get async } +``` +There are three methods provided by the ``DownloadAPIManaging`` protocol: + +1. Request download for a given endpoint or a simple URL. Returns a standard (URLSessionDownloadTask, Response) result for the HTTP handshake. This result is not the actual downloaded file, but the HTTP response received after the download is initiated. +```swift + func downloadRequest( + _ endpoint: Requestable, + resumableData: Data? = nil, + retryConfiguration: RetryConfiguration? = .default + ) async throws -> DownloadResult { + + func downloadRequest( + _ fileURL: URL, + resumableData: Data? = nil, + retryConfiguration: RetryConfiguration? = .default + ) async throws -> DownloadResult { +``` + +2. Get progress async stream for a given task to observe task download progress and state. +```swift +func progressStream(for task: URLSessionTask) -> AsyncStream +``` + +The `DownloadState` struct provides you with information about the download itself, including bytes downloaded, total byte size of the file being downloaded or the error if any occurs. + +3. Invalidate download session in case DownloadAPIManager is not used as singleton to prevent memory leaks. +```swift +func invalidateSession(shouldFinishTasks: Bool = false) +``` +DownloadAPIManager is not deallocated from memory since URLSession is holding a reference to it. If you wish to use new instances of the DownloadAPIManager, don't forget to invalidate the session if it is not needed anymore. + +## UploadAPIManager +Similarly to DownloadAPIManager we have an UploadAPIManager responsible for the creation and management of a network file uploads. It conforms to the ``UploadAPIManaging`` protocol which allows you to define your own custom UploadAPIManager if needed. Multiple parallel uploads are supported. + +The initialisation is equivalent to APIManager, except the session is created for the user based on a given `URLSessionConfiguration`: +```swift +init( + urlSessionConfiguration: URLSessionConfiguration = .default, + requestAdapters: [RequestAdapting] = [], + responseProcessors: [ResponseProcessing] = [StatusCodeProcessor.shared], + errorProcessors: [ErrorProcessing] = [] +) +``` + +Adapters and processors are passed during initialisation and cannot be changed afterwards. + +The UploadAPIManager contains a public property that enables you to keep track of current tasks in progress. +```swift +var activeTasks: [UploadTask] { get async } +``` + +You can start an upload by calling the `upload` function by passing `UploadType` which defines three types of resources for upload `Data`, file `URL` and `MultipartFormData`. + +```swift +func upload( + _ type: UploadType, + to endpoint: Requestable +) async throws -> UploadTask +``` + +An `UploadTask` is a struct which under the hood represents + manages a URLSessionUploadTask and provides its state. + +After firing an upload by one of these three methods, you can get a StateStream either from the `UploadTask` itself or from the manager with the following method. +```swift +func stateStream(for uploadTaskId: UploadTask.ID) async -> StateStream +``` +The `StateStream` is a typealias for `AsyncPublisher>`. +The `UploadTask.State` struct provides you with information about the upload itself, including bytes uploaded, total byte size of the file being uploaded or the error if any occurs. + +The manager also allows for retries of uploads. +```swift + func retry(taskId: String) async throws +``` + +You should invalidate upload session in case UploadAPIManager is not used as singleton to prevent memory leaks. +```swift +func invalidateSession(shouldFinishTasks: Bool = false) +``` +UploadAPIManager is not deallocated from memory since URLSession is holding a reference to it. If you wish to use new instances of the UploadAPIManager, don't forget to invalidate the session if it is not needed anymore. + +## Retry ability +Both APIManager and DownloadAPIManager allow for configurable retry mechanism. + +```swift +let retryConfiguration = RetryConfiguration(retries: 2, delay: .constant(1)) { error in + // custom logic here that determines whether the request should be retried + // e.g you can only retry with 5xx http error codes +} +``` + +## Modifiers +Modifiers are useful pieces of code that modify request/response in the network request pipeline. +![Interceptors diagram](Sources/Networking/Documentation.docc/Resources/interceptors-diagram.png) + +There are three types you can leverage:
+ +``RequestAdapting`` + +Adapters are request transformable components that perform operations on the URLRequest before it is dispatched. They are used to further customise HTTP requests before they are carried out by editing the URLRequest (e.g updating headers). + +``ResponseProcessing`` + +Response processors are handling the ``Response`` received after a successful network request. + +``ErrorProcessing`` + +Error processors are handling the ``Error`` received after a failed network request. + +``RequestInterceptor`` + +Interceptors handle both adapting and response/error processing. + +By conforming to these protocols, you can create your own adaptors/processors/interceptors. In the following part, modifiers provided by Networking are introduced. + +## Interceptors + +### LoggingInterceptor +Networking provides a default ``LoggingInterceptor`` which internally uses `os_log` to pretty print requests/responses. You can utilise it to get logging console output either for requests, responses or both. + +```swift +APIManager( + // + requestAdapters: [LoggingInterceptor.shared], + responseProcessors: [LoggingInterceptor.shared], + errorProcessors: [LoggingInterceptor.shared] + // +) +``` + +### AuthorizationTokenInterceptor +Networking provides a default authorization handling for OAuth scenarios. Use the default ``AuthorizationTokenInterceptor`` with the APIManager to obtain the behaviour of JWT Bearer authorization header injection and access token expiration refresh flow. + +Start by implementing an authorization manager by conforming to ``AuthorizationManaging``. This manager requires you to provide storage defined by ``AuthorizationStorageManaging`` (where OAuth credentials will be stored) and a refresh method that will perform the refresh token network call to obtain a new OAuth pair. Optionally, you can provide custom implementations for ``AuthorizationManaging/authorizeRequest(_:)`` (by default, this method sets the authorization header) or access token getter (by default, this method returns the access token saved in provided storage). + +```swift +let authorizationManager = CustomAuthorizationManager() +let authorizationInterceptor = AuthorizationTokenInterceptor(authorizationManager: authorizationManager) +APIManager( + // + requestAdapters: [authorizationInterceptor], + responseProcessors: [authorizationInterceptor], + // +) +``` + +```swift +final class CustomAuthorizationManager: AuthorizationManaging { + let storage: AuthorizationStorageManaging = CustomAuthorizationStorageManager() + + /// For refresh token logic, create a new instance of APIManager + /// without injecting `AuthorizationTokenInterceptor` to avoid cycling during refreshes. + private let apiManager: APIManager = APIManager() + + func refreshAuthorizationData(with refreshToken: String) async throws -> Networking.AuthorizationData { + // Perform a network request to obtain refreshed OAuth credentials. + } +} +``` + +## Processors + +### StatusCodeProcessor +Each ``Requestable`` endpoint definition contains an ``Requestable/acceptableStatusCodes`` range of acceptable status codes. By default, these are set to `200..<400`. Networking provides a default status code processor that makes sure the received response's HTTP code is an acceptable one, otherwise an ``NetworkError/unacceptableStatusCode(statusCode:acceptedStatusCodes:response:)`` error is thrown. + +```swift +APIManager( + // + responseProcessors: [StatusCodeProcessor.shared], + // +) +``` + +### EndpointRequestStorageProcessor +Networking provides an ``EndpointRequestStorageProcessor`` which allows for requests and responses to be saved locally into the file system. Requests are stored in a sequential manner. Each session is kept in its own dedicated folder. The ``EndpointRequestStorageModel`` includes both successful and erroneous data. + +Initialise by optionally providing a `FileManager` instance, `JSONEncoder` to be used during request/response data encoding and a configuration. The configuration allows you to set optionally a multiPeerSharing configuration if you wish to utilise the multipeer connectivity feature for sharing the ``EndpointRequestStorageModel`` with devices using the `MultipeerConnectivity` framework. + +```swift +init( + fileManager: FileManager = .default, + jsonEncoder: JSONEncoder? = nil, + config: Config = .default +) +``` + +## Associated array query parameters +When specifying urlParameters in the endpoint definition, use an ``ArrayParameter`` to define multiple values for a single URL query parameter. The struct lets you decide which ``ArrayEncoding`` will be used during the creation of the URL. + +There are two currently supported encodings: + +1. Comma separated +```swift +http://example.com?filter=1,2,3 +``` + +2. Individual (default) +```swift +http://example.com?filter=1&filter=2&filter=3 +``` + +### Example +```swift +var urlParameters: [String: Any]? { + ["filter": ArrayParameter([1, 2, 3], arrayEncoding: .individual)] +} +``` diff --git a/Sources/Networking/Core/APIManager.swift b/Sources/Networking/Core/APIManager.swift index 481a2587..226c6560 100644 --- a/Sources/Networking/Core/APIManager.swift +++ b/Sources/Networking/Core/APIManager.swift @@ -1,6 +1,6 @@ // // APIManager.swift -// +// // // Created by Matej Molnár on 24.11.2022. // @@ -23,7 +23,14 @@ open class APIManager: APIManaging, Retryable { errorProcessors: [ErrorProcessing] = [] ) { /// generate session id in readable format - sessionId = Date().ISO8601Format() + if #unavailable(iOS 15) { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + sessionId = dateFormatter.string(from: Date()) + } else { + sessionId = Date().ISO8601Format() + } + self.responseProvider = urlSession self.requestAdapters = requestAdapters self.responseProcessors = responseProcessors @@ -37,7 +44,13 @@ open class APIManager: APIManaging, Retryable { errorProcessors: [ErrorProcessing] = [] ) { /// generate session id in readable format - sessionId = Date().ISO8601Format() + if #unavailable(iOS 15) { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + sessionId = dateFormatter.string(from: Date()) + } else { + sessionId = Date().ISO8601Format() + } self.responseProvider = responseProvider self.requestAdapters = requestAdapters self.responseProcessors = responseProcessors diff --git a/Sources/Networking/Core/DownloadAPIManager.swift b/Sources/Networking/Core/DownloadAPIManager.swift index c61a7b6f..05d96b13 100644 --- a/Sources/Networking/Core/DownloadAPIManager.swift +++ b/Sources/Networking/Core/DownloadAPIManager.swift @@ -1,6 +1,6 @@ // // DownloadAPIManager.swift -// +// // // Created by Matej Molnár on 07.03.2023. // @@ -34,7 +34,13 @@ open class DownloadAPIManager: NSObject, Retryable { errorProcessors: [ErrorProcessing] = [] ) { /// generate session id in readable format - sessionId = Date().ISO8601Format() + if #unavailable(iOS 15) { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + sessionId = dateFormatter.string(from: Date()) + } else { + sessionId = Date().ISO8601Format() + } self.requestAdapters = requestAdapters self.responseProcessors = responseProcessors diff --git a/Sources/Networking/Core/DownloadAPIManaging.swift b/Sources/Networking/Core/DownloadAPIManaging.swift index bf3dbd5a..5b0fb33c 100644 --- a/Sources/Networking/Core/DownloadAPIManaging.swift +++ b/Sources/Networking/Core/DownloadAPIManaging.swift @@ -1,6 +1,6 @@ // // DownloadAPIManaging.swift -// +// // // Created by Dominika Gajdová on 12.05.2023. // diff --git a/Sources/Networking/Core/EndpointIdentifiable.swift b/Sources/Networking/Core/EndpointIdentifiable.swift index f692aa76..5434e4d5 100644 --- a/Sources/Networking/Core/EndpointIdentifiable.swift +++ b/Sources/Networking/Core/EndpointIdentifiable.swift @@ -85,8 +85,7 @@ private extension EndpointIdentifiable { // the items need to be sorted because the final identifier should be the same no matter the order of query items in the URL if let queryItems = urlComponents.queryItems? .sorted(by: { $0.name < $1.name }) - .flatMap({ [$0.name, $0.value ?? ""] }) - { + .flatMap({ [$0.name, $0.value ?? ""] }) { components.append(contentsOf: queryItems) } @@ -96,4 +95,3 @@ private extension EndpointIdentifiable { return components } } - diff --git a/Sources/Networking/Core/MockResponseProvider.swift b/Sources/Networking/Core/MockResponseProvider.swift index 13463f62..307c471c 100644 --- a/Sources/Networking/Core/MockResponseProvider.swift +++ b/Sources/Networking/Core/MockResponseProvider.swift @@ -1,6 +1,6 @@ // // MockResponseProvider.swift -// +// // // Created by Matej Molnár on 04.01.2023. // @@ -71,6 +71,7 @@ private extension MockResponseProvider { } // return previous response, if no more stored indexed api calls + // swiftlint:disable:next empty_count if count > 0, let data = NSDataAsset(name: "\(sessionId)_\(request.identifier)_\(count - 1)", bundle: bundle)?.data { return try decoder.decode(EndpointRequestStorageModel.self, from: data) } diff --git a/Sources/Networking/Core/ResponseProviding.swift b/Sources/Networking/Core/ResponseProviding.swift index a40034a0..f6a374bb 100644 --- a/Sources/Networking/Core/ResponseProviding.swift +++ b/Sources/Networking/Core/ResponseProviding.swift @@ -1,6 +1,6 @@ // // ResponseProviding.swift -// +// // // Created by Matej Molnár on 04.01.2023. // diff --git a/Sources/Networking/Core/RetryConfiguration.swift b/Sources/Networking/Core/RetryConfiguration.swift index 61480e7b..e793f5d0 100644 --- a/Sources/Networking/Core/RetryConfiguration.swift +++ b/Sources/Networking/Core/RetryConfiguration.swift @@ -20,8 +20,7 @@ public struct RetryConfiguration { public init( retries: Int, delay: DelayConfiguration, - retryHandler: @escaping (Error) -> Bool) - { + retryHandler: @escaping (Error) -> Bool) { self.retries = retries self.delay = delay self.retryHandler = retryHandler diff --git a/Sources/Networking/Core/Retryable.swift b/Sources/Networking/Core/Retryable.swift index 42106a59..7ad94f52 100644 --- a/Sources/Networking/Core/Retryable.swift +++ b/Sources/Networking/Core/Retryable.swift @@ -1,6 +1,6 @@ // // Retryable.swift -// +// // // Created by Dominika Gajdová on 09.05.2023. // diff --git a/Sources/Networking/Core/Upload/MultipartFormData+BodyPart.swift b/Sources/Networking/Core/Upload/MultipartFormData+BodyPart.swift index 86da8894..043b0c09 100644 --- a/Sources/Networking/Core/Upload/MultipartFormData+BodyPart.swift +++ b/Sources/Networking/Core/Upload/MultipartFormData+BodyPart.swift @@ -1,6 +1,6 @@ // // MultipartFormData+BodyPart.swift -// +// // // Created by Tony Ngo on 18.06.2023. // diff --git a/Sources/Networking/Core/Upload/MultipartFormData+EncodingError.swift b/Sources/Networking/Core/Upload/MultipartFormData+EncodingError.swift index d081c49c..9ea1273c 100644 --- a/Sources/Networking/Core/Upload/MultipartFormData+EncodingError.swift +++ b/Sources/Networking/Core/Upload/MultipartFormData+EncodingError.swift @@ -1,6 +1,6 @@ // // MultipartFormData+EncodingError.swift -// +// // // Created by Tony Ngo on 19.06.2023. // @@ -10,10 +10,10 @@ import Foundation public extension MultipartFormData { enum EncodingError: LocalizedError { case invalidFileUrl(URL) - case invalidFileName(at: URL) + case invalidFileName(for: URL) case missingFileSize(for: URL) case dataStreamReadFailed(with: Error) - case dataStreamWriteFailed(at: URL) - case fileAlreadyExists(at: URL) + case dataStreamWriteFailed(for: URL) + case fileAlreadyExists(for: URL) } } diff --git a/Sources/Networking/Core/Upload/MultipartFormData.swift b/Sources/Networking/Core/Upload/MultipartFormData.swift index ade36a46..03a86fed 100644 --- a/Sources/Networking/Core/Upload/MultipartFormData.swift +++ b/Sources/Networking/Core/Upload/MultipartFormData.swift @@ -1,6 +1,6 @@ // // MultipartFormData.swift -// +// // // Created by Tony Ngo on 18.06.2023. // @@ -71,7 +71,7 @@ public extension MultipartFormData { let fileName = fileName ?? fileUrl.lastPathComponent guard !fileName.isEmpty && !fileUrl.pathExtension.isEmpty else { - throw EncodingError.invalidFileName(at: fileUrl) + throw EncodingError.invalidFileName(for: fileUrl) } guard diff --git a/Sources/Networking/Core/Upload/MultipartFormDataEncoder.swift b/Sources/Networking/Core/Upload/MultipartFormDataEncoder.swift index a37fecf8..79cbf04b 100644 --- a/Sources/Networking/Core/Upload/MultipartFormDataEncoder.swift +++ b/Sources/Networking/Core/Upload/MultipartFormDataEncoder.swift @@ -1,6 +1,6 @@ // // MultipartFormDataEncoder.swift -// +// // // Created by Tony Ngo on 18.06.2023. // @@ -31,14 +31,9 @@ open class MultipartFormDataEncoder { } } - -/** - -The main reason why there are methods to encode data & encode file is similar to `uploadTask(with:from:)` and `uploadTask(with:fromFile:)` ig one could convert the content of the file to Data using Data(contentsOf:) and use the first method to send data. One has the data available in memory while the second reads the data directly from the file thus doesn't load the data into memory so it is more efficient. - */ - // MARK: - MultipartFormDataEncoding extension MultipartFormDataEncoder: MultipartFormDataEncoding { + /// The main reason why there are methods to encode data & encode file is similar to `uploadTask(with:from:)` and `uploadTask(with:fromFile:)` if one could convert the content of the file to Data using Data(contentsOf:) and use the first method to send data. One has the data available in memory while the second reads the data directly from the file thus doesn't load the data into memory so it is more efficient. public func encode(_ multipartFormData: MultipartFormData) throws -> Data { var encoded = Data() @@ -64,11 +59,11 @@ extension MultipartFormDataEncoder: MultipartFormDataEncoding { } guard !fileManager.fileExists(at: fileUrl) else { - throw MultipartFormData.EncodingError.fileAlreadyExists(at: fileUrl) + throw MultipartFormData.EncodingError.fileAlreadyExists(for: fileUrl) } guard let outputStream = OutputStream(url: fileUrl, append: false) else { - throw MultipartFormData.EncodingError.dataStreamWriteFailed(at: fileUrl) + throw MultipartFormData.EncodingError.dataStreamWriteFailed(for: fileUrl) } try encode(multipartFormData, into: outputStream) @@ -158,7 +153,7 @@ private extension MultipartFormDataEncoder { // Encode headers in a deterministic manner for easier testing let encodedHeaders = contentHeaders .sorted(by: { $0.key.rawValue < $1.key.rawValue }) - .map { "\($0.key.rawValue): \($0.value)"} + .map { "\($0.key.rawValue): \($0.value)" } .joined(separator: "\(crlf)") encoded.append(encodedHeaders) diff --git a/Sources/Networking/Core/Upload/MultipartFormDataEncoding.swift b/Sources/Networking/Core/Upload/MultipartFormDataEncoding.swift index 79e510c9..6050f454 100644 --- a/Sources/Networking/Core/Upload/MultipartFormDataEncoding.swift +++ b/Sources/Networking/Core/Upload/MultipartFormDataEncoding.swift @@ -1,6 +1,6 @@ // // MultipartFormDataEncoding.swift -// +// // // Created by Tony Ngo on 18.06.2023. // diff --git a/Sources/Networking/Core/Upload/UploadAPIManager.swift b/Sources/Networking/Core/Upload/UploadAPIManager.swift index 501d85d5..a553845a 100644 --- a/Sources/Networking/Core/Upload/UploadAPIManager.swift +++ b/Sources/Networking/Core/Upload/UploadAPIManager.swift @@ -9,6 +9,7 @@ import Combine import Foundation /// Default upload API manager +@available(iOS 15.0, *) open class UploadAPIManager: NSObject { // MARK: - Public Properties public var activeTasks: [UploadTask] { @@ -60,6 +61,7 @@ open class UploadAPIManager: NSObject { } // MARK: URLSessionDataDelegate +@available(iOS 15.0, *) extension UploadAPIManager: URLSessionDataDelegate { public func urlSession( _ session: URLSession, @@ -92,6 +94,7 @@ extension UploadAPIManager: URLSessionDataDelegate { } // MARK: - URLSessionTaskDelegate +@available(iOS 15.0, *) extension UploadAPIManager: URLSessionTaskDelegate { public func urlSession( _ session: URLSession, @@ -130,6 +133,7 @@ extension UploadAPIManager: URLSessionTaskDelegate { } // MARK: - UploadAPIManaging +@available(iOS 15.0, *) extension UploadAPIManager: UploadAPIManaging { public func invalidateSession(shouldFinishTasks: Bool) { if shouldFinishTasks { @@ -216,6 +220,7 @@ extension UploadAPIManager: UploadAPIManaging { } // MARK: - Private API +@available(iOS 15.0, *) private extension UploadAPIManager { @discardableResult func uploadRequest( diff --git a/Sources/Networking/Core/Upload/UploadAPIManaging.swift b/Sources/Networking/Core/Upload/UploadAPIManaging.swift index db867d62..2fe2c0a3 100644 --- a/Sources/Networking/Core/Upload/UploadAPIManaging.swift +++ b/Sources/Networking/Core/Upload/UploadAPIManaging.swift @@ -1,6 +1,6 @@ // // UploadAPIManaging.swift -// +// // // Created by Tony Ngo on 12.06.2023. // @@ -8,6 +8,7 @@ import Combine import Foundation +@available(iOS 15.0, *) public protocol UploadAPIManaging { typealias StateStream = AsyncPublisher> @@ -72,6 +73,7 @@ public protocol UploadAPIManaging { func invalidateSession(shouldFinishTasks: Bool) } +@available(iOS 15.0, *) public extension UploadAPIManaging { /// Initiates a `multipart/form-data` upload request to the specified `endpoint`. /// diff --git a/Sources/Networking/Core/Upload/UploadTask+State.swift b/Sources/Networking/Core/Upload/UploadTask+State.swift index ef7030ca..8b5bd2e5 100644 --- a/Sources/Networking/Core/Upload/UploadTask+State.swift +++ b/Sources/Networking/Core/Upload/UploadTask+State.swift @@ -1,6 +1,6 @@ // // UploadTask+State.swift -// +// // // Created by Tony Ngo on 12.06.2023. // diff --git a/Sources/Networking/Core/Upload/UploadTask.swift b/Sources/Networking/Core/Upload/UploadTask.swift index 740694c9..1e09622e 100644 --- a/Sources/Networking/Core/Upload/UploadTask.swift +++ b/Sources/Networking/Core/Upload/UploadTask.swift @@ -1,6 +1,6 @@ // // UploadTask.swift -// +// // // Created by Tony Ngo on 12.06.2023. // @@ -10,6 +10,7 @@ import Foundation /// Represents and manages an upload task and provides its state. public struct UploadTask { + // swiftlint:disable:next type_name public typealias ID = String /// The session task this object represents. @@ -62,6 +63,7 @@ public extension UploadTask { } // MARK: - Internal API +@available(iOS 15.0, *) extension UploadTask { /// The identifier of the underlying `URLSessionUploadTask`. var taskIdentifier: Int { diff --git a/Sources/Networking/Core/Upload/Uploadable.swift b/Sources/Networking/Core/Upload/Uploadable.swift index 290db57e..c974b93a 100644 --- a/Sources/Networking/Core/Upload/Uploadable.swift +++ b/Sources/Networking/Core/Upload/Uploadable.swift @@ -1,6 +1,6 @@ // // Uploadable.swift -// +// // // Created by Tony Ngo on 13.06.2023. // diff --git a/Sources/Networking/Documentation.docc/Documentation.md b/Sources/Networking/Documentation.docc/Documentation.md index 584af800..09c6f61d 100644 --- a/Sources/Networking/Documentation.docc/Documentation.md +++ b/Sources/Networking/Documentation.docc/Documentation.md @@ -175,6 +175,46 @@ func invalidateSession(shouldFinishTasks: Bool = false) ``` DownloadAPIManager is not deallocated from memory since URLSession is holding a reference to it. If you wish to use new instances of the DownloadAPIManager, don't forget to invalidate the session if it is not needed anymore. +## UploadAPIManager +Similarly to DownloadAPIManager we have an UploadAPIManager responsible for the creation and management of network file uploads. It conforms to the ``UploadAPIManaging`` protocol which allows you to define your own custom UploadAPIManager if needed. Multiple parallel uploads are supported. + +The initialisation is equivalent to APIManager, except the session is created for the user based on a given `URLSessionConfiguration`: +```swift +init( + urlSessionConfiguration: URLSessionConfiguration = .default, + requestAdapters: [RequestAdapting] = [], + responseProcessors: [ResponseProcessing] = [StatusCodeProcessor.shared], + errorProcessors: [ErrorProcessing] = [] +) +``` + +Adapters and processors are passed during initialisation and cannot be changed afterwards. + +The UploadAPIManager contains a public property that enables you to keep track of current tasks in progress. +```swift +var activeTasks: [UploadTask] { get async } +``` +``UploadAPIManaging`` defines three methods for upload based on the upload type `Data`, file `URL` and `MultipartFormData`. Each of these methods return an `UploadTask`. +An `UploadTask` is a struct which under the hood represents + manages a URLSessionUploadTask and provides its state. + +After firing an upload by one of these three methods, you can get a StateStream either from the `UploadTask` itself or from the manager with the following method. +```swift +func stateStream(for uploadTaskId: UploadTask.ID) async -> StateStream +``` +The `StateStream` is a typealias for `AsyncPublisher>`. +The `UploadTask.State` struct provides you with information about the upload itself, including bytes uploaded, total byte size of the file being uploaded or the error if any occurs. + +The manager also allows for retries of uploads. +```swift + func retry(taskId: String) async throws +``` + +You should invalidate upload session in case UploadAPIManager is not used as singleton to prevent memory leaks. +```swift +func invalidateSession(shouldFinishTasks: Bool = false) +``` +UploadAPIManager is not deallocated from memory since URLSession is holding a reference to it. If you wish to use new instances of the UploadAPIManager, don't forget to invalidate the session if it is not needed anymore. + ## Retry ability Both APIManager and DownloadAPIManager allow for configurable retry mechanism. diff --git a/Sources/Networking/Misc/ArrayEncoding.swift b/Sources/Networking/Misc/ArrayEncoding.swift index bfd673d8..a3926a2c 100644 --- a/Sources/Networking/Misc/ArrayEncoding.swift +++ b/Sources/Networking/Misc/ArrayEncoding.swift @@ -1,6 +1,6 @@ // // ArrayEncoding.swift -// +// // // Created by Dominika Gajdová on 08.05.2023. // diff --git a/Sources/Networking/Misc/ArrayParameter.swift b/Sources/Networking/Misc/ArrayParameter.swift index b8f3265c..dfe9b930 100644 --- a/Sources/Networking/Misc/ArrayParameter.swift +++ b/Sources/Networking/Misc/ArrayParameter.swift @@ -1,6 +1,6 @@ // -// File.swift -// +// ArrayParameter.swift +// // // Created by Dominika Gajdová on 08.05.2023. // diff --git a/Sources/Networking/Misc/Counter.swift b/Sources/Networking/Misc/Counter.swift index 12e90087..8862c504 100644 --- a/Sources/Networking/Misc/Counter.swift +++ b/Sources/Networking/Misc/Counter.swift @@ -1,6 +1,6 @@ // // Counter.swift -// +// // // Created by Matej Molnár on 14.12.2022. // diff --git a/Sources/Networking/Misc/MockResponseProviderError.swift b/Sources/Networking/Misc/MockResponseProviderError.swift index 730024b8..0319461a 100644 --- a/Sources/Networking/Misc/MockResponseProviderError.swift +++ b/Sources/Networking/Misc/MockResponseProviderError.swift @@ -1,6 +1,6 @@ // // MockResponseProviderError.swift -// +// // // Created by Matej Molnár on 04.01.2023. // diff --git a/Sources/Networking/Misc/ThreadSafeDictionary.swift b/Sources/Networking/Misc/ThreadSafeDictionary.swift index 5d81e04a..864ffed5 100644 --- a/Sources/Networking/Misc/ThreadSafeDictionary.swift +++ b/Sources/Networking/Misc/ThreadSafeDictionary.swift @@ -1,6 +1,6 @@ // // ThreadSafeDictionary.swift -// +// // // Created by Dominika Gajdová on 25.05.2023. // @@ -20,7 +20,7 @@ actor ThreadSafeDictionary { } func set(value: Value?, for task: Key) { - values[task] = value + values[task] = value } /// Updates the property of a given keyPath. diff --git a/Sources/Networking/Misc/URLSessionTask+AsyncResponse.swift b/Sources/Networking/Misc/URLSessionTask+AsyncResponse.swift index 8451765d..d75e7c84 100644 --- a/Sources/Networking/Misc/URLSessionTask+AsyncResponse.swift +++ b/Sources/Networking/Misc/URLSessionTask+AsyncResponse.swift @@ -1,6 +1,6 @@ // // URLSessionTask+AsyncResponse.swift -// +// // // Created by Dominika Gajdová on 12.05.2023. // diff --git a/Sources/Networking/Misc/URLSessionTask+DownloadState.swift b/Sources/Networking/Misc/URLSessionTask+DownloadState.swift index 239d19e5..0417013e 100644 --- a/Sources/Networking/Misc/URLSessionTask+DownloadState.swift +++ b/Sources/Networking/Misc/URLSessionTask+DownloadState.swift @@ -1,6 +1,6 @@ // -// DownloadState.swift -// +// URLSessionTask+DownloadState.swift +// // // Created by Matej Molnár on 07.03.2023. // diff --git a/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationData.swift b/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationData.swift index bf794614..4bc5cf59 100644 --- a/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationData.swift +++ b/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationData.swift @@ -1,6 +1,6 @@ // // AuthorizationData.swift -// +// // // Created by Dominika Gajdová on 20.12.2022. // @@ -23,8 +23,8 @@ public struct AuthorizationData: Codable, Sendable { } // MARK: Computed propeties -extension AuthorizationData { - public var isExpired: Bool { +public extension AuthorizationData { + var isExpired: Bool { guard let expiresIn else { /// If there is no information about expiration, always assume it is not expired. return false diff --git a/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationError.swift b/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationError.swift index 271526b7..7ac788ba 100644 --- a/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationError.swift +++ b/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationError.swift @@ -1,6 +1,6 @@ // // AuthorizationError.swift -// +// // // Created by Dominika Gajdová on 02.01.2023. // diff --git a/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationManaging.swift b/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationManaging.swift index 95dd0a18..f177adb9 100644 --- a/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationManaging.swift +++ b/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationManaging.swift @@ -1,6 +1,6 @@ // // AuthorizationManaging.swift -// +// // // Created by Dominika Gajdová on 20.12.2022. // diff --git a/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationStorageManaging.swift b/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationStorageManaging.swift index f779e95d..4939e932 100644 --- a/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationStorageManaging.swift +++ b/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationStorageManaging.swift @@ -1,6 +1,6 @@ // // AuthorizationStorageManaging.swift -// +// // // Created by Dominika Gajdová on 20.12.2022. // @@ -9,7 +9,7 @@ import Foundation /// Basic operations to store `AuthorizationData` /// To keep consistency all operations are async -public protocol AuthorizationStorageManaging { +public protocol AuthorizationStorageManaging { func saveData(_ data: AuthorizationData) async throws func getData() async throws -> AuthorizationData func deleteData() async throws diff --git a/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationTokenInterceptor.swift b/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationTokenInterceptor.swift index 693616cc..2c2c5d29 100644 --- a/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationTokenInterceptor.swift +++ b/Sources/Networking/Modifiers/Interceptors/Authorization/AuthorizationTokenInterceptor.swift @@ -1,6 +1,6 @@ // // AuthorizationTokenInterceptor.swift -// +// // // Created by Dominika Gajdová on 08.12.2022. // diff --git a/Sources/Networking/Modifiers/Interceptors/LoggingInterceptor.swift b/Sources/Networking/Modifiers/Interceptors/LoggingInterceptor.swift index fe12d8d2..4cb36cff 100644 --- a/Sources/Networking/Modifiers/Interceptors/LoggingInterceptor.swift +++ b/Sources/Networking/Modifiers/Interceptors/LoggingInterceptor.swift @@ -1,6 +1,6 @@ // // LoggingInterceptor.swift -// +// // // Created by Matej Molnár on 01.12.2022. // @@ -73,9 +73,7 @@ private extension LoggingInterceptor { let requestBody = request.httpBody, let object = try? JSONSerialization.jsonObject(with: requestBody, options: []), let data = try? JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted]), - let body = String(data: data, encoding: .utf8) - { - // swiftlint:disable:previous opening_brace + let body = String(data: data, encoding: .utf8) { os_log("👉 Body: %{public}@", type: .info, body) } @@ -131,4 +129,3 @@ private extension LoggingInterceptor { os_log("❌❌❌ ERROR END ❌❌❌", type: .error) } } - diff --git a/Sources/Networking/Modifiers/Processors/EndpointRequestStorageProcessor/EndpointRequestStorageProcessor.swift b/Sources/Networking/Modifiers/Processors/EndpointRequestStorageProcessor/EndpointRequestStorageProcessor.swift index 6b91677f..91ec57a2 100644 --- a/Sources/Networking/Modifiers/Processors/EndpointRequestStorageProcessor/EndpointRequestStorageProcessor.swift +++ b/Sources/Networking/Modifiers/Processors/EndpointRequestStorageProcessor/EndpointRequestStorageProcessor.swift @@ -1,6 +1,6 @@ // // EndpointRequestStorageProcessor.swift -// +// // // Created by Matej Molnár on 12.12.2022. // diff --git a/Sources/Networking/Modifiers/Processors/StatusCodeProcessor.swift b/Sources/Networking/Modifiers/Processors/StatusCodeProcessor.swift index e4af464c..cd907b5a 100644 --- a/Sources/Networking/Modifiers/Processors/StatusCodeProcessor.swift +++ b/Sources/Networking/Modifiers/Processors/StatusCodeProcessor.swift @@ -1,6 +1,6 @@ // // StatusCodeProcessor.swift -// +// // // Created by Matej Molnár on 01.12.2022. // diff --git a/Sources/Networking/Utils/Sequence+Convenience.swift b/Sources/Networking/Utils/Sequence+Convenience.swift index cf97ed41..9ac25667 100644 --- a/Sources/Networking/Utils/Sequence+Convenience.swift +++ b/Sources/Networking/Utils/Sequence+Convenience.swift @@ -1,6 +1,6 @@ // // Sequence+Convenience.swift -// +// // // Created by Tomas Cejka on 15.11.2022. // diff --git a/Sources/Networking/Utils/URL+Convenience.swift b/Sources/Networking/Utils/URL+Convenience.swift index 92bf9fbe..f578517a 100644 --- a/Sources/Networking/Utils/URL+Convenience.swift +++ b/Sources/Networking/Utils/URL+Convenience.swift @@ -1,6 +1,6 @@ // // URL+Convenience.swift -// +// // // Created by Tony Ngo on 18.06.2023. // @@ -18,7 +18,7 @@ public extension URL { } var fileSize: Int? { - guard let resources = try? resourceValues(forKeys:[.fileSizeKey]) else { + guard let resources = try? resourceValues(forKeys: [.fileSizeKey]) else { return nil } return resources.fileSize diff --git a/Tests/NetworkingTests/AssociatedArrayQueryTests.swift b/Tests/NetworkingTests/AssociatedArrayQueryTests.swift index e12c9512..26390774 100644 --- a/Tests/NetworkingTests/AssociatedArrayQueryTests.swift +++ b/Tests/NetworkingTests/AssociatedArrayQueryTests.swift @@ -1,6 +1,6 @@ // -// File.swift -// +// AssociatedArrayQueryTests.swift +// // // Created by Dominika Gajdová on 08.05.2023. // @@ -15,47 +15,49 @@ final class AssociatedArrayQueryTests: XCTestCase { case arraySeparated case both - var baseURL: URL { URL(string: "http://someurl.com")! } + var baseURL: URL { + // swiftlint:disable:next force_unwrapping + URL(string: "https://someurl.com/")! + } + var path: String { "" } - var urlParameters: [String: Any]? { + var urlParameters: [String: Any]? { switch self { case .single: - return ["filter": 1] + ["filter": 1] case .arrayIndividual: - return ["filter": ArrayParameter([1, 2, 3], arrayEncoding: .individual)] + ["filter": ArrayParameter([1, 2, 3], arrayEncoding: .individual)] case .arraySeparated: - return ["filter": ArrayParameter([1, 2, 3], arrayEncoding: .commaSeparated)] + ["filter": ArrayParameter([1, 2, 3], arrayEncoding: .commaSeparated)] case .both: - return ["filter": ArrayParameter([1, 2, 3], arrayEncoding: .individual), "data": 5] + ["filter": ArrayParameter([1, 2, 3], arrayEncoding: .individual), "data": 5] } } } func testMultipleKeyParamaterURLCreation() async throws { let urlRequest1 = try TestRouter.single.asRequest() - XCTAssertEqual("http://someurl.com/?filter=1", urlRequest1.url?.absoluteString ?? "") + XCTAssertEqual("https://someurl.com/?filter=1", urlRequest1.url?.absoluteString ?? "") let urlRequest2 = try TestRouter.arrayIndividual.asRequest() - XCTAssertEqual("http://someurl.com/?filter=1&filter=2&filter=3", urlRequest2.url?.absoluteString ?? "") + XCTAssertEqual("https://someurl.com/?filter=1&filter=2&filter=3", urlRequest2.url?.absoluteString ?? "") let urlRequest3 = try TestRouter.arraySeparated.asRequest() - XCTAssertEqual("http://someurl.com/?filter=1,2,3", urlRequest3.url?.absoluteString ?? "") + XCTAssertEqual("https://someurl.com/?filter=1,2,3", urlRequest3.url?.absoluteString ?? "") let urlRequest4 = try TestRouter.both.asRequest() if let url = urlRequest4.url, let components = URLComponents(url: url, resolvingAgainstBaseURL: true), let queryItems = components.queryItems, - let parameters = TestRouter.both.urlParameters - { - let result = parameters.allSatisfy { (key, value) in + let parameters = TestRouter.both.urlParameters { + let result = parameters.allSatisfy { (key, _) in queryItems.contains(where: { $0.name == key }) } - XCTAssertTrue(result) } else { XCTFail("Invalid request url and/or query parameters.") diff --git a/Tests/NetworkingTests/AuthorizationTokenInterceptorTests.swift b/Tests/NetworkingTests/AuthorizationTokenInterceptorTests.swift index 7bac3d57..b3bc1ffb 100644 --- a/Tests/NetworkingTests/AuthorizationTokenInterceptorTests.swift +++ b/Tests/NetworkingTests/AuthorizationTokenInterceptorTests.swift @@ -1,6 +1,6 @@ // // AuthorizationTokenInterceptorTests.swift -// +// // // Created by Matej Molnár on 02.02.2023. // @@ -26,7 +26,7 @@ final class AuthorizationTokenInterceptorTests: XCTestCase { let adaptedRequest = try await authTokenInterceptor.adapt(request, for: endpointRequest) - XCTAssertEqual(adaptedRequest.allHTTPHeaderFields![HTTPHeader.HeaderField.authorization.rawValue], "Bearer \(validAuthData.accessToken)") + XCTAssertEqual(adaptedRequest.allHTTPHeaderFields?[HTTPHeader.HeaderField.authorization.rawValue], "Bearer \(validAuthData.accessToken)") } func testFailedRequestAuthorization() async throws { @@ -40,7 +40,7 @@ final class AuthorizationTokenInterceptorTests: XCTestCase { do { _ = try await authTokenInterceptor.adapt(request, for: endpointRequest) } catch { - XCTAssertEqual(error as! AuthorizationError, AuthorizationError.missingAuthorizationData) + XCTAssertEqual(error as? AuthorizationError, AuthorizationError.missingAuthorizationData) } } @@ -77,7 +77,7 @@ final class AuthorizationTokenInterceptorTests: XCTestCase { let adaptedRequest = try await authTokenInterceptor.adapt(request, for: endpointRequest) - XCTAssertEqual(adaptedRequest.allHTTPHeaderFields![HTTPHeader.HeaderField.authorization.rawValue], "Bearer \(refreshedAuthData.accessToken)") + XCTAssertEqual(adaptedRequest.allHTTPHeaderFields?[HTTPHeader.HeaderField.authorization.rawValue], "Bearer \(refreshedAuthData.accessToken)") } func testFailedTokenRefresh() async throws { @@ -94,7 +94,7 @@ final class AuthorizationTokenInterceptorTests: XCTestCase { do { _ = try await authTokenInterceptor.adapt(request, for: endpointRequest) } catch { - XCTAssertEqual(error as! AuthorizationError, AuthorizationError.expiredAccessToken) + XCTAssertEqual(error as? AuthorizationError, AuthorizationError.expiredAccessToken) } } @@ -121,7 +121,7 @@ final class AuthorizationTokenInterceptorTests: XCTestCase { group.addTask { do { let request = try await authTokenInterceptor.adapt(request, for: endpointRequest) - XCTAssertEqual(request.allHTTPHeaderFields![HTTPHeader.HeaderField.authorization.rawValue], "Bearer \(refreshedAuthData.accessToken)") + XCTAssertEqual(request.allHTTPHeaderFields?[HTTPHeader.HeaderField.authorization.rawValue], "Bearer \(refreshedAuthData.accessToken)") } catch { XCTAssert(false, "function shouldn't throw and error: \(error)") } @@ -151,7 +151,7 @@ final class AuthorizationTokenInterceptorTests: XCTestCase { _ = try await authTokenInterceptor.adapt(request, for: endpointRequest) XCTAssert(false, "function didn't throw an error even though it should have") } catch { - XCTAssertEqual(error as! AuthorizationError, AuthorizationError.expiredAccessToken) + XCTAssertEqual(error as? AuthorizationError, AuthorizationError.expiredAccessToken) } } } @@ -202,6 +202,7 @@ private enum MockRouter: Requestable { case testAuthenticationNotRequired var baseURL: URL { + // swiftlint:disable:next force_unwrapping URL(string: "test.com")! } @@ -212,9 +213,9 @@ private enum MockRouter: Requestable { var isAuthenticationRequired: Bool { switch self { case .testAuthenticationRequired: - return true + true case .testAuthenticationNotRequired: - return false + false } } } diff --git a/Tests/NetworkingTests/EndpointIdentifiableTests.swift b/Tests/NetworkingTests/EndpointIdentifiableTests.swift index 77e45f68..35dd7e83 100644 --- a/Tests/NetworkingTests/EndpointIdentifiableTests.swift +++ b/Tests/NetworkingTests/EndpointIdentifiableTests.swift @@ -25,18 +25,18 @@ final class EndpointIdentifiableTests: XCTestCase { var path: String { switch self { case .testPlain: - return "testPlain" + "testPlain" case .testMethod: - return "testMethod" + "testMethod" case .testParameters: - return "testParameters" + "testParameters" } } var urlParameters: [String: Any]? { switch self { case .testParameters: - return [ + [ "page": 1, "limit": 20, "empty": "", @@ -44,16 +44,16 @@ final class EndpointIdentifiableTests: XCTestCase { "alphabetically": true ] default: - return nil + nil } } var method: HTTPMethod { switch self { case .testMethod: - return .delete + .delete default: - return .get + .get } } @@ -61,13 +61,13 @@ final class EndpointIdentifiableTests: XCTestCase { switch self { case .testPlain: // swiftlint:disable:next force_unwrapping - return URL(string: "https://identifiable.tests/testPlain")! + URL(string: "https://identifiable.tests/testPlain")! case .testMethod: // swiftlint:disable:next force_unwrapping - return URL(string: "https://identifiable.tests/testMethod")! + URL(string: "https://identifiable.tests/testMethod")! case .testParameters: // swiftlint:disable:next force_unwrapping - return URL(string: "https://identifiable.tests/testParameters?page=1&limit=20&empty=&string=!test!&alphabetically=true")! + URL(string: "https://identifiable.tests/testParameters?page=1&limit=20&empty=&string=!test!&alphabetically=true")! } } } diff --git a/Tests/NetworkingTests/EndpointRequestStorageProcessorTests.swift b/Tests/NetworkingTests/EndpointRequestStorageProcessorTests.swift index c42eaa18..3980fde9 100644 --- a/Tests/NetworkingTests/EndpointRequestStorageProcessorTests.swift +++ b/Tests/NetworkingTests/EndpointRequestStorageProcessorTests.swift @@ -1,6 +1,6 @@ // // EndpointRequestStorageProcessorTests.swift -// +// // // Created by Matej Molnár on 12.12.2022. // @@ -33,44 +33,44 @@ final class EndpointRequestStorageProcessorTests: XCTestCase { var path: String { switch self { case .testStoringGet: - return "storing" + "storing" case .testStoringPost: - return "storing" + "storing" case .testStoringImage: - return "image" + "image" case .testStoringError: - return "error" + "error" } } var method: HTTPMethod { switch self { case .testStoringGet, .testStoringImage, .testStoringError: - return .get + .get case .testStoringPost: - return .post + .post } } - var urlParameters: [String : Any]? { + var urlParameters: [String: Any]? { switch self { case .testStoringGet, .testStoringPost, .testStoringImage, .testStoringError: - return ["query": "mock"] + ["query": "mock"] } } - var headers: [String : String]? { + var headers: [String: String]? { switch self { case .testStoringGet, .testStoringPost, .testStoringImage, .testStoringError: - return ["mockRequestHeader": "mock"] + ["mockRequestHeader": "mock"] } } var dataType: RequestDataType? { switch self { case .testStoringGet, .testStoringImage, .testStoringError: - return nil + nil case .testStoringPost: - return .encodable(MockBody(parameter: "mock")) + .encodable(MockBody(parameter: "mock")) } } } @@ -278,13 +278,13 @@ final class EndpointRequestStorageProcessorTests: XCTestCase { ) } + // swiftlint:enable force_unwrapping static var allTests = [ ("testResponseStaysTheSameAfterStoringData", testResponseStaysTheSameAfterStoringData), ("testStoredDataForGetRequestWithJSONResponse", testStoredDataForGetRequestWithJSONResponse), ("testStoredDataForGetRequestWithImageResponse", testStoredDataForGetRequestWithImageResponse), ("testStoredDataForGetRequestWithErrorResponse", testStoredDataForGetRequestWithErrorResponse), - ("testStoredDataForPostRequest", testStoredDataForPostRequest), - + ("testStoredDataForPostRequest", testStoredDataForPostRequest) ] } diff --git a/Tests/NetworkingTests/ErrorProcessorTests.swift b/Tests/NetworkingTests/ErrorProcessorTests.swift index a9400f39..195768e2 100644 --- a/Tests/NetworkingTests/ErrorProcessorTests.swift +++ b/Tests/NetworkingTests/ErrorProcessorTests.swift @@ -1,6 +1,6 @@ // -// File.swift -// +// ErrorProcessorTests.swift +// // // Created by Dominika Gajdová on 05.12.2022. // @@ -19,13 +19,13 @@ final class ErrorProcessorTests: XCTestCase { switch self { case .testMockSimpleError: // swiftlint:disable:next force_unwrapping - return URL(string: "https://reqres.in/api")! + URL(string: "https://reqres.in/api")! case .testURLError: // swiftlint:disable:next force_unwrapping - return URL(string: "https://nonexistenturladdress")! + URL(string: "https://nonexistenturladdress")! case .testErrorProcessing: // swiftlint:disable:next force_unwrapping - return URL(string: "https://sample.com")! + URL(string: "https://sample.com")! } } @@ -36,9 +36,10 @@ final class ErrorProcessorTests: XCTestCase { // Our mocked error processors don't utilise the endpointRequest parameter so we can use the same mocked endpointRequest for all tests private let mockEndpointRequest = EndpointRequest(MockRouter.testErrorProcessing, sessionId: "sessionId_error_process") - // swiftlint:disable:next force_unwrapping + private var testUrl: URL { - URL(string: "http://sometesturl.com")! + // swiftlint:disable:next force_unwrapping + URL(string: "https://sometesturl.com")! } func test_errorProcessing_process_mappingUnacceptableToSimpleErrorShouldSucceed() { diff --git a/Tests/NetworkingTests/MockResponseProviderTests.swift b/Tests/NetworkingTests/MockResponseProviderTests.swift index 4edd8096..c3ca6027 100644 --- a/Tests/NetworkingTests/MockResponseProviderTests.swift +++ b/Tests/NetworkingTests/MockResponseProviderTests.swift @@ -1,6 +1,6 @@ // // MockResponseProviderTests.swift -// +// // // Created by Matej Molnár on 05.01.2023. // @@ -12,22 +12,24 @@ final class MockResponseProviderTests: XCTestCase { // swiftlint:disable:next force_unwrapping private lazy var mockUrlRequest = URLRequest(url: URL(string: "https://reqres.in/api/users?page=2")!) private let mockSessionId = "2023-01-04T16:15:29Z" + private let mockHeaderFields = [ - "Server" : "cloudflare", - "Etag" : "W/\"406-ut0vzoCuidvyMf8arZpMpJ6ZRDw\"", - "x-powered-by" : "Express", - "nel" : "{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}", - "Content-Encoding" : "br", - "Vary" : "Accept-Encoding", - "report-to" : "{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v3?s=5XGHUrnfYDsl7guBAx0nFk7LTbUgOLjp5%2BGMkSPetC5OrW6fKlUc1NBBtOKHKe9yWrcbXkF4TQe8jsv1c4KggYW1q4pYf5G2rQvA8XACg1znl6MbWiNj1w2wOg%3D%3D\"}],\"group\":\"cf-nel\",\"max_age\":604800}", - "Content-Type" : "application/json; charset=utf-8", - "cf-cache-status" : "HIT", - "Cache-Control" : "max-age=14400", - "Access-Control-Allow-Origin" : "*", - "cf-ray" : "784545f34d2f27bc-PRG", - "Date" : "Wed, 04 Jan 2023 16:15:29 GMT", - "Via" : "1.1 vegur", - "Age" : "6306" + "Server": "cloudflare", + "Etag": "W/\"406-ut0vzoCuidvyMf8arZpMpJ6ZRDw\"", + "x-powered-by": "Express", + "nel": "{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}", + "Content-Encoding": "br", + "Vary": "Accept-Encoding", + // swiftlint:disable:next line_length + "report-to": "{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v3?s=5XGHUrnfYDsl7guBAx0nFk7LTbUgOLjp5%2BGMkSPetC5OrW6fKlUc1NBBtOKHKe9yWrcbXkF4TQe8jsv1c4KggYW1q4pYf5G2rQvA8XACg1znl6MbWiNj1w2wOg%3D%3D\"}],\"group\":\"cf-nel\",\"max_age\":604800}", + "Content-Type": "application/json; charset=utf-8", + "cf-cache-status": "HIT", + "Cache-Control": "max-age=14400", + "Access-Control-Allow-Origin": "*", + "cf-ray": "784545f34d2f27bc-PRG", + "Date": "Wed, 04 Jan 2023 16:15:29 GMT", + "Via": "1.1 vegur", + "Age": "6306" ] func testLoadingData() async throws { diff --git a/Tests/NetworkingTests/MultipartFormDataEncoderTests.swift b/Tests/NetworkingTests/MultipartFormDataEncoderTests.swift index aca40bf7..80e8a252 100644 --- a/Tests/NetworkingTests/MultipartFormDataEncoderTests.swift +++ b/Tests/NetworkingTests/MultipartFormDataEncoderTests.swift @@ -1,6 +1,6 @@ // // MultipartFormDataEncoderTests.swift -// +// // // Created by Tony Ngo on 18.06.2023. // @@ -77,6 +77,7 @@ final class MultipartFormDataEncoderTests: XCTestCase { func test_encode_throwsInvalidFileUrl() { let sut = makeSUT() let formData = MultipartFormData() + // swiftlint:disable:next force_unwrapping let tmpFileUrl = URL(string: "invalid/path")! do { @@ -84,7 +85,7 @@ final class MultipartFormDataEncoderTests: XCTestCase { XCTFail("Encoding should have failed.") } catch MultipartFormData.EncodingError.invalidFileUrl { } catch { - XCTFail("Should have failed with MultipartFormData.EncodingError.fileAlreadyExists") + XCTFail("Should have failed with MultipartFormData.EncodingError.invalidFileUrl") } } diff --git a/Tests/NetworkingTests/StatusCodeProcessorTests.swift b/Tests/NetworkingTests/StatusCodeProcessorTests.swift index c40a033d..3240b711 100644 --- a/Tests/NetworkingTests/StatusCodeProcessorTests.swift +++ b/Tests/NetworkingTests/StatusCodeProcessorTests.swift @@ -1,6 +1,6 @@ // // StatusCodeProcessorTests.swift -// +// // // Created by Matej Molnár on 01.12.2022. // @@ -24,22 +24,22 @@ final class StatusCodeProcessorTests: XCTestCase { var path: String { switch self { case .emptyAcceptStatuses: - return "emptyAcceptStatuses" + "emptyAcceptStatuses" case .regularAcceptStatuses: - return "regularAcceptStatuses" + "regularAcceptStatuses" case .irregularAcceptStatuses: - return "irregularAcceptStatuses" + "irregularAcceptStatuses" } } var acceptableStatusCodes: Range? { switch self { case .emptyAcceptStatuses: - return nil + nil case .regularAcceptStatuses: - return HTTPStatusCode.successAndRedirectCodes + HTTPStatusCode.successAndRedirectCodes case .irregularAcceptStatuses: - return 400 ..< 500 + 400 ..< 500 } } } @@ -120,9 +120,11 @@ final class StatusCodeProcessorTests: XCTestCase { // MARK: - Factory methods to create mock objects private extension StatusCodeProcessorTests { + func createMockResponseParams( _ router: MockRouter, statusCode: HTTPStatusCode + // swiftlint:disable:next large_tuple ) -> (response: Response, urlRequest: URLRequest, endpointRequest: EndpointRequest) { let mockEndpointRequest = EndpointRequest(router, sessionId: sessionId) let mockURLRequest = URLRequest(url: router.baseURL)