@@ -6,6 +6,8 @@ import OpenAPIURLSession
66import Dependencies
77
88extension DeveloperAPIClient {
9+ @TaskLocal fileprivate static var cursor : [ URLQueryItem ] = [ ]
10+
911 public init (
1012 auth: DeveloperAPIAuthData
1113 ) {
@@ -22,6 +24,39 @@ extension DeveloperAPIClient {
2224 ]
2325 )
2426 }
27+
28+ /// Perform a paginated request, starting at the page given by `link`.
29+ ///
30+ /// `link` should be a URL that contains a `cursor` query parameter. The
31+ /// cursor will be applied to any requests made inside the closure.
32+ public static func withNextLink< T> (
33+ _ link: String ? ,
34+ isolation: isolated ( any Actor ) ? = #isolation,
35+ perform action: ( ) async throws -> T
36+ ) async throws -> T {
37+ let cursor : [ URLQueryItem ]
38+
39+ if let link {
40+ guard let components = URLComponents ( string: link) ,
41+ let newOffset = components. queryItems? . first ( where: { $0. name == " cursor " } ) ,
42+ let newLimit = components. queryItems? . first ( where: { $0. name == " limit " } )
43+ else { throw Errors . badNextLink ( link) }
44+ // the next value will contain a cursor (offset) *and* a limit even if we didn't
45+ // provide a limit in our initial request. we need to include the limit in subsequent
46+ // requests, otherwise the cursor isn't respected.
47+ cursor = [ newOffset, newLimit]
48+ } else {
49+ cursor = [ ]
50+ }
51+
52+ return try await $cursor. withValue ( cursor) {
53+ try await action ( )
54+ }
55+ }
56+
57+ public enum Errors : Error {
58+ case badNextLink( String )
59+ }
2560}
2661
2762public enum DeveloperAPIAuthData : Sendable {
@@ -126,9 +161,13 @@ public struct DeveloperAPIXcodeAuthMiddleware: ClientMiddleware {
126161
127162 let path = request. path ?? " / "
128163 var components = URLComponents ( string: path) ?? . init( )
129- components. queryItems = ( components. queryItems ?? [ ] ) + [
164+
165+ var items = components. queryItems ?? [ ]
166+ items. upsertQueryItems ( DeveloperAPIClient . cursor + [
130167 URLQueryItem ( name: " teamId " , value: authData. teamID. rawValue)
131- ]
168+ ] )
169+ components. queryItems = items
170+
132171 let query = components. percentEncodedQuery ?? " "
133172
134173 components. query = nil
@@ -214,69 +253,28 @@ public struct DeveloperAPIASCAuthMiddleware: ClientMiddleware {
214253 _ baseURL: URL
215254 ) async throws -> ( HTTPResponse , HTTPBody ? )
216255 ) async throws -> ( HTTPResponse , HTTPBody ? ) {
217- let jwt = try await generator. generate ( )
218256 var request = request
219- request. headerFields [ . authorization] = " Bearer \( jwt) "
220- return try await next ( request, body, baseURL)
221- }
222- }
223-
224- struct LoggingMiddleware : ClientMiddleware {
225- static let regex : NSRegularExpression ? = {
226- guard let pat = ProcessInfo . processInfo. environment [ " XTL_DEV_LOG " ] else { return nil }
227- return try ? NSRegularExpression ( pattern: pat)
228- } ( )
229257
230- func intercept(
231- _ request: HTTPRequest ,
232- body: HTTPBody ? ,
233- baseURL: URL ,
234- operationID: String ,
235- next: ( HTTPRequest , HTTPBody ? , URL ) async throws -> ( HTTPResponse , HTTPBody ? )
236- ) async throws -> ( HTTPResponse , HTTPBody ? ) {
237- var ( response, body) = try await next ( request, body, baseURL)
238-
239- guard Self . regex? . firstMatch (
240- in: operationID,
241- range: NSRange ( operationID. startIndex... , in: operationID)
242- ) != nil
243- else { return ( response, body) }
244-
245- print ( " \n \( operationID) response status -> \( response. status) " )
246-
247- if let unwrapped = body {
248- let data = try await Data ( collecting: unwrapped, upTo: . max)
249- // body may only be consumable once, replace it with the collected data
250- body = . init( data)
258+ let jwt = try await generator. generate ( )
259+ request. headerFields [ . authorization] = " Bearer \( jwt) "
251260
252- let text = String ( decoding: data, as: UTF8 . self)
253- print ( " \( operationID) response body -> \( text) " )
261+ let cursor = DeveloperAPIClient . cursor
262+ if !cursor. isEmpty {
263+ var components = URLComponents ( string: request. path ?? " " ) ?? . init( )
264+ var items = components. queryItems ?? [ ]
265+ items. upsertQueryItems ( cursor)
266+ components. queryItems = items
267+ request. path = components. string
254268 }
255269
256- return ( response , body)
270+ return try await next ( request , body, baseURL )
257271 }
258272}
259273
260- // syntactic sugar to make it nicer to work with `anyOf:` types
261- public protocol OpenAPIExtensibleEnum {
262- associatedtype Value1Payload : RawRepresentable < String > , Codable, Hashable, Sendable, CaseIterable
263- var value1 : Value1Payload ? { get set }
264- var value2 : String ? { get set }
265-
266- init ( value1: Value1Payload ? , value2: String ? )
267- }
268-
269- extension OpenAPIExtensibleEnum {
270- public var rawValue : String {
271- value2!
272- }
273-
274- public init ( _ value: Value1Payload ) {
275- self . init ( value1: value, value2: nil )
274+ extension [ URLQueryItem ] {
275+ fileprivate mutating func upsertQueryItems( _ items: [ URLQueryItem ] ) {
276+ let newNames = Set ( items. map ( \. name) )
277+ removeAll { newNames. contains ( $0. name) }
278+ append ( contentsOf: items)
276279 }
277280}
278-
279- extension Components . Schemas . BundleIdPlatform : OpenAPIExtensibleEnum { }
280- extension Components . Schemas . CapabilityType : OpenAPIExtensibleEnum { }
281- extension Components . Schemas . CertificateType : OpenAPIExtensibleEnum { }
282- extension Components . Schemas . Device . AttributesPayload . DeviceClassPayload : OpenAPIExtensibleEnum { }
0 commit comments