Last active
August 22, 2025 07:50
-
-
Save ptoffy/61036564e6239817d57a30cbedf19dbc to your computer and use it in GitHub Desktop.
GoogleCloudKit extension example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| app.googleCloud.credentials = try GoogleCloudCredentialsConfiguration(projectId: "...", credentialsFile: "....json") | |
| app.googleCloud.storage.configuration = .default() | |
| app.googleCloud.service.configuration = .init( | |
| scope: [.cloudPlatform], serviceAccount: "default", project: "afa-markets-vapor", region: Environment.GCP.region | |
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import GoogleCloudKit | |
| extension GoogleCloudAPIRequest { | |
| public func withToken<GoogleCloudModel>( | |
| _ closure: @escaping (OAuthAccessToken) async throws -> GoogleCloudModel | |
| ) async throws -> GoogleCloudModel { | |
| guard | |
| let token = currentToken, | |
| let created = tokenCreatedTime, | |
| refreshableToken.isFresh(token: token, created: created) | |
| else { | |
| let newToken = try await refreshableToken.refresh().get() | |
| self.currentToken = newToken | |
| self.tokenCreatedTime = Date() | |
| return try await closure(newToken) | |
| } | |
| return try await closure(token) | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import AsyncHTTPClient | |
| import GoogleCloud | |
| struct GoogleCloudServiceClient: Sendable { | |
| let request: GoogleCloudServiceRequest | |
| init( | |
| credentials: GoogleCloudCredentialsConfiguration, | |
| config: GoogleCloudServiceConfiguration, | |
| httpClient: HTTPClient, | |
| logger: Logger, | |
| eventLoop: any EventLoop | |
| ) throws { | |
| let refreshableToken = OAuthCredentialLoader.getRefreshableToken( | |
| credentials: credentials, | |
| withConfig: config, | |
| andClient: httpClient, | |
| eventLoop: eventLoop | |
| ) | |
| guard | |
| let projectId = ProcessInfo.processInfo.environment["PROJECT_ID"] ?? (refreshableToken as? OAuthServiceAccount)?.credentials | |
| .projectId ?? config.project ?? credentials.project | |
| else { | |
| throw GoogleCloudServiceError.projectIdMissing | |
| } | |
| self.request = .init( | |
| httpClient: httpClient, | |
| oauth: refreshableToken, | |
| project: projectId, | |
| region: config.region, | |
| logger: logger | |
| ) | |
| } | |
| func getService(named name: String) async throws -> ServiceModel { | |
| try await request.send(.GET, to: "https://run.googleapis.com/v2/\(name)") | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @preconcurrency import GoogleCloud | |
| // Here I've added what I need for the service I'm using but this is quite dynamic | |
| struct GoogleCloudServiceConfiguration: GoogleCloudAPIConfiguration, Sendable { | |
| var scope: [any GoogleCloudAPIScope] | |
| let serviceAccount: String | |
| let project: String? | |
| let subscription: String? = nil | |
| let region: String | |
| init(scope: [GoogleCloudServiceScope], serviceAccount: String, project: String, region: String) { | |
| self.scope = scope | |
| self.serviceAccount = serviceAccount | |
| self.project = project | |
| self.region = region | |
| } | |
| } | |
| enum GoogleCloudServiceScope: GoogleCloudAPIScope { | |
| case cloudPlatform | |
| var value: String { | |
| switch self { | |
| // Check out your scopes at https://developers.google.com/identity/protocols/oauth2/scopes | |
| case .cloudPlatform: "https://www.googleapis.com/auth/cloud-platform" | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import GoogleCloudKit | |
| final class GoogleCloud<Service>Request: GoogleCloudAPIRequest, @unchecked Sendable { | |
| let refreshableToken: any OAuthRefreshable | |
| let project: String | |
| let region: String | |
| let httpClient: HTTPClient | |
| let responseDecoder: JSONDecoder | |
| var currentToken: Core.OAuthAccessToken? | |
| var tokenCreatedTime: Date? | |
| let logger: Logger | |
| init( | |
| httpClient: HTTPClient, | |
| oauth: some OAuthRefreshable, | |
| project: String, | |
| region: String, | |
| logger: Logger | |
| ) { | |
| self.refreshableToken = oauth | |
| self.httpClient = httpClient | |
| self.project = project | |
| self.region = region | |
| self.logger = logger | |
| self.responseDecoder = JSONDecoder() | |
| let dateFormatter = DateFormatter() | |
| dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" | |
| self.responseDecoder.dateDecodingStrategy = .formatted(dateFormatter) | |
| } | |
| public func send<Result: GoogleCloudModel>( | |
| _ method: HTTPMethod, | |
| to path: String, | |
| headers: HTTPHeaders = [:], | |
| query: String = "", | |
| body: HTTPClientRequest.Body? = nil | |
| ) async throws -> Result { | |
| try await withToken { token in | |
| var headers = headers | |
| headers.add(name: .authorization, value: "Bearer \(token.accessToken)") | |
| let url = "\(path)?\(query)" | |
| var request = HTTPClientRequest(url: url) | |
| request.headers = headers | |
| request.method = method | |
| if let body { | |
| request.body = body | |
| } | |
| let response = try await self.httpClient.execute(request, timeout: .seconds(60)) | |
| let responseBody = try await response.body.collect(upTo: 1024 * 1024) | |
| guard 200...299 ~= response.status.code else { | |
| self.logger.error("Sent \(method) request to \(url), headers: \(headers))") | |
| throw Abort(.init(statusCode: Int(response.status.code)), reason: "GCP API Error: \(String(buffer: responseBody))") | |
| } | |
| return try self.responseDecoder.decode(Result.self, from: responseBody) | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import GoogleCloudKit | |
| // https://cloud.google.com/run/docs/reference/rest | |
| struct ServiceModel: GoogleCloudModel, Codable {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import GoogleCloud | |
| import Vapor | |
| extension Application.GoogleCloudPlatform { | |
| private struct CloudServiceAPIKey: StorageKey { | |
| typealias Value = GoogleCloudServiceAPI | |
| } | |
| private struct CloudServiceConfigurationKey: StorageKey { | |
| typealias Value = GoogleCloudServiceConfiguration | |
| } | |
| var service: GoogleCloudServiceAPI { | |
| get { | |
| if let existing = self.application.storage[CloudServiceAPIKey.self] { | |
| return existing | |
| } else { | |
| let new = GoogleCloudServiceAPI(application: self.application, eventLoop: self.application.eventLoopGroup.next()) | |
| self.application.storage[CloudServiceAPIKey.self] = new | |
| return new | |
| } | |
| } | |
| nonmutating set { | |
| self.application.storage[CloudServiceAPIKey.self] = newValue | |
| } | |
| } | |
| struct GoogleCloudServiceAPI: Sendable { | |
| let application: Application | |
| let eventLoop: any EventLoop | |
| var client: GoogleCloudServiceClient { | |
| do { | |
| return try GoogleCloudServiceClient( | |
| credentials: application.googleCloud.credentials, | |
| config: configuration, | |
| httpClient: application.http.client.shared, | |
| logger: application.logger, | |
| eventLoop: eventLoop | |
| ) | |
| } catch { | |
| fatalError("\(error.localizedDescription)") | |
| } | |
| } | |
| var configuration: GoogleCloudServiceConfiguration { | |
| get { | |
| guard let configuration = application.storage[CloudServiceConfigurationKey.self] else { | |
| fatalError("Cloud service configuration has not been set. Use app.googleCloud.service.configuration = ...") | |
| } | |
| return configuration | |
| } | |
| nonmutating set { | |
| application.storage[CloudServiceConfigurationKey.self] = newValue | |
| } | |
| } | |
| } | |
| } | |
| extension Request { | |
| private struct GoogleCloudServiceKey: StorageKey { | |
| typealias Value = GoogleCloudServiceClient | |
| } | |
| var gcServiceClient: GoogleCloudServiceClient { | |
| if let existing = application.storage[GoogleCloudServiceKey.self] { | |
| return existing | |
| } else { | |
| let new = Application.GoogleCloudPlatform.GoogleCloudServiceAPI(application: self.application, eventLoop: self.eventLoop) | |
| .client | |
| application.storage[GoogleCloudServiceKey.self] = new | |
| return new | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment