From 1a445cc25f5665e9ab8b98447f232016445f5e70 Mon Sep 17 00:00:00 2001 From: Aaron Sky Date: Thu, 29 Aug 2024 08:05:48 -0400 Subject: [PATCH] renamed RetryBehavior to RetryStrategy. update docs. --- Examples/Utilities/EnvAuthenticator.swift | 1 + README.md | 5 ++- .../AppStoreAPI.docc/AppStoreAPI.md | 2 +- .../Articles/UploadingFiles.md | 4 +-- .../Extensions/AppStoreConnectClient.md | 2 +- Sources/AppStoreAPI/UploadOperations.swift | 3 +- .../AppStoreConnect.docc/AppStoreConnect.md | 2 +- .../Extensions/AppStoreConnectClient.md | 12 +++---- .../AppStoreConnectClient.swift | 32 +++++++++++++------ .../AppStoreConnect/Authentication/JWT.swift | 3 +- .../Networking/Pagination.swift | 5 +-- .../AppStoreConnect/Networking/Request.swift | 6 +--- .../AppStoreConnect/Networking/Response.swift | 8 ++--- ...etryBehavior.swift => RetryStrategy.swift} | 14 ++++++-- .../AppStoreConnectClientTests.swift | 2 +- 15 files changed, 62 insertions(+), 39 deletions(-) rename Sources/AppStoreConnect/Networking/{RetryBehavior.swift => RetryStrategy.swift} (62%) diff --git a/Examples/Utilities/EnvAuthenticator.swift b/Examples/Utilities/EnvAuthenticator.swift index 50738cd0..e7701e31 100644 --- a/Examples/Utilities/EnvAuthenticator.swift +++ b/Examples/Utilities/EnvAuthenticator.swift @@ -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. diff --git a/README.md b/README.md index 37597f3b..3106ebd6 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 diff --git a/Sources/AppStoreAPI/AppStoreAPI.docc/AppStoreAPI.md b/Sources/AppStoreAPI/AppStoreAPI.docc/AppStoreAPI.md index 67a569ae..c997df6a 100644 --- a/Sources/AppStoreAPI/AppStoreAPI.docc/AppStoreAPI.md +++ b/Sources/AppStoreAPI/AppStoreAPI.docc/AppStoreAPI.md @@ -13,7 +13,7 @@ The entire [publicly documented API surface](https://developer.apple.com/documen ### Uploading Files - -- ``AppStoreConnectClient/upload(operation:from:)`` +- ``AppStoreConnect/AppStoreConnectClient/upload(operation:from:retry:)`` - ``UploadOperation`` - ``HTTPHeader`` - ``AppMediaAssetState`` diff --git a/Sources/AppStoreAPI/AppStoreAPI.docc/Articles/UploadingFiles.md b/Sources/AppStoreAPI/AppStoreAPI.docc/Articles/UploadingFiles.md index 40f344ae..03e861c8 100644 --- a/Sources/AppStoreAPI/AppStoreAPI.docc/Articles/UploadingFiles.md +++ b/Sources/AppStoreAPI/AppStoreAPI.docc/Articles/UploadingFiles.md @@ -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 } @@ -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`` diff --git a/Sources/AppStoreAPI/AppStoreAPI.docc/Extensions/AppStoreConnectClient.md b/Sources/AppStoreAPI/AppStoreAPI.docc/Extensions/AppStoreConnectClient.md index 8da93dea..058ff81d 100644 --- a/Sources/AppStoreAPI/AppStoreAPI.docc/Extensions/AppStoreConnectClient.md +++ b/Sources/AppStoreAPI/AppStoreAPI.docc/Extensions/AppStoreConnectClient.md @@ -4,4 +4,4 @@ ### Uploading Files -- ``upload(_:)`` +- ``AppStoreConnect/AppStoreConnectClient/upload(operation:from:retry:)`` diff --git a/Sources/AppStoreAPI/UploadOperations.swift b/Sources/AppStoreAPI/UploadOperations.swift index f88e57cf..3da478c0 100644 --- a/Sources/AppStoreAPI/UploadOperations.swift +++ b/Sources/AppStoreAPI/UploadOperations.swift @@ -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) diff --git a/Sources/AppStoreConnect/AppStoreConnect.docc/AppStoreConnect.md b/Sources/AppStoreConnect/AppStoreConnect.docc/AppStoreConnect.md index 005ef7cb..ad11c6fc 100644 --- a/Sources/AppStoreConnect/AppStoreConnect.docc/AppStoreConnect.md +++ b/Sources/AppStoreConnect/AppStoreConnect.docc/AppStoreConnect.md @@ -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`` diff --git a/Sources/AppStoreConnect/AppStoreConnect.docc/Extensions/AppStoreConnectClient.md b/Sources/AppStoreConnect/AppStoreConnect.docc/Extensions/AppStoreConnectClient.md index 2a7bf966..808d6e40 100644 --- a/Sources/AppStoreConnect/AppStoreConnect.docc/Extensions/AppStoreConnectClient.md +++ b/Sources/AppStoreConnect/AppStoreConnect.docc/Extensions/AppStoreConnectClient.md @@ -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:)`` diff --git a/Sources/AppStoreConnect/AppStoreConnectClient.swift b/Sources/AppStoreConnect/AppStoreConnectClient.swift index 1836a915..ded315be 100644 --- a/Sources/AppStoreConnect/AppStoreConnectClient.swift +++ b/Sources/AppStoreConnect/AppStoreConnectClient.swift @@ -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, - retry: RetryBehavior = .never + retry: RetryStrategy = .never ) async throws { let urlRequest = try URLRequest(request: request, encoder: encoder, authenticator: &authenticator) @@ -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( _ request: Request, - retry: RetryBehavior = .never + retry: RetryStrategy = .never ) async throws -> Response where Response: Decodable { let urlRequest = try URLRequest(request: request, encoder: encoder, authenticator: &authenticator) @@ -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( _ request: Request, - retry: RetryBehavior = .never + retry: RetryStrategy = .never ) -> PagedResponses { PagedResponses(request: request, client: self, retry: retry) } @@ -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( _ request: Request, pageAfter currentPage: Response, - retry: RetryBehavior = .never + retry: RetryStrategy = .never ) async throws -> Response? where Response: Decodable { guard let nextPage = pagedDocumentLinks(currentPage)?.next else { return nil @@ -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(_ object: Entity) -> PagedDocumentLinks? { @@ -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, - retry: RetryBehavior = .never + retry: RetryStrategy = .never ) async throws -> URL { let urlRequest = try URLRequest(request: request, encoder: encoder, authenticator: &authenticator) diff --git a/Sources/AppStoreConnect/Authentication/JWT.swift b/Sources/AppStoreConnect/Authentication/JWT.swift index 562edc7d..f23ac843 100644 --- a/Sources/AppStoreConnect/Authentication/JWT.swift +++ b/Sources/AppStoreConnect/Authentication/JWT.swift @@ -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 diff --git a/Sources/AppStoreConnect/Networking/Pagination.swift b/Sources/AppStoreConnect/Networking/Pagination.swift index 14d96ffa..fd095b8e 100644 --- a/Sources/AppStoreConnect/Networking/Pagination.swift +++ b/Sources/AppStoreConnect/Networking/Pagination.swift @@ -15,7 +15,8 @@ public struct PagedResponses: AsyncSequence, Asy let request: Request /// A reference to the API client. let client: AppStoreConnectClient - let retry: RetryBehavior + /// The retry strategy. + let retry: RetryStrategy /// Creates the sequence. /// - Parameters: @@ -24,7 +25,7 @@ public struct PagedResponses: AsyncSequence, Asy init( request: Request, client: AppStoreConnectClient, - retry: RetryBehavior + retry: RetryStrategy ) { self.request = request self.client = client diff --git a/Sources/AppStoreConnect/Networking/Request.swift b/Sources/AppStoreConnect/Networking/Request.swift index a05185e2..43f84558 100644 --- a/Sources/AppStoreConnect/Networking/Request.swift +++ b/Sources/AppStoreConnect/Networking/Request.swift @@ -117,6 +117,7 @@ public struct Request: 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. @@ -159,7 +160,6 @@ public struct Request: 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. @@ -177,7 +177,6 @@ public struct Request: 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. @@ -195,7 +194,6 @@ public struct Request: 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. @@ -213,7 +211,6 @@ public struct Request: 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. @@ -231,7 +228,6 @@ public struct Request: Sendable { /// /// - Parameters: /// - path: Path to the resource. - /// - baseURL: Base URL of the resource. /// - query: Query string encoded parameters. /// - headers: Request headers. /// - Returns: The request. diff --git a/Sources/AppStoreConnect/Networking/Response.swift b/Sources/AppStoreConnect/Networking/Response.swift index 4cc303f2..2c56c0ac 100644 --- a/Sources/AppStoreConnect/Networking/Response.swift +++ b/Sources/AppStoreConnect/Networking/Response.swift @@ -91,10 +91,10 @@ public struct Response: 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(using decoder: JSONDecoder) throws -> Output where Output: Decodable, Received == Data { guard let data = data else { throw ResponseError.dataAssertionFailed @@ -103,9 +103,9 @@ public struct Response: 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 diff --git a/Sources/AppStoreConnect/Networking/RetryBehavior.swift b/Sources/AppStoreConnect/Networking/RetryStrategy.swift similarity index 62% rename from Sources/AppStoreConnect/Networking/RetryBehavior.swift rename to Sources/AppStoreConnect/Networking/RetryStrategy.swift index 21a50da2..a0aa7daf 100644 --- a/Sources/AppStoreConnect/Networking/RetryBehavior.swift +++ b/Sources/AppStoreConnect/Networking/RetryStrategy.swift @@ -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(_ block: @Sendable () async throws -> T) async rethrows -> T { @@ -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) } } diff --git a/Tests/AppStoreConnectTests/AppStoreConnectClientTests.swift b/Tests/AppStoreConnectTests/AppStoreConnectClientTests.swift index aabc911a..ffd15552 100644 --- a/Tests/AppStoreConnectTests/AppStoreConnectClientTests.swift +++ b/Tests/AppStoreConnectTests/AppStoreConnectClientTests.swift @@ -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() }) )