Skip to content

Commit

Permalink
renamed RetryBehavior to RetryStrategy. update docs.
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronsky committed Aug 29, 2024
1 parent b995660 commit 1a445cc
Show file tree
Hide file tree
Showing 15 changed files with 62 additions and 39 deletions.
1 change: 1 addition & 0 deletions Examples/Utilities/EnvAuthenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public struct EnvAuthenticator: Authenticator {
/// Creates an ``EnvAuthenticator`` using predefined environment names for its internal inputs.
///
/// - Parameters:
/// - api: The Apple API this token is compatible.
/// - environmentFile: Path to an environment file. Defaults to a `.env` file in the current directory.
/// - keyIDVariableName: The environment variable name to refer to the API private key ID.
/// - issuerIDVariableName: The environment variable name to refer to the API issuer ID.
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ do {

You can learn more about how to handle errors from the App Store Connect API at Apple's documentation page via [Interpreting and Handling Errors](https://developer.apple.com/documentation/appstoreconnectapi/interpreting_and_handling_errors). You can learn more about rate limiting at Apple's documentation page via [Identifying Rate Limits](https://developer.apple.com/documentation/appstoreconnectapi/identifying_rate_limits). Corresponding documentation for the Enterprise Program API can be found [here](https://developer.apple.com/documentation/enterpriseprogramapi/interpreting-and-handling-errors) and [here](https://developer.apple.com/documentation/enterpriseprogramapi/identifying-rate-limits), respectively.


Finally, `AppStoreConnectClient` can automatically retry API errors with a status code of 500. Pass a `RetryStrategy` value to the `retry:` parameter on any `AppStoreConnectClient` method. Supported values are `.never` (default), `.fixedInterval`, `.exponentialBackoff`, or a `.custom` handler.

### Paging Large Data Sets

All requests for resource collections (apps, builds, beta groups, etc.) support pagination. Responses for paginated resources will contain a `links` property of type `PagedDocumentLinks`, with "reference" URLs for `first`, `next`, and `self`. You can also find more information about the per-page limit and total count of resources in the response's `meta` field of type `PagingInformation`. You typically shouldn't require any of this information for typical pagination.
Expand All @@ -116,7 +119,7 @@ for try await appsPage in client.pages(Resources.v1.apps.get()) {
}
```

You can also page forward manually using the `send(_:pageAfter:)` method on `AppStoreConnectClient`.
You can also page forward manually using the `send(_:pageAfter:retry:)` method on `AppStoreConnectClient`.

### Uploading Assets

Expand Down
2 changes: 1 addition & 1 deletion Sources/AppStoreAPI/AppStoreAPI.docc/AppStoreAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The entire [publicly documented API surface](https://developer.apple.com/documen
### Uploading Files

- <doc:UploadingFiles>
- ``AppStoreConnectClient/upload(operation:from:)``
- ``AppStoreConnect/AppStoreConnectClient/upload(operation:from:retry:)``
- ``UploadOperation``
- ``HTTPHeader``
- ``AppMediaAssetState``
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ let reserveScreenshot = try await client.send(
)
```

Using the screenshot response from the last step, unwrap the ``AppScreenshot/Attributes-swift.struct/uploadOperations`` array. While iterating on the array, provide the screenshot data and each operation in sequential order to ``AppStoreConnectClient/upload(operation:from:)``.
Using the screenshot response from the last step, unwrap the ``AppScreenshot/Attributes-swift.struct/uploadOperations`` array. While iterating on the array, provide the screenshot data and each operation in sequential order to ``AppStoreConnect/AppStoreConnectClient/upload(operation:from:retry:)``.

```swift
guard let uploadOperations = reserveScreenshot.data.attributes?.uploadOperations else { return }
Expand Down Expand Up @@ -76,6 +76,6 @@ let committedScreenshot = try await client.send(

Committing the asset will cause App Store Connect to asynchronously process the asset. It may take several minutes to complete processing. You can perform ``Resources/V1-swift.struct/AppScreenshots-swift.struct/WithID/get(fieldsAppScreenshots:include:)`` to check its processing status and catch errors, which will be stored in ``AppScreenshot/Attributes-swift.struct/assetDeliveryState``. If your asset's state is set to ``AppMediaAssetState/State-swift.enum/failed``, you will need to restart this workflow to create a new screenshot with the errors addressed.

- ``AppStoreConnectClient/upload(operation:from:)``
- ``AppStoreConnect/AppStoreConnectClient/upload(operation:from:retry:)``
- ``UploadOperation``
- ``AppMediaAssetState``
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

### Uploading Files

- ``upload(_:)``
- ``AppStoreConnect/AppStoreConnectClient/upload(operation:from:retry:)``
3 changes: 2 additions & 1 deletion Sources/AppStoreAPI/UploadOperations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ extension AppStoreConnectClient {
/// - Parameters:
/// - operation: Information about the expected size of the chunk and its upload destination.
/// - data: The data representation of the uploaded resource.
/// - retry: Retry strategy.
/// - Throws: An error describing the manner in which the upload failed to complete.
public func upload(
operation: UploadOperation,
from data: Data,
retry: RetryBehavior = .never
retry: RetryStrategy = .never
) async throws {
guard let offset = operation.offset, let length = operation.length else {
throw UploadOperation.Error.chunkBoundsMismatch(offset: operation.offset, length: operation.length)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ This target is the client, networking layer, authentication layer, and queueing

### Paging Large Data Sets

- ``AppStoreConnectClient/pages(_:)``
- ``AppStoreConnectClient/pages(_:retry:)``
- ``PagedResponses``
- ``PagedDocumentLinks``

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@

### Sending Requests

- ``send(_:)-4zqp1``
- ``send(_:)-5w42d``
- ``send(_:pageAfter:)``
- ``send(_:retry:)-tm4b``
- ``send(_:retry:)-4kv7j``
- ``send(_:pageAfter:retry:)``

#### Pagination

- ``pages(_:)``
- ``send(_:pageAfter:)``
- ``pages(_:retry:)``
- ``send(_:pageAfter:retry:)``

### Downloading Files

- ``download(_:)``
- ``download(_:retry:)``
32 changes: 23 additions & 9 deletions Sources/AppStoreConnect/AppStoreConnectClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,14 @@ public actor AppStoreConnectClient {
// MARK: - Requests

/// Performs the given request asynchronously.
/// - Parameter request: A request.
///
/// - Parameters:
/// - request: A request.
/// - retry: Retry strategy.
/// - Throws: An error describing the manner in which the request failed to complete.
public func send(
_ request: Request<Void>,
retry: RetryBehavior = .never
retry: RetryStrategy = .never
) async throws {
let urlRequest = try URLRequest(request: request, encoder: encoder, authenticator: &authenticator)

Expand All @@ -62,12 +65,15 @@ public actor AppStoreConnectClient {
}

/// Performs the given request asynchronously.
/// - Parameter request: A request.
///
/// - Parameters:
/// - request: A request.
/// - retry: Retry strategy.
/// - Returns: The response from the App Store Connect API.
/// - Throws: An error describing the manner in which the request failed to complete.
public func send<Response>(
_ request: Request<Response>,
retry: RetryBehavior = .never
retry: RetryStrategy = .never
) async throws -> Response where Response: Decodable {
let urlRequest = try URLRequest(request: request, encoder: encoder, authenticator: &authenticator)

Expand All @@ -81,11 +87,14 @@ public actor AppStoreConnectClient {
// MARK: - Pagination

/// Convenience method for accessing a series of paged resources in a sequence asynchronously.
/// - Parameter request: The initial request of the sequence.
///
/// - Parameters:
/// - request: The initial request of the sequence.
/// - retry: Retry strategy.
/// - Returns: A ``PagedResponses`` sequence which will provide with each page's response asynchronously.
public nonisolated func pages<Response>(
_ request: Request<Response>,
retry: RetryBehavior = .never
retry: RetryStrategy = .never
) -> PagedResponses<Response> {
PagedResponses(request: request, client: self, retry: retry)
}
Expand All @@ -95,12 +104,13 @@ public actor AppStoreConnectClient {
/// - Parameters:
/// - request: A request.
/// - currentPage: The API object representing the current page.
/// - retry: Retry strategy.
/// - Returns: The response from the App Store Connect API.
/// - Throws: An error describing the manner in which the request failed to complete.
public func send<Response>(
_ request: Request<Response>,
pageAfter currentPage: Response,
retry: RetryBehavior = .never
retry: RetryStrategy = .never
) async throws -> Response? where Response: Decodable {
guard let nextPage = pagedDocumentLinks(currentPage)?.next else {
return nil
Expand All @@ -116,6 +126,7 @@ public actor AppStoreConnectClient {
}

/// Performs the given request asynchronously.
///
/// - Parameter object: Some object that may contain a property of the type ``PagedDocumentLinks``.
/// - Returns: The ``PagedDocumentLinks`` instance, if one exists.
private nonisolated func pagedDocumentLinks<Entity>(_ object: Entity) -> PagedDocumentLinks? {
Expand All @@ -127,12 +138,15 @@ public actor AppStoreConnectClient {
// MARK: - Downloads

/// Downloads a resource asynchronously to a temporary location.
/// - Parameter request: A request.
///
/// - Parameters:
/// - request: A request.
/// - retry: Retry strategy.
/// - Returns: URL to the location of the resource on-disk.
/// - Throws: An error describing the manner in which the request failed to complete.
public func download(
_ request: Request<Data>,
retry: RetryBehavior = .never
retry: RetryStrategy = .never
) async throws -> URL {
let urlRequest = try URLRequest(request: request, encoder: encoder, authenticator: &authenticator)

Expand Down
3 changes: 1 addition & 2 deletions Sources/AppStoreConnect/Authentication/JWT.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ public protocol Authenticator: Sendable {
var api: API { get }

/// Returns the token to use for authentication with the App Store Connect or Enterprise Program APIs.
/// - Parameters:
/// - audience: An identifier supplied to the "aud" parameter on the JWT payload.
///
/// - Returns: The token to use for authentication.
/// - Throws: An error if an issue was encountered during token signing.
mutating func token() throws -> String
Expand Down
5 changes: 3 additions & 2 deletions Sources/AppStoreConnect/Networking/Pagination.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public struct PagedResponses<Response: Decodable & Sendable>: AsyncSequence, Asy
let request: Request<Response>
/// A reference to the API client.
let client: AppStoreConnectClient
let retry: RetryBehavior
/// The retry strategy.
let retry: RetryStrategy

/// Creates the sequence.
/// - Parameters:
Expand All @@ -24,7 +25,7 @@ public struct PagedResponses<Response: Decodable & Sendable>: AsyncSequence, Asy
init(
request: Request<Response>,
client: AppStoreConnectClient,
retry: RetryBehavior
retry: RetryStrategy
) {
self.request = request
self.client = client
Expand Down
6 changes: 1 addition & 5 deletions Sources/AppStoreConnect/Networking/Request.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ public struct Request<Response>: Sendable {
///
/// - Parameters:
/// - path: Path to the resource.
/// - baseURL: Base URL of the resource.
/// - method: HTTP method.
/// - query: Query string encoded parameters.
/// - body: Body of the request.
Expand Down Expand Up @@ -159,7 +160,6 @@ public struct Request<Response>: Sendable {
///
/// - Parameters:
/// - path: Path to the resource.
/// - baseURL: Base URL of the resource.
/// - query: Query string encoded parameters.
/// - body: Body of the request.
/// - headers: Request headers.
Expand All @@ -177,7 +177,6 @@ public struct Request<Response>: Sendable {
///
/// - Parameters:
/// - path: Path to the resource.
/// - baseURL: Base URL of the resource.
/// - query: Query string encoded parameters.
/// - body: Body of the request.
/// - headers: Request headers.
Expand All @@ -195,7 +194,6 @@ public struct Request<Response>: Sendable {
///
/// - Parameters:
/// - path: Path to the resource.
/// - baseURL: Base URL of the resource.
/// - query: Query string encoded parameters.
/// - body: Body of the request.
/// - headers: Request headers.
Expand All @@ -213,7 +211,6 @@ public struct Request<Response>: Sendable {
///
/// - Parameters:
/// - path: Path to the resource.
/// - baseURL: Base URL of the resource.
/// - query: Query string encoded parameters.
/// - body: Body of the request.
/// - headers: Request headers.
Expand All @@ -231,7 +228,6 @@ public struct Request<Response>: Sendable {
///
/// - Parameters:
/// - path: Path to the resource.
/// - baseURL: Base URL of the resource.
/// - query: Query string encoded parameters.
/// - headers: Request headers.
/// - Returns: The request.
Expand Down
8 changes: 4 additions & 4 deletions Sources/AppStoreConnect/Networking/Response.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,10 @@ public struct Response<Received: Equatable & Sendable>: Equatable, Sendable {
}
}

/// Decodes the ``data`` into the intended container type.
/// Decodes the `data` into the intended container type.
/// - Parameter decoder: The decoder to decode the data with.
/// - Returns: The decoded data.
/// - Throws: An error if ``data`` is `nil` or cannot be decoded as the expected type.
/// - Throws: An error if `data` is `nil` or cannot be decoded as the expected type.
public func decode<Output>(using decoder: JSONDecoder) throws -> Output where Output: Decodable, Received == Data {
guard let data = data else {
throw ResponseError.dataAssertionFailed
Expand All @@ -103,9 +103,9 @@ public struct Response<Received: Equatable & Sendable>: Equatable, Sendable {
return try decoder.decode(Output.self, from: data)
}

/// Unwraps the stored ``URL`` and returns it.
/// Unwraps the stored `URL` and returns it.
/// - Returns: URL to a downloaded file on-disk.
/// - Throws: An error if ``data`` is nil.
/// - Throws: An error if `data` is nil.
public func decode() throws -> URL where Received == URL {
guard let data = data else {
throw ResponseError.dataAssertionFailed
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import Foundation

public enum RetryBehavior: Sendable {
/// Wraps retry logic used by the client when encountering recoverable errors.
///
/// This enum is intended to be provided as input to the methods on ``AppStoreConnect``. It only
/// knows how to handle errors from the App Store Connect and Enterprise Program APIs.
public enum RetryStrategy: Sendable {
/// The default strategy. Avoids retrying under any circumstnace.
case never
/// The strategy that retries on a fixed interval, in seconds, until a limit is reached.
case fixedInterval(_ seconds: UInt64, limit: Int)
/// The strategy that retries using an exponential backoff approach until a limit is reached.
case exponentialBackoff(_ seconds: UInt64, limit: Int, factor: Int = 2)
case custom(_ waiter: @Sendable () async throws -> Void)
/// The strategy that retries using a user-defined function. No additional waiting is performed.
case custom(_ waiter: @Sendable (any Error) async throws -> Void)

@discardableResult
public func retrying<T>(_ block: @Sendable () async throws -> T) async rethrows -> T {
Expand All @@ -30,7 +38,7 @@ public enum RetryBehavior: Sendable {

try await Task.sleep(nanoseconds: UInt64(duration) * 1_000_000_000)
case .custom(let waiter):
try await waiter()
try await waiter(error)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ final class AppStoreConnectTests: XCTestCase {
try await XCTAssertThrowsError(
await testData.context.client.send(
testData.context.request(),
retry: .custom({
retry: .custom({ _ in
try await context.incrementIterations()
})
)
Expand Down

0 comments on commit 1a445cc

Please sign in to comment.