Skip to content

Commit 8da7d89

Browse files
committed
Lazy OAuth via interceptor in Swift client, drop sync semaphore
Adds BarcodeAuthInterceptor to the AsposeBarcodeCloudClient template: the client installs it on its per-instance apiConfiguration so the URLSession pipeline lazily fetches and injects the bearer token on first use, deduplicating concurrent token fetches. Drops apply() and the blocking authorize() throws (DispatchSemaphore) variant; adds an async authorize() async throws overload for explicit warm-up. Updates the AsposeBarcodeCloudClient and README mustache templates and bumps the Swift submodule pointer to the regenerated SDK.
1 parent 7811746 commit 8da7d89

3 files changed

Lines changed: 141 additions & 57 deletions

File tree

codegen/Templates/swift/AsposeBarcodeCloudClient.mustache

Lines changed: 134 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -98,24 +98,30 @@ public typealias AsposeBarcodeCloudTokenFetcher = @Sendable (
9898
public final class AsposeBarcodeCloudClient: @unchecked Sendable {
9999
public let configuration: AsposeBarcodeCloudConfiguration
100100
public let apiConfiguration: AsposeBarcodeCloudAPIConfiguration
101-
private let tokenFetcher: AsposeBarcodeCloudTokenFetcher
101+
private let authInterceptor: BarcodeAuthInterceptor
102102
103103
public init(
104104
configuration: AsposeBarcodeCloudConfiguration,
105105
tokenFetcher: AsposeBarcodeCloudTokenFetcher? = nil
106106
) {
107107
self.configuration = configuration
108-
self.tokenFetcher = tokenFetcher ?? AsposeBarcodeCloudClient.defaultTokenFetcher
109-
self.apiConfiguration = AsposeBarcodeCloudAPIConfiguration(
108+
let fetcher = tokenFetcher ?? AsposeBarcodeCloudClient.defaultTokenFetcher
109+
110+
let apiConfig = AsposeBarcodeCloudAPIConfiguration(
110111
basePath: configuration.host,
111112
customHeaders: [
112113
"x-aspose-client": configuration.sdkName,
113114
"x-aspose-client-version": configuration.sdkVersion,
114115
]
115116
)
116-
if let accessToken = configuration.accessToken, !accessToken.isEmpty {
117-
apiConfiguration.customHeaders["Authorization"] = "Bearer \(accessToken)"
118-
}
117+
let interceptor = BarcodeAuthInterceptor(
118+
configuration: configuration,
119+
tokenFetcher: fetcher
120+
)
121+
apiConfig.interceptor = interceptor
122+
123+
self.apiConfiguration = apiConfig
124+
self.authInterceptor = interceptor
119125
}
120126

121127
public convenience init(
@@ -142,58 +148,21 @@ public final class AsposeBarcodeCloudClient: @unchecked Sendable {
142148
))
143149
}
144150

145-
public func apply() {
146-
apiConfiguration.basePath = configuration.host
147-
var headers = apiConfiguration.customHeaders
148-
headers["x-aspose-client"] = configuration.sdkName
149-
headers["x-aspose-client-version"] = configuration.sdkVersion
150-
151-
if let accessToken = configuration.accessToken, !accessToken.isEmpty {
152-
headers["Authorization"] = "Bearer \(accessToken)"
153-
} else {
154-
headers.removeValue(forKey: "Authorization")
155-
}
156-
apiConfiguration.customHeaders = headers
157-
}
158-
159151
public func authorize(completion: @escaping @Sendable (Result<String, AsposeBarcodeCloudClientError>) -> Void) {
160-
if let accessToken = configuration.accessToken, !accessToken.isEmpty {
161-
apply()
162-
completion(.success(accessToken))
163-
return
164-
}
165-
166-
tokenFetcher(configuration) { result in
167-
switch result {
168-
case let .success(accessToken):
169-
self.configuration.accessToken = accessToken
170-
self.apply()
171-
completion(.success(accessToken))
172-
case let .failure(error):
173-
completion(.failure(error))
174-
}
175-
}
152+
authInterceptor.ensureToken(completion: completion)
176153
}
177154

178155
@discardableResult
179-
public func authorize() throws -> String {
180-
let semaphore = DispatchSemaphore(value: 0)
181-
let tokenResult = OpenAPIMutex<Result<String, AsposeBarcodeCloudClientError>?>(nil)
182-
183-
authorize { result in
184-
tokenResult.withValue { $0 = result }
185-
semaphore.signal()
186-
}
187-
188-
semaphore.wait()
189-
190-
switch tokenResult.value {
191-
case let .success(accessToken):
192-
return accessToken
193-
case let .failure(error):
194-
throw error
195-
case .none:
196-
throw AsposeBarcodeCloudClientError.invalidTokenResponse
156+
public func authorize() async throws -> String {
157+
try await withCheckedThrowingContinuation { continuation in
158+
authorize { result in
159+
switch result {
160+
case let .success(token):
161+
continuation.resume(returning: token)
162+
case let .failure(error):
163+
continuation.resume(throwing: error)
164+
}
165+
}
197166
}
198167
}
199168

@@ -249,3 +218,114 @@ public final class AsposeBarcodeCloudClient: @unchecked Sendable {
249218
}.resume()
250219
}
251220
}
221+
222+
final class BarcodeAuthInterceptor: OpenAPIInterceptor, @unchecked Sendable {
223+
private let configuration: AsposeBarcodeCloudConfiguration
224+
private let tokenFetcher: AsposeBarcodeCloudTokenFetcher
225+
private let state: OpenAPIMutex<State>
226+
227+
private struct State {
228+
var token: String?
229+
var inFlightFetch: Bool
230+
var pendingWaiters: [@Sendable (Result<String, AsposeBarcodeCloudClientError>) -> Void]
231+
}
232+
233+
init(
234+
configuration: AsposeBarcodeCloudConfiguration,
235+
tokenFetcher: @escaping AsposeBarcodeCloudTokenFetcher
236+
) {
237+
self.configuration = configuration
238+
self.tokenFetcher = tokenFetcher
239+
let initialToken = configuration.accessToken.flatMap { $0.isEmpty ? nil : $0 }
240+
self.state = OpenAPIMutex(State(
241+
token: initialToken,
242+
inFlightFetch: false,
243+
pendingWaiters: []
244+
))
245+
}
246+
247+
func intercept<T>(
248+
urlRequest: URLRequest,
249+
urlSession: URLSessionProtocol,
250+
requestBuilder: RequestBuilder<T>,
251+
completion: @Sendable @escaping (Result<URLRequest, any Error>) -> Void
252+
) {
253+
guard requestBuilder.requiresAuthentication else {
254+
completion(.success(urlRequest))
255+
return
256+
}
257+
258+
ensureToken { result in
259+
switch result {
260+
case let .success(token):
261+
var modified = urlRequest
262+
modified.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
263+
completion(.success(modified))
264+
case let .failure(error):
265+
completion(.failure(error))
266+
}
267+
}
268+
}
269+
270+
func retry<T>(
271+
urlRequest: URLRequest,
272+
urlSession: URLSessionProtocol,
273+
requestBuilder: RequestBuilder<T>,
274+
data: Data?,
275+
response: URLResponse?,
276+
error: any Error,
277+
completion: @Sendable @escaping (OpenAPIInterceptorRetry) -> Void
278+
) {
279+
completion(.dontRetry)
280+
}
281+
282+
func ensureToken(completion: @escaping @Sendable (Result<String, AsposeBarcodeCloudClientError>) -> Void) {
283+
enum Action {
284+
case immediate(Result<String, AsposeBarcodeCloudClientError>)
285+
case wait
286+
case startFetch
287+
}
288+
289+
var action: Action = .wait
290+
state.withValue { state in
291+
if let token = state.token, !token.isEmpty {
292+
action = .immediate(.success(token))
293+
return
294+
}
295+
state.pendingWaiters.append(completion)
296+
if state.inFlightFetch {
297+
action = .wait
298+
} else {
299+
state.inFlightFetch = true
300+
action = .startFetch
301+
}
302+
}
303+
304+
switch action {
305+
case let .immediate(result):
306+
completion(result)
307+
case .wait:
308+
break
309+
case .startFetch:
310+
tokenFetcher(configuration) { [weak self] result in
311+
self?.deliverToken(result)
312+
}
313+
}
314+
}
315+
316+
private func deliverToken(_ result: Result<String, AsposeBarcodeCloudClientError>) {
317+
var waiters: [@Sendable (Result<String, AsposeBarcodeCloudClientError>) -> Void] = []
318+
state.withValue { state in
319+
state.inFlightFetch = false
320+
if case let .success(token) = result {
321+
state.token = token
322+
self.configuration.accessToken = token
323+
}
324+
waiters = state.pendingWaiters
325+
state.pendingWaiters.removeAll()
326+
}
327+
for waiter in waiters {
328+
waiter(result)
329+
}
330+
}
331+
}

codegen/Templates/swift/README.mustache

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,12 @@ let client = AsposeBarcodeCloudClient(
3333
clientId: "your-client-id",
3434
clientSecret: "your-client-secret"
3535
)
36+
```
37+
38+
The OAuth access token is fetched lazily on the first API call. To warm it up at app start, await the explicit method:
3639

37-
try client.authorize()
40+
```swift
41+
_ = try await client.authorize()
3842
```
3943

4044
If you already have an access token, configure the SDK directly:
@@ -43,7 +47,7 @@ If you already have an access token, configure the SDK directly:
4347
let client = AsposeBarcodeCloudClient(accessToken: "your-access-token")
4448
```
4549

46-
`AsposeBarcodeCloudClient` owns a per-instance `apiConfiguration` and sets `Authorization`, `x-aspose-client`, and `x-aspose-client-version` headers on it. Pass `client.apiConfiguration` to each API call so the headers are applied.
50+
`AsposeBarcodeCloudClient` owns a per-instance `apiConfiguration` and sets `x-aspose-client` and `x-aspose-client-version` headers on it. The bundled `BarcodeAuthInterceptor` injects the `Authorization` header on each authenticated request. Pass `client.apiConfiguration` to each API call so the headers are applied.
4751

4852
### Generate
4953

0 commit comments

Comments
 (0)