diff --git a/.github/workflows/swift-tests.yaml b/.github/workflows/swift-tests.yaml index d322710..8e1d00d 100644 --- a/.github/workflows/swift-tests.yaml +++ b/.github/workflows/swift-tests.yaml @@ -9,8 +9,8 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Select Xcode 16.4 - run: sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer + - name: Select Xcode 26.0 + run: sudo xcode-select -s /Applications/Xcode_26.app/Contents/Developer - name: Build run: swift build -v - name: Run tests diff --git a/README.md b/README.md index f50f92f..acb424b 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,13 @@ A Swift package that provides elegant state management for asynchronous operatio ## Overview -AsyncLoad provides two main components for handling asynchronous operations: +AsyncLoad provides components for handling asynchronous operations: - `AsyncLoad`: For loading data operations -- `AsyncAction`: For action-based operations +- `AsyncAction`: For action-based operations +- `CachedAsyncLoad`: For loading operations that preserve cached data during refreshes +- `CachedAsyncAction`: For actions that preserve cached data during retries - `AsyncLoadView`: A SwiftUI view component for displaying async states +- `CachedAsyncLoadView`: A SwiftUI view component for cached async states ## Requirements @@ -25,7 +28,7 @@ Add AsyncLoad to your project by adding the following to your `Package.swift`: ```swift dependencies: [ - .package(url: "https://github.com/yourusername/AsyncLoad", from: "1.0.0") + .package(url: "https://github.com/diamirio/AsyncLoad", from: "1.0.0") ] ``` @@ -118,6 +121,101 @@ class FormViewModel { } ``` +### CachedAsyncLoad + +An enhanced version of AsyncLoad that preserves cached data during loading and error states. + +```swift +public enum CachedAsyncLoad: Equatable { + case none // Initial state + case loading(T? = nil) // Loading with optional cached data + case error(T? = nil, Error) // Error with optional cached data + case loaded(T) // Successfully loaded with data +} +``` + +#### Properties + +- `isLoading: Bool` - Returns true if the state is `.loading` +- `item: T?` - Returns the loaded item if state is `.loaded`, nil otherwise +- `error: Error?` - Returns the error if state is `.error`, nil otherwise + +#### Example Usage + +```swift +import AsyncLoad + +@Observable +class CachedDataViewModel { + var userProfile: CachedAsyncLoad = .none + + func loadUserProfile(id: String) async { + // Start loading while preserving any existing data + if case .loaded(let existingUser) = userProfile { + userProfile = .loading(existingUser) + } else { + userProfile = .loading() + } + + do { + let user = try await userService.fetchUser(id: id) + userProfile = .loaded(user) + } catch { + // Preserve existing data even during error + let existingUser = userProfile.item + userProfile = .error(existingUser, error) + } + } +} +``` + +### CachedAsyncAction + +Similar to AsyncAction but preserves cached data during loading and error states. + +```swift +public enum CachedAsyncAction: Equatable { + case none // Initial state + case loading(T? = nil) // Action in progress with optional cached data + case error(T? = nil, Error) // Action failed with optional cached data + case success(T) // Action completed successfully +} +``` + +#### Properties + +- `isLoading: Bool` - Returns true if the state is `.loading` +- `item: T?` - Returns the success result if state is `.success`, nil otherwise +- `error: Error?` - Returns the error if state is `.error`, nil otherwise + +#### Example Usage + +```swift +import AsyncLoad + +@Observable +class CachedFormViewModel { + var submitAction: CachedAsyncAction = .none + + func submitForm(data: FormData) async { + // Preserve previous successful response during retry + if case .success(let previousResponse) = submitAction { + submitAction = .loading(previousResponse) + } else { + submitAction = .loading() + } + + do { + let response = try await apiService.submit(data) + submitAction = .success(response) + } catch { + let previousResponse = submitAction.item + submitAction = .error(previousResponse, error) + } + } +} +``` + ### AsyncLoadView A SwiftUI view component that automatically handles the display of different async states. @@ -182,16 +280,19 @@ struct UserProfileView: View { ## Features - **Type-safe**: Generic enums ensure type safety for your data -- **Equatable**: Both AsyncLoad and AsyncAction conform to Equatable for easy state comparison -- **SwiftUI Integration**: AsyncLoadView provides seamless integration with SwiftUI +- **Equatable**: All async state enums conform to Equatable for easy state comparison +- **SwiftUI Integration**: AsyncLoadView and CachedAsyncLoadView provide seamless integration with SwiftUI - **Error Handling**: Built-in error state management - **Loading States**: Automatic loading state handling with progress indicators +- **Cached Data**: CachedAsyncLoad and CachedAsyncAction preserve data during refreshes and errors - **Flexible UI**: Customizable content and error views ## Best Practices 1. **Use AsyncLoad for data fetching** operations (GET requests, loading content) 2. **Use AsyncAction for user actions** (POST/PUT/DELETE requests, form submissions) -3. **Always handle all states** in your UI to provide good user experience -4. **Use AsyncLoadView** for simple cases to reduce boilerplate code -5. **Reset states** appropriately (e.g., set to `.none` when appropriate) +3. **Use CachedAsyncLoad** when you want to preserve data during refreshes or show stale data during errors +4. **Use CachedAsyncAction** when you want to preserve previous results during action retries +5. **Always handle all states** in your UI to provide good user experience +6. **Use AsyncLoadView and CachedAsyncLoadView** for simple cases to reduce boilerplate code +7. **Reset states** appropriately (e.g., set to `.none` when appropriate) diff --git a/Sources/AsyncLoad/AsyncLoad/AsyncAction.swift b/Sources/AsyncLoad/AsyncLoad/AsyncAction.swift index 3da666f..215b3d8 100644 --- a/Sources/AsyncLoad/AsyncLoad/AsyncAction.swift +++ b/Sources/AsyncLoad/AsyncLoad/AsyncAction.swift @@ -1,6 +1,6 @@ import Foundation -public enum AsyncAction: Equatable { +public enum AsyncAction: Equatable, Sendable { case none case loading case error(Error) @@ -45,4 +45,34 @@ public enum AsyncAction: Equatable { false } } + + public static func == (lhs: AsyncAction, rhs: AsyncAction) -> Bool where T : Equatable { + switch (lhs, rhs) { + case (.none, .none): + true + case (.loading, .loading): + true + case (.error, .error): + true + case let (.success(lhsItem), .success(rhsItem)): + lhsItem == rhsItem + default: + false + } + } + + public static func != (lhs: AsyncAction, rhs: AsyncAction) -> Bool where T : Equatable { + switch (lhs, rhs) { + case (.none, .none): + false + case (.loading, .loading): + false + case (.error, .error): + false + case let (.success(lhsItem), .success(rhsItem)): + lhsItem != rhsItem + default: + true + } + } } diff --git a/Sources/AsyncLoad/AsyncLoad/AsyncLoad.swift b/Sources/AsyncLoad/AsyncLoad/AsyncLoad.swift index 0297b92..2fb48f9 100644 --- a/Sources/AsyncLoad/AsyncLoad/AsyncLoad.swift +++ b/Sources/AsyncLoad/AsyncLoad/AsyncLoad.swift @@ -1,6 +1,6 @@ import Foundation -public enum AsyncLoad: Equatable { +public enum AsyncLoad: Equatable, Sendable { case none case loading case error(Error) @@ -45,4 +45,34 @@ public enum AsyncLoad: Equatable { false } } + + public static func == (lhs: AsyncLoad, rhs: AsyncLoad) -> Bool where T : Equatable { + switch (lhs, rhs) { + case (.none, .none): + true + case (.loading, .loading): + true + case (.error, .error): + true + case let (.loaded(lhsItem), .loaded(rhsItem)): + lhsItem == rhsItem + default: + false + } + } + + public static func != (lhs: AsyncLoad, rhs: AsyncLoad) -> Bool where T : Equatable { + switch (lhs, rhs) { + case (.none, .none): + false + case (.loading, .loading): + false + case (.error, .error): + false + case let (.loaded(rhsItem), .loaded(lhsItem)): + lhsItem != rhsItem + default: + true + } + } } diff --git a/Sources/AsyncLoad/AsyncLoad/AsyncLoadView.swift b/Sources/AsyncLoad/AsyncLoad/AsyncLoadView.swift index bf2d30a..c57ef83 100644 --- a/Sources/AsyncLoad/AsyncLoad/AsyncLoadView.swift +++ b/Sources/AsyncLoad/AsyncLoad/AsyncLoadView.swift @@ -2,7 +2,11 @@ import Foundation #if canImport(SwiftUI) import SwiftUI -public struct AsyncLoadView: View { +public struct AsyncLoadView< + Item: Sendable, + Content: View, + ErrorContent: View +>: View { let state: AsyncLoad @ViewBuilder diff --git a/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncAction.swift b/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncAction.swift index c4488c7..162cc58 100644 --- a/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncAction.swift +++ b/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncAction.swift @@ -1,6 +1,6 @@ import Foundation -public enum CachedAsyncAction: Equatable { +public enum CachedAsyncAction: Equatable, Sendable { case none case loading(T? = nil) case error(T? = nil, Error) @@ -45,4 +45,34 @@ public enum CachedAsyncAction: Equatable { false } } + + public static func == (lhs: CachedAsyncAction, rhs: CachedAsyncAction) -> Bool where T : Equatable { + switch (lhs, rhs) { + case (.none, .none): + true + case let (.loading(lhsItem), .loading(rhsItem)): + lhsItem == rhsItem + case let (.error(lhsItem, _), .error(rhsItem, _)): + lhsItem == rhsItem + case let (.success(lhsItem), .success(rhsItem)): + lhsItem == rhsItem + default: + false + } + } + + public static func != (lhs: CachedAsyncAction, rhs: CachedAsyncAction) -> Bool where T : Equatable { + switch (lhs, rhs) { + case (.none, .none): + false + case let (.loading(lhsItem), .loading(rhsItem)): + lhsItem != rhsItem + case let (.error(lhsItem, _), .error(rhsItem, _)): + lhsItem != rhsItem + case let (.success(lhsItem), .success(rhsItem)): + lhsItem != rhsItem + default: + false + } + } } diff --git a/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncLoad.swift b/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncLoad.swift index 92cfa9a..6e79f1c 100644 --- a/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncLoad.swift +++ b/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncLoad.swift @@ -1,6 +1,6 @@ import Foundation -public enum CachedAsyncLoad: Equatable { +public enum CachedAsyncLoad: Equatable, Sendable { case none case loading(T? = nil) case error(T? = nil, Error) @@ -45,4 +45,34 @@ public enum CachedAsyncLoad: Equatable { false } } + + public static func == (lhs: CachedAsyncLoad, rhs: CachedAsyncLoad) -> Bool where T : Equatable { + switch (lhs, rhs) { + case (.none, .none): + true + case let (.loading(lhsItem), .loading(rhsItem)): + lhsItem == rhsItem + case let (.error(lhsItem, _), .error(rhsItem, _)): + lhsItem == rhsItem + case let (.loaded( lhsItem), .loaded(rhsItem)): + lhsItem == rhsItem + default: + false + } + } + + public static func != (lhs: CachedAsyncLoad, rhs: CachedAsyncLoad) -> Bool where T : Equatable { + switch (lhs, rhs) { + case (.none, .none): + true + case let (.loading(lhsItem), .loading(rhsItem)): + lhsItem != rhsItem + case let (.error(lhsItem, _), .error(rhsItem, _)): + lhsItem != rhsItem + case let (.loaded( lhsItem), .loaded(rhsItem)): + lhsItem != rhsItem + default: + false + } + } } diff --git a/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncLoadView.swift b/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncLoadView.swift index e1803ca..03c6e9b 100644 --- a/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncLoadView.swift +++ b/Sources/AsyncLoad/CachedAsyncLoad/CachedAsyncLoadView.swift @@ -3,7 +3,7 @@ import Foundation import SwiftUI public struct CachedAsyncLoadView< - Item, + Item: Sendable, Content: View, ErrorContent: View, LoadingContent: View diff --git a/Tests/AsyncLoadTests/AsyncActionEquatableItemTests.swift b/Tests/AsyncLoadTests/AsyncActionEquatableItemTests.swift new file mode 100644 index 0000000..602e927 --- /dev/null +++ b/Tests/AsyncLoadTests/AsyncActionEquatableItemTests.swift @@ -0,0 +1,43 @@ +import Testing +@testable import AsyncLoad + +@Suite("Test equatable AsyncAction") +struct AsyncActionEquatableItemTests { + @Test<[AsyncActionParameter]>("Should be equal ", arguments: [ + .init(.none, .none), + .init(.loading, .loading), + .init(.error(TestingError.some), .error(TestingError.some)), + .init(.error(TestingError.some), .error(TestingError.other)), + .init(.success("some"), .success("some")), + ]) + func equalString(param: AsyncActionParameter) async throws { + #expect(param.action1 == param.action2) + } + + @Test<[AsyncActionParameter]>("Should not be equal ", arguments: [ + .init(.success("some"), .success("other")), + .init(.error(TestingError.some), .success("other")), + ]) + func nonEqualString(param: AsyncActionParameter) async throws { + #expect(param.action1 != param.action2) + } + + @Test<[AsyncActionParameter]>("Should be equal ", arguments: [ + .init(.none, .none), + .init(.loading, .loading), + .init(.error(TestingError.some), .error(TestingError.some)), + .init(.error(TestingError.some), .error(TestingError.other)), + .init(.success(User(name: "some")), .success(User(name: "some"))), + ]) + func equalUser(param: AsyncActionParameter) async throws { + #expect(param.action1 == param.action2) + } + + @Test<[AsyncActionParameter]>("Should not be equal ", arguments: [ + .init(.success(User(name: "some")), .success(User(name: "other"))), + .init(.error(TestingError.some), .success(User(name: "some"))), + ]) + func nonEqualuser(param: AsyncActionParameter) async throws { + #expect(param.action1 != param.action2) + } +} diff --git a/Tests/AsyncLoadTests/AsyncActionNonEquatableItemTests.swift b/Tests/AsyncLoadTests/AsyncActionNonEquatableItemTests.swift new file mode 100644 index 0000000..c5b45dd --- /dev/null +++ b/Tests/AsyncLoadTests/AsyncActionNonEquatableItemTests.swift @@ -0,0 +1,26 @@ +import Testing +@testable import AsyncLoad + +@Suite("Test non-equatable AsyncAction") +struct AsyncActionNonEquatableItemTests { + @Test<[AsyncActionParameter]>("Should be equal (structural)", arguments: [ + .init(.none, .none), + .init(.loading, .loading), + .init(.success(NonEquatableItem(name: "Hello")), .success(NonEquatableItem(name: "Different"))), + .init(.success(NonEquatableItem(name: "Some")), .success(NonEquatableItem(name: "Other"))), + .init(.error(TestingError.some), .error(TestingError.some)), + .init(.error(TestingError.some), .error(TestingError.other)), + ]) + func structuralEquality(param: AsyncActionParameter) async throws { + #expect(param.action1 == param.action2) + } + + @Test<[AsyncActionParameter]>("Should be equal (mixed types)", arguments: [ + .init(.loading, .loading), + .init(.error(TestingError.some), .error(TestingError.some)), + .init(.error(TestingError.some), .error(TestingError.other)), + ]) + func mixedTypeEquality(param: AsyncActionParameter) async throws { + #expect(param.action1 == param.action2) + } +} diff --git a/Tests/AsyncLoadTests/AsyncLoadEquatableItemTests.swift b/Tests/AsyncLoadTests/AsyncLoadEquatableItemTests.swift new file mode 100644 index 0000000..d0a44fa --- /dev/null +++ b/Tests/AsyncLoadTests/AsyncLoadEquatableItemTests.swift @@ -0,0 +1,43 @@ +import Testing +@testable import AsyncLoad + +@Suite("Test equatable AsyncLoad") +struct AsyncLoadEquatableItemTests { + @Test<[AsyncLoadParameter]>("Should be equal ", arguments: [ + .init(.none, .none), + .init(.loading, .loading), + .init(.error(TestingError.some), .error(TestingError.some)), + .init(.error(TestingError.some), .error(TestingError.other)), + .init(.loaded("some"), .loaded("some")), + ]) + func equalString(param: AsyncLoadParameter) async throws { + #expect(param.load1 == param.load2) + } + + @Test<[AsyncLoadParameter]>("Should not be equal ", arguments: [ + .init(.loaded("Some"), .loaded("Other")), + .init(.error(TestingError.some), .loaded("some")), + ]) + func nonEqualString(param: AsyncLoadParameter) async throws { + #expect(param.load1 != param.load2) + } + + @Test<[AsyncLoadParameter]>("Should be equal ", arguments: [ + .init(.none, .none), + .init(.loading, .loading), + .init(.error(TestingError.some), .error(TestingError.some)), + .init(.error(TestingError.some), .error(TestingError.other)), + .init(.loaded(User(name: "some")), .loaded(User(name: "some"))), + ]) + func equalUser(param: AsyncLoadParameter) async throws { + #expect(param.load1 == param.load2) + } + + @Test<[AsyncLoadParameter]>("Should not be equal ", arguments: [ + .init(.loaded(User(name: "Alex")), .loaded(User(name: "Daniel"))), + .init(.error(TestingError.some), .loaded(User(name: "some"))), + ]) + func nonEqualUser(param: AsyncLoadParameter) async throws { + #expect(param.load1 != param.load2) + } +} diff --git a/Tests/AsyncLoadTests/AsyncLoadNonEquatableItemTests.swift b/Tests/AsyncLoadTests/AsyncLoadNonEquatableItemTests.swift new file mode 100644 index 0000000..9281991 --- /dev/null +++ b/Tests/AsyncLoadTests/AsyncLoadNonEquatableItemTests.swift @@ -0,0 +1,26 @@ +import Testing +@testable import AsyncLoad + +@Suite("Test non-equatable AsyncLoad") +struct AsyncLoadNonEquatableItemTests { + @Test<[AsyncLoadParameter]>("Should be equal (structural)", arguments: [ + .init(.none, .none), + .init(.loading, .loading), + .init(.loaded(NonEquatableItem(name: "Hello")), .loaded(NonEquatableItem(name: "Different"))), + .init(.loaded(NonEquatableItem(name: "Some")), .loaded(NonEquatableItem(name: "Other"))), + .init(.error(TestingError.some), .error(TestingError.some)), + .init(.error(TestingError.some), .error(TestingError.other)), + ]) + func structuralEquality(param: AsyncLoadParameter) async throws { + #expect(param.load1 == param.load2) + } + + @Test<[AsyncLoadParameter]>("Should be equal (mixed types)", arguments: [ + .init(.loading, .loading), + .init(.error(TestingError.some), .error(TestingError.some)), + .init(.error(TestingError.some), .error(TestingError.other)), + ]) + func mixedTypeEquality(param: AsyncLoadParameter) async throws { + #expect(param.load1 == param.load2) + } +} diff --git a/Tests/AsyncLoadTests/AsyncLoadTests.swift b/Tests/AsyncLoadTests/AsyncLoadTests.swift deleted file mode 100644 index 1cbecb5..0000000 --- a/Tests/AsyncLoadTests/AsyncLoadTests.swift +++ /dev/null @@ -1,56 +0,0 @@ -import Testing -@testable import AsyncLoad - -enum TestingError: Error { - case some -} - -@Test func testAsyncLoadEquatable() async throws { - var load1: AsyncLoad = .none - var load2: AsyncLoad = .none - - #expect(load1 == load2) - - load1 = .loading - load2 = .loading - - #expect(load1 == load2) - - load1 = .loaded("some") - load2 = .loaded("other") - - #expect(load1 == load2) - - load1 = .error(TestingError.some) - load2 = .error(TestingError.some) - - #expect(load1 == load2) -} - - -@Test func testCachedAsyncLoadEquatable() async throws { - var load1: CachedAsyncLoad = .none - var load2: CachedAsyncLoad = .none - - #expect(load1 == load2) - - load1 = .loading() - load2 = .loading() - - #expect(load1 == load2) - - load1 = .loaded("some") - load2 = .loaded("other") - - #expect(load1 == load2) - - load1 = .loading("some") - load2 = .loading("other") - - #expect(load1 == load2) - - load1 = .error("some", TestingError.some) - load2 = .error(nil, TestingError.some) - - #expect(load1 == load2) -} diff --git a/Tests/AsyncLoadTests/CachedAsyncActionEquatableItemTests.swift b/Tests/AsyncLoadTests/CachedAsyncActionEquatableItemTests.swift new file mode 100644 index 0000000..25fb8a9 --- /dev/null +++ b/Tests/AsyncLoadTests/CachedAsyncActionEquatableItemTests.swift @@ -0,0 +1,48 @@ +import Testing +@testable import AsyncLoad + +@Suite("Test equatable CachedAsyncAction") +struct CachedAsyncActionEquatableItemTests { + @Test<[CachedAsyncActionParameter]>("Should be equal ", arguments: [ + .init(.none, .none), + .init(.loading(), .loading()), + .init(.success("some"), .success("some")), + .init(.loading("some"), .loading("some")), + .init(.error("some", TestingError.some), .error("some", TestingError.some)), + ]) + func equalString(param: CachedAsyncActionParameter) async throws { + #expect(param.action1 == param.action2) + } + + @Test<[CachedAsyncActionParameter]>("Should not be equal ", arguments: [ + .init(.success("Other"), .success("Other1")), + .init(.loading("some"), .loading("other")), + .init(.error(nil, TestingError.some), .error("some", TestingError.some)), + .init(.error(nil, TestingError.some), .error("some", TestingError.other)), + .init(.error("some", TestingError.some), .error(nil, TestingError.some)), + ]) + func nonEqualString(param: CachedAsyncActionParameter) async throws { + #expect(param.action1 != param.action2) + } + + @Test<[CachedAsyncActionParameter]>("Should be equal ", arguments: [ + .init(.none, .none), + .init(.loading(), .loading()), + .init(.success(User(name: "some")), .success(User(name: "some"))), + .init(.loading(User(name: "some")), .loading(User(name: "some"))), + .init(.error(User(name: "some"), TestingError.some), .error(User(name: "some"), TestingError.some)), + ]) + func equalUser(param: CachedAsyncActionParameter) async throws { + #expect(param.action1 == param.action2) + } + + @Test<[CachedAsyncActionParameter]>("Should not be equal ", arguments: [ + .init(.success(User(name: "Alex")), .success(User(name: "Daniel"))), + .init(.loading(User(name: "Alex")), .loading(User(name: "Daniel"))), + .init(.error(nil, TestingError.some), .error(User(name: "some"), TestingError.some)), + .init(.error(User(name: "Alex"), TestingError.some), .error(User(name: "Daniel"), TestingError.some)), + ]) + func nonEqualUser(param: CachedAsyncActionParameter) async throws { + #expect(param.action1 != param.action2) + } +} diff --git a/Tests/AsyncLoadTests/CachedAsyncActionNonEquatableItemTests.swift b/Tests/AsyncLoadTests/CachedAsyncActionNonEquatableItemTests.swift new file mode 100644 index 0000000..e369b89 --- /dev/null +++ b/Tests/AsyncLoadTests/CachedAsyncActionNonEquatableItemTests.swift @@ -0,0 +1,19 @@ +import Testing +@testable import AsyncLoad + +@Suite("Test non-equatable CachedAsyncAction") +struct CachedAsyncActionNonEquatableItemTests { + @Test<[CachedAsyncActionParameter]>("Should be equal (structural)", arguments: [ + .init(.none, .none), + .init(.loading(), .loading()), + .init(.success(NonEquatableItem(name: "Hello")), .success(NonEquatableItem(name: "Different"))), + .init(.success(NonEquatableItem(name: "Some")), .success(NonEquatableItem(name: "Other"))), + .init(.loading(NonEquatableItem(name: "Some")), .loading(NonEquatableItem(name: "Other"))), + .init(.error(nil, TestingError.some), .error(nil, TestingError.some)), + .init(.error(nil, TestingError.some), .error(NonEquatableItem(name: "Hello"), TestingError.other)), + .init(.error(NonEquatableItem(name: "Hello"), TestingError.some), .error(NonEquatableItem(name: "World"), TestingError.some)), + ]) + func structuralEquality(param: CachedAsyncActionParameter) async throws { + #expect(param.action1 == param.action2) + } +} diff --git a/Tests/AsyncLoadTests/CachedAsyncLoadEquatableItemTests.swift b/Tests/AsyncLoadTests/CachedAsyncLoadEquatableItemTests.swift new file mode 100644 index 0000000..e554e38 --- /dev/null +++ b/Tests/AsyncLoadTests/CachedAsyncLoadEquatableItemTests.swift @@ -0,0 +1,48 @@ +import Testing +@testable import AsyncLoad + +@Suite("Test equatable CachedAsyncLoad") +struct CachedAsyncLoadEquatableItemTests { + @Test<[CachedAsyncLoadParameter]>("Should be equal ", arguments: [ + .init(.none, .none), + .init(.loading(), .loading()), + .init(.loaded("some"), .loaded("some")), + .init(.loading("some"), .loading("some")), + .init(.error("some", TestingError.some), .error("some", TestingError.some)), + ]) + func equalString(param: CachedAsyncLoadParameter) async throws { + #expect(param.load1 == param.load2) + } + + @Test<[CachedAsyncLoadParameter]>("Should not be equal ", arguments: [ + .init(.loaded("Other"), .loaded("Other1")), + .init(.loading("some"), .loading("other")), + .init(.error(nil, TestingError.some), .error("some", TestingError.some)), + .init(.error(nil, TestingError.some), .error("some", TestingError.other)), + .init(.error("some", TestingError.some), .error(nil, TestingError.some)), + ]) + func nonEqualString(param: CachedAsyncLoadParameter) async throws { + #expect(param.load1 != param.load2) + } + + @Test<[CachedAsyncLoadParameter]>("Should be equal ", arguments: [ + .init(.none, .none), + .init(.loading(), .loading()), + .init(.loaded(User(name: "some")), .loaded(User(name: "some"))), + .init(.loading(User(name: "some")), .loading(User(name: "some"))), + .init(.error(User(name: "some"), TestingError.some), .error(User(name: "some"), TestingError.some)), + ]) + func equalUser(param: CachedAsyncLoadParameter) async throws { + #expect(param.load1 == param.load2) + } + + @Test<[CachedAsyncLoadParameter]>("Should not be equal ", arguments: [ + .init(.loaded(User(name: "Alex")), .loaded(User(name: "Daniel"))), + .init(.loading(User(name: "Alex")), .loading(User(name: "Daniel"))), + .init(.error(nil, TestingError.some), .error(User(name: "some"), TestingError.some)), + .init(.error(User(name: "Alex"), TestingError.some), .error(User(name: "Daniel"), TestingError.some)), + ]) + func nonEqualUser(param: CachedAsyncLoadParameter) async throws { + #expect(param.load1 != param.load2) + } +} diff --git a/Tests/AsyncLoadTests/CachedAsyncLoadNonEquatableItemTests.swift b/Tests/AsyncLoadTests/CachedAsyncLoadNonEquatableItemTests.swift new file mode 100644 index 0000000..c55af85 --- /dev/null +++ b/Tests/AsyncLoadTests/CachedAsyncLoadNonEquatableItemTests.swift @@ -0,0 +1,19 @@ +import Testing +@testable import AsyncLoad + +@Suite("Test non-equatable CachedAsyncLoad") +struct CachedAsyncLoadNonEquatableItemTests { + @Test<[CachedAsyncLoadParameter]>("Should be equal (structural)", arguments: [ + .init(.none, .none), + .init(.loading(), .loading()), + .init(.loaded(NonEquatableItem(name: "Hello")), .loaded(NonEquatableItem(name: "Different"))), + .init(.loaded(NonEquatableItem(name: "Some")), .loaded(NonEquatableItem(name: "Other"))), + .init(.loading(NonEquatableItem(name: "Some")), .loading(NonEquatableItem(name: "Other"))), + .init(.error(nil, TestingError.some), .error(nil, TestingError.some)), + .init(.error(nil, TestingError.some), .error(NonEquatableItem(name: "Hello"), TestingError.other)), + .init(.error(NonEquatableItem(name: "Hello"), TestingError.some), .error(NonEquatableItem(name: "World"), TestingError.some)), + ]) + func structuralEquality(param: CachedAsyncLoadParameter) async throws { + #expect(param.load1 == param.load2) + } +} diff --git a/Tests/AsyncLoadTests/Utils.swift b/Tests/AsyncLoadTests/Utils.swift new file mode 100644 index 0000000..b3cdbb0 --- /dev/null +++ b/Tests/AsyncLoadTests/Utils.swift @@ -0,0 +1,59 @@ +import Foundation +@testable import AsyncLoad + +enum TestingError: Error { + case some + case other +} + +final class NonEquatableItem: Sendable { + let name: String + + init(name: String) { + self.name = name + } +} + +struct User: Equatable { + let name: String +} + +struct AsyncActionParameter { + let action1: AsyncAction + let action2: AsyncAction + + init(_ action1: AsyncAction, _ action2: AsyncAction) { + self.action1 = action1 + self.action2 = action2 + } +} + +struct AsyncLoadParameter { + let load1: AsyncLoad + let load2: AsyncLoad + + init(_ load1: AsyncLoad, _ load2: AsyncLoad) { + self.load1 = load1 + self.load2 = load2 + } +} + +struct CachedAsyncActionParameter { + let action1: CachedAsyncAction + let action2: CachedAsyncAction + + init(_ action1: CachedAsyncAction, _ action2: CachedAsyncAction) { + self.action1 = action1 + self.action2 = action2 + } +} + +struct CachedAsyncLoadParameter { + let load1: CachedAsyncLoad + let load2: CachedAsyncLoad + + init(_ load1: CachedAsyncLoad, _ load2: CachedAsyncLoad) { + self.load1 = load1 + self.load2 = load2 + } +}