diff --git a/Sources/AppleMapsKit/AppleMapsClient.swift b/Sources/AppleMapsKit/AppleMapsClient.swift index aa79ae5..5ea44f3 100644 --- a/Sources/AppleMapsKit/AppleMapsClient.swift +++ b/Sources/AppleMapsKit/AppleMapsClient.swift @@ -456,11 +456,11 @@ public struct AppleMapsClient: Sendable { ) async throws -> [Eta] { var destinationCoordinates: [(latitude: Double, longitude: Double)] = [] for destination in destinations { - try await destinationCoordinates.append(self.getCoordinate(from: destination)) + try await destinationCoordinates.append(getCoordinate(from: destination)) } - return try await self.eta( - from: self.getCoordinate(from: origin), + return try await eta( + from: getCoordinate(from: origin), to: destinationCoordinates, transportType: transportType, departureDate: departureDate, @@ -520,8 +520,7 @@ public struct AppleMapsClient: Sendable { /// - Throws: Error response object. private func httpGet(url: URL) async throws -> ByteBuffer { var headers = HTTPHeaders() - let accessToken = try await authorizationProvider.validToken().accessToken - headers.add(name: "Authorization", value: "Bearer \(accessToken)") + headers.add(name: "Authorization", value: "Bearer \(try await authorizationProvider.accessToken)") var request = HTTPClientRequest(url: url.absoluteString) request.headers = headers diff --git a/Sources/AppleMapsKit/AppleMapsKitError.swift b/Sources/AppleMapsKit/AppleMapsKitError.swift index 0a791b4..8352ca6 100644 --- a/Sources/AppleMapsKit/AppleMapsKitError.swift +++ b/Sources/AppleMapsKit/AppleMapsKitError.swift @@ -42,6 +42,6 @@ public struct AppleMapsKitError: Error, Sendable { extension AppleMapsKitError: CustomStringConvertible { public var description: String { - "AppleMapsKitError(errorType: \(self.errorType))" + "AppleMapsKitError(errorType: \(errorType))" } } diff --git a/Sources/AppleMapsKit/Authorization/AuthorizationProvider.swift b/Sources/AppleMapsKit/Authorization/AuthorizationProvider.swift index dfa7a13..6e783d9 100644 --- a/Sources/AppleMapsKit/Authorization/AuthorizationProvider.swift +++ b/Sources/AppleMapsKit/Authorization/AuthorizationProvider.swift @@ -28,101 +28,93 @@ actor AuthorizationProvider { self.key = key } - func validToken() async throws -> TokenResponse { - // If we're currently refreshing a token, await the value for our refresh task to make sure we return the refreshed token. - if let refreshTask { - return try await refreshTask.value - } + var accessToken: String { + get async throws { + // If we're currently refreshing a token, await the value for our refresh task to make sure we return the refreshed token. + if let refreshTask { + return try await refreshTask.value.accessToken + } - // If we don't have a current token, we request a new one. - guard let currentToken else { - return try await refreshToken() - } + // If we don't have a current token, we request a new one. + guard let currentToken else { + return try await newToken + } - if currentToken.isValid { - return currentToken - } + if currentToken.isValid { + return currentToken.accessToken + } - // None of the above applies so we'll need to refresh the token. - return try await refreshToken() + // None of the above applies so we'll need to refresh the token. + return try await newToken + } } - private func refreshToken() async throws -> TokenResponse { - if let refreshTask { - return try await refreshTask.value - } + private var newToken: String { + get async throws { + // If we're currently refreshing a token, await the value for our refresh task to make sure we return the refreshed token. + if let refreshTask { + return try await refreshTask.value.accessToken + } - let task = Task { () throws -> TokenResponse in - defer { refreshTask = nil } - let authToken = try await createJWT(teamID: teamID, keyID: keyID, key: key) - let newToken = try await getAccessToken(authToken: authToken) - currentToken = newToken - return newToken - } + // If we don't have a current token, we request a new one. + let task = Task { () throws -> TokenResponse in + defer { refreshTask = nil } + let newToken = try await tokenResponse + currentToken = newToken + return newToken + } - self.refreshTask = task - return try await task.value + refreshTask = task + return try await task.value.accessToken + } } } extension AuthorizationProvider { - /// Makes an HTTP request to exchange Auth token for Access token. - /// - /// - Parameters: - /// - httpClient: The HTTP client to use. - /// - authToken: The authorization token. - /// - /// - Throws: Error response object. - /// - /// - Returns: An access token. - private func getAccessToken(authToken: String) async throws -> TokenResponse { - var headers = HTTPHeaders() - headers.add(name: "Authorization", value: "Bearer \(authToken)") - - var request = HTTPClientRequest(url: "\(apiServer)/v1/token") - request.headers = headers - - let response = try await httpClient.execute(request, timeout: .seconds(30)) - - if response.status == .ok { - return try await JSONDecoder().decode(TokenResponse.self, from: response.body.collect(upTo: 1024 * 1024)) - } else { - throw try await JSONDecoder().decode(ErrorResponse.self, from: response.body.collect(upTo: 1024 * 1024)) + private var tokenResponse: TokenResponse { + get async throws { + var headers = HTTPHeaders() + headers.add(name: "Authorization", value: "Bearer \(try await jwtToken)") + + var request = HTTPClientRequest(url: "\(apiServer)/v1/token") + request.headers = headers + + let response = try await httpClient.execute(request, timeout: .seconds(30)) + + if response.status == .ok { + return try await JSONDecoder().decode(TokenResponse.self, from: response.body.collect(upTo: 1024 * 1024)) + } else { + throw try await JSONDecoder().decode(ErrorResponse.self, from: response.body.collect(upTo: 1024 * 1024)) + } } } - /// Creates a JWT token, which is auth token in this context. - /// - /// - Parameters: - /// - teamID: A 10-character Team ID obtained from your Apple Developer account. - /// - keyID: A 10-character key identifier that provides the ID of the private key that you obtain from your Apple Developer account. - /// - key: A MapKit JS private key. - /// - /// - Returns: A JWT token represented as `String`. - private func createJWT(teamID: String, keyID: String, key: String) async throws -> String { - let keys = try await JWTKeyCollection().add(ecdsa: ES256PrivateKey(pem: key)) - - var header = JWTHeader() - header.alg = "ES256" - header.kid = keyID - header.typ = "JWT" - - struct Payload: JWTPayload { - let iss: IssuerClaim - let iat: IssuedAtClaim - let exp: ExpirationClaim - - func verify(using key: some JWTAlgorithm) throws { - try self.exp.verifyNotExpired() + private var jwtToken: String { + get async throws { + let keys = try await JWTKeyCollection().add(ecdsa: ES256PrivateKey(pem: key)) + + var header = JWTHeader() + header.alg = "ES256" + header.kid = keyID + header.typ = "JWT" + + struct Payload: JWTPayload { + let iss: IssuerClaim + let iat: IssuedAtClaim + let exp: ExpirationClaim + + func verify(using key: some JWTAlgorithm) throws { + try exp.verifyNotExpired() + } } - } - let payload = Payload( - iss: IssuerClaim(value: teamID), - iat: IssuedAtClaim(value: Date()), - exp: ExpirationClaim(value: Date().addingTimeInterval(30 * 60)) - ) + let payload = Payload( + iss: IssuerClaim(value: teamID), + iat: IssuedAtClaim(value: Date()), + exp: ExpirationClaim(value: Date().addingTimeInterval(30 * 60)) + ) - return try await keys.sign(payload, header: header) + return try await keys.sign(payload, header: header) + } } } diff --git a/Sources/AppleMapsKit/DTOs/ErrorResponse.swift b/Sources/AppleMapsKit/DTOs/ErrorResponse.swift index 29e4646..95757ba 100644 --- a/Sources/AppleMapsKit/DTOs/ErrorResponse.swift +++ b/Sources/AppleMapsKit/DTOs/ErrorResponse.swift @@ -9,7 +9,7 @@ public struct ErrorResponse: Error, Codable, Sendable { extension ErrorResponse: CustomStringConvertible { public var description: String { - var result = #"AppleMapsKitError(message: \#(self.message ?? "nil")"# + var result = #"AppleMapsKitError(message: \#(message ?? "nil")"# if let details { result.append(", details: \(details)") diff --git a/Tests/AppleMapsKitTests/AppleMapsKitTests.swift b/Tests/AppleMapsKitTests/AppleMapsKitTests.swift index bbd4cbb..b161f66 100644 --- a/Tests/AppleMapsKitTests/AppleMapsKitTests.swift +++ b/Tests/AppleMapsKitTests/AppleMapsKitTests.swift @@ -30,13 +30,7 @@ struct AppleMapsKitTests { "Geocode", arguments: zip( [(37.78, -122.42), nil], - [ - nil, - MapRegion( - northLatitude: 38, eastLongitude: -122.1, southLatitude: 37.5, - westLongitude: -122.5 - ), - ] + [nil, MapRegion(northLatitude: 38, eastLongitude: -122.1, southLatitude: 37.5, westLongitude: -122.5)] ) ) func geocode(searchLocation: (latitude: Double, longitude: Double)?, searchRegion: MapRegion?) async throws { @@ -69,13 +63,7 @@ struct AppleMapsKitTests { "Search", arguments: zip( [(37.78, -122.42), nil], - [ - nil, - MapRegion( - northLatitude: 38, eastLongitude: -122.1, southLatitude: 37.5, - westLongitude: -122.5 - ), - ] + [nil, MapRegion(northLatitude: 38, eastLongitude: -122.1, southLatitude: 37.5, westLongitude: -122.5)] ) ) func search(searchLocation: (latitude: Double, longitude: Double)?, searchRegion: MapRegion?) async throws { @@ -103,19 +91,19 @@ struct AppleMapsKitTests { } @Test("Search with invalid Result Type") func searchWithInvalidResultType() async throws { - do { - let _ = try await client.search( + await #expect { + try await client.search( for: "eiffel tower", resultTypeFilter: [.pointOfInterest, .physicalFeature, .poi, .address, .query] ) - Issue.record("This call should throw an error") - } catch let error as AppleMapsKitError { - #expect(error.errorType.base == .invalidSearchResultType) + } throws: { error in + guard let error = error as? AppleMapsKitError else { return false } + return error.errorType.base == .invalidSearchResultType } } @Test("Search with Page Token") func searchWithPageToken() async throws { - try await withKnownIssue { + await withKnownIssue { let searchResponse = try await client.search( for: "eiffel tower", resultTypeFilter: [.pointOfInterest, .physicalFeature, .poi, .address], @@ -125,8 +113,6 @@ struct AppleMapsKitTests { ) let results = try #require(searchResponse.results) #expect(!results.isEmpty) - } when: { - credentialsAreInvalid } } @@ -134,13 +120,7 @@ struct AppleMapsKitTests { "Search Auto Complete", arguments: zip( [(37.78, -122.42), nil], - [ - nil, - MapRegion( - northLatitude: 38, eastLongitude: -122.1, southLatitude: 37.5, - westLongitude: -122.5 - ), - ] + [nil, MapRegion(northLatitude: 38, eastLongitude: -122.1, southLatitude: 37.5, westLongitude: -122.5)] ) ) func searchAutoComplete(searchLocation: (latitude: Double, longitude: Double)?, searchRegion: MapRegion?) async throws { @@ -169,13 +149,7 @@ struct AppleMapsKitTests { "Directions", arguments: zip( [(37.7857, -122.4011), nil], - [ - nil, - MapRegion( - northLatitude: 38, eastLongitude: -122.1, southLatitude: 37.5, - westLongitude: -122.5 - ), - ] + [nil, MapRegion(northLatitude: 38, eastLongitude: -122.1, southLatitude: 37.5, westLongitude: -122.5)] ) ) func directions(searchLocation: (latitude: Double, longitude: Double)?, searchRegion: MapRegion?) async throws {