Skip to content

Commit

Permalink
Merge pull request #2 from ra1028/improve-async-status
Browse files Browse the repository at this point in the history
Improve AsyncStatus
  • Loading branch information
ra1028 committed Mar 5, 2021
2 parents 00a61c2 + be24220 commit 7bf5d0b
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 25 deletions.
21 changes: 5 additions & 16 deletions Examples/TheMovieDB/CustomHooks/UseFetchPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,10 @@ func useFetchPage<Response: Decodable>(
results.current += response.results
}

guard page.current == 0 else {
return (status: .success(results.current), fetch: fetch)
}

switch status {
case .pending:
return (status: .pending, fetch: fetch)

case .running:
return (status: .running, fetch: fetch)
let newStatus =
page.current == 0
? status.map { _ in results.current }
: .success(results.current)

case .success:
return (status: .success(results.current), fetch: fetch)

case .failure(let error):
return (status: .failure(error), fetch: fetch)
}
return (status: newStatus, fetch: fetch)
}
8 changes: 1 addition & 7 deletions Examples/TheMovieDB/CustomHooks/UseNetworkImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,5 @@ func useNetworkImage(for path: String, size: NetworkImageSize) -> UIImage? {
.receive(on: DispatchQueue.main)
}

switch status {
case .success(let image):
return image

case .failure, .pending, .running:
return nil
}
return try? status.get() ?? nil
}
6 changes: 5 additions & 1 deletion Examples/TheMovieDB/TopRatedMoviesPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ struct TopRatedMoviesPage: HookView {
failure(error, onReload: fetch)

case .pending, .running:
ProgressView()
loading
}
}
.navigationTitle("Top Rated Movies")
Expand All @@ -31,6 +31,10 @@ struct TopRatedMoviesPage: HookView {
.onAppear(perform: fetch)
}

var loading: some View {
ProgressView().frame(maxWidth: .infinity, maxHeight: .infinity)
}

func failure(_ error: URLError, onReload: @escaping () -> Void) -> some View {
VStack(spacing: 16) {
Text("Failed to fetch movies")
Expand Down
82 changes: 81 additions & 1 deletion Sources/Hooks/AsyncStatus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,97 @@ public enum AsyncStatus<Success, Failure: Error> {
/// Represents a success status meaning that the operation provided an error with failure.
case failure(Failure)

/// Returns a result converted from the status. If the status is `pending` or `running`, it returns nil.
/// Returns a Boolean value indicating whether this instance represents a `running`.
public var isRunning: Bool {
guard case .running = self else {
return false
}
return true
}

/// Returns a result converted from the status.
/// If this instance represents a `pending` or a `running`, this returns nil.
public var result: Result<Success, Failure>? {
switch self {
case .pending, .running:
return nil

case .success(let success):
return .success(success)

case .failure(let error):
return .failure(error)
}
}

/// Returns a new status, mapping any success value using the given transformation.
/// - Parameter transform: A closure that takes the success value of this instance.
/// - Returns: An `AsyncStatus` instance with the result of evaluating `transform` as the new success value if this instance represents a success.
public func map<NewSuccess>(_ transform: (Success) -> NewSuccess) -> AsyncStatus<NewSuccess, Failure> {
flatMap { .success(transform($0)) }
}

/// Returns a new result, mapping any failure value using the given transformation.
/// - Parameter transform: A closure that takes the failure value of the instance.
/// - Returns: An `AsyncStatus` instance with the result of evaluating `transform` as the new failure value if this instance represents a failure.
public func mapError<NewFailure: Error>(_ transform: (Failure) -> NewFailure) -> AsyncStatus<Success, NewFailure> {
flatMapError { .failure(transform($0)) }
}

/// Returns a new result, mapping any success value using the given transformation and unwrapping the produced status.
/// - Parameter transform: A closure that takes the success value of the instance.
/// - Returns: An `AsyncStatus` instance, either from the closure or the previous `.success`.
public func flatMap<NewSuccess>(_ transform: (Success) -> AsyncStatus<NewSuccess, Failure>) -> AsyncStatus<NewSuccess, Failure> {
switch self {
case .pending:
return .pending

case .running:
return .running

case .success(let value):
return transform(value)

case .failure(let error):
return .failure(error)
}
}

/// Returns a new result, mapping any failure value using the given transformation and unwrapping the produced status.
/// - Parameter transform: A closure that takes the failure value of the instance.
/// - Returns: An `AsyncStatus` instance, either from the closure or the previous `.failure`.
public func flatMapError<NewFailure: Error>(_ transform: (Failure) -> AsyncStatus<Success, NewFailure>) -> AsyncStatus<Success, NewFailure> {
switch self {
case .pending:
return .pending

case .running:
return .running

case .success(let value):
return .success(value)

case .failure(let error):
return transform(error)
}
}

/// Returns the success value as a throwing expression.
/// If this instance represents a `pending` or a `running`, this returns nil.
///
/// Use this method to retrieve the value of this status if it represents a success, or to catch the value if it represents a failure.
/// - Throws: The failure value, if the instance represents a failure.
/// - Returns: The success value, if the instance represents a success,If the status is `pending` or `running`, this returns nil. .
public func get() throws -> Success? {
switch self {
case .pending, .running:
return nil

case .success(let value):
return value

case .failure(let error):
throw error
}
}
}
Expand Down
140 changes: 140 additions & 0 deletions Tests/AsyncStatusTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@ import Hooks
import XCTest

final class AsyncStatusTests: XCTestCase {
func testIsRunning() {
let statuses: [AsyncStatus<Int, URLError>] = [
.pending,
.running,
.success(0),
.failure(URLError(.badURL)),
]

let expected = [
false,
true,
false,
false,
]

for (status, expected) in zip(statuses, expected) {
XCTAssertEqual(status.isRunning, expected)
}
}

func testResult() {
let statuses: [AsyncStatus<Int, URLError>] = [
.pending,
Expand All @@ -21,4 +41,124 @@ final class AsyncStatusTests: XCTestCase {
XCTAssertEqual(status.result, expected)
}
}

func testMap() {
let statuses: [AsyncStatus<Int, URLError>] = [
.pending,
.running,
.success(0),
.failure(URLError(.badURL)),
]

let expected: [AsyncStatus<Int, URLError>] = [
.pending,
.running,
.success(100),
.failure(URLError(.badURL)),
]

for (status, expected) in zip(statuses, expected) {
XCTAssertEqual(status.map { _ in 100 }, expected)
}
}

func testMapError() {
let statuses: [AsyncStatus<Int, URLError>] = [
.pending,
.running,
.success(0),
.failure(URLError(.badURL)),
]

let expected: [AsyncStatus<Int, URLError>] = [
.pending,
.running,
.success(0),
.failure(URLError(.cancelled)),
]

for (status, expected) in zip(statuses, expected) {
XCTAssertEqual(
status.mapError { _ in URLError(.cancelled) },
expected
)
}
}

func testFlatMap() {
let statuses: [AsyncStatus<Int, URLError>] = [
.pending,
.running,
.success(0),
.failure(URLError(.badURL)),
]

let expected: [AsyncStatus<Int, URLError>] = [
.pending,
.running,
.failure(URLError(.callIsActive)),
.failure(URLError(.badURL)),
]

for (status, expected) in zip(statuses, expected) {
XCTAssertEqual(
status.flatMap { _ in .failure(URLError(.callIsActive)) },
expected
)
}
}

func testFlatMapError() {
let statuses: [AsyncStatus<Int, URLError>] = [
.pending,
.running,
.success(0),
.failure(URLError(.badURL)),
]

let expected: [AsyncStatus<Int, URLError>] = [
.pending,
.running,
.success(0),
.success(100),
]

for (status, expected) in zip(statuses, expected) {
XCTAssertEqual(
status.flatMapError { _ in .success(100) },
expected
)
}
}

func testGet() throws {
let statuses: [AsyncStatus<Int, URLError>] = [
.pending,
.running,
.success(100),
]

let expected: [Int?] = [
nil,
nil,
100,
nil,
]

for (status, expected) in zip(statuses, expected) {
XCTAssertEqual(
try status.get(),
expected
)
}

do {
let error = URLError(.badServerResponse)
_ = try AsyncStatus<Int, URLError>.failure(error).get()
}
catch {
let error = error as? URLError
XCTAssertEqual(error, URLError(.badServerResponse))
}
}
}

0 comments on commit 7bf5d0b

Please sign in to comment.