From a785c00290f817cbde8670efffff494b488fa47b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Olejni=CC=81k?= Date: Sun, 10 Nov 2024 18:39:51 +0100 Subject: [PATCH 01/11] Prepare for App default credentials --- Package.resolved | 48 +++--- Package.swift | 23 +-- .../ACKLocalizationCore/ACKLocalization.swift | 145 +++++++++--------- .../Extensions/TokenExtensions.swift | 10 ++ .../Extensions/TokenProvider+Combine.swift | 17 ++ .../Model/AccessToken.swift | 44 ------ .../Model/Configuration.swift | 4 +- .../Model/ServiceAccount.swift | 26 ---- .../Services/AuthAPIService.swift | 63 -------- 9 files changed, 136 insertions(+), 244 deletions(-) create mode 100644 Sources/ACKLocalizationCore/Extensions/TokenExtensions.swift create mode 100644 Sources/ACKLocalizationCore/Extensions/TokenProvider+Combine.swift delete mode 100644 Sources/ACKLocalizationCore/Model/AccessToken.swift delete mode 100644 Sources/ACKLocalizationCore/Model/ServiceAccount.swift delete mode 100644 Sources/ACKLocalizationCore/Services/AuthAPIService.swift diff --git a/Package.resolved b/Package.resolved index d66f81f..e07ee35 100644 --- a/Package.resolved +++ b/Package.resolved @@ -2,57 +2,57 @@ "object": { "pins": [ { - "package": "BigInt", - "repositoryURL": "https://github.com/attaswift/BigInt", + "package": "GoogleAuth", + "repositoryURL": "https://github.com/olejnjak/google-auth-swift", "state": { "branch": null, - "revision": "0ed110f7555c34ff468e72e1686e59721f2b0da6", - "version": "5.3.0" + "revision": "630ebdd31375b62e71565cfea97cf322ba79a2a6", + "version": "0.1.0" } }, { - "package": "CryptoSwift", - "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git", + "package": "jwt-kit", + "repositoryURL": "https://github.com/vapor/jwt-kit", "state": { "branch": null, - "revision": "af1b58fc569bfde777462349b9f7314b61762be0", - "version": "1.3.2" + "revision": "02a0fa600eee1bdc892013d62fc795fc623a5cc3", + "version": "5.1.0" } }, { - "package": "Auth", - "repositoryURL": "https://github.com/googleapis/google-auth-library-swift.git", + "package": "swift-asn1", + "repositoryURL": "https://github.com/apple/swift-asn1.git", "state": { "branch": null, - "revision": "4b510d91fc74f1415eae6dabc9836b8c3e1f44f6", - "version": "0.5.3" + "revision": "7faebca1ea4f9aaf0cda1cef7c43aecd2311ddf6", + "version": "1.3.0" } }, { - "package": "swift-atomics", - "repositoryURL": "https://github.com/apple/swift-atomics.git", + "package": "swift-certificates", + "repositoryURL": "https://github.com/apple/swift-certificates.git", "state": { "branch": null, - "revision": "919eb1d83e02121cdb434c7bfc1f0c66ef17febe", - "version": "1.0.2" + "revision": "1fbb6ef21f1525ed5faf4c95207b9c11bea27e94", + "version": "1.6.1" } }, { - "package": "swift-collections", - "repositoryURL": "https://github.com/apple/swift-collections.git", + "package": "swift-crypto", + "repositoryURL": "https://github.com/apple/swift-crypto.git", "state": { "branch": null, - "revision": "f504716c27d2e5d4144fa4794b12129301d17729", - "version": "1.0.3" + "revision": "06dc63c6d8da54ee11ceb268cde1fa68161afc96", + "version": "3.9.1" } }, { - "package": "swift-nio", - "repositoryURL": "https://github.com/apple/swift-nio.git", + "package": "swift-log", + "repositoryURL": "https://github.com/apple/swift-log.git", "state": { "branch": null, - "revision": "edfceecba13d68c1c993382806e72f7e96feaa86", - "version": "2.44.0" + "revision": "9cb486020ebf03bfa5b5df985387a14a98744537", + "version": "1.6.1" } } ] diff --git a/Package.swift b/Package.swift index 9ce97e4..3b02de5 100644 --- a/Package.swift +++ b/Package.swift @@ -1,15 +1,13 @@ -// swift-tools-version:5.1 -// The swift-tools-version declares the minimum version of Swift required to build this package. +// swift-tools-version:6.0 import PackageDescription let package = Package( name: "ACKLocalization", platforms: [ - .macOS(.v10_15), + .macOS(.v13), ], products: [ - // Products define the executables and libraries produced by a package, and make them visible to other packages. .library( name: "ACKLocalizationCore", targets: ["ACKLocalizationCore"]), @@ -19,21 +17,26 @@ let package = Package( ], dependencies: [ .package( - url: "https://github.com/googleapis/google-auth-library-swift", - .upToNextMajor(from: "0.5.2") + url: "https://github.com/olejnjak/google-auth-swift", + from: "0.1.0" ), ], targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "ACKLocalizationCore", - dependencies: ["OAuth2"]), + dependencies: [ + .product( + name: "GoogleAuth", + package: "google-auth-swift" + ), + ] + ), .target( name: "ACKLocalization", dependencies: ["ACKLocalizationCore"]), .testTarget( name: "ACKLocalizationCoreTests", dependencies: ["ACKLocalizationCore"]), - ] + ], + swiftLanguageModes: [.v5] ) diff --git a/Sources/ACKLocalizationCore/ACKLocalization.swift b/Sources/ACKLocalizationCore/ACKLocalization.swift index 065ce97..7788ca0 100644 --- a/Sources/ACKLocalizationCore/ACKLocalization.swift +++ b/Sources/ACKLocalizationCore/ACKLocalization.swift @@ -1,21 +1,12 @@ -// -// ACKLocalization.swift -// -// -// Created by Jakub Olejník on 11/12/2019. -// - import Combine import Foundation +import GoogleAuth /// Type used for representation of result map values that will be written to destination file public typealias MappedValues = [String: [LocRow]] /// Class containing all `ACKLocalization` logic public final class ACKLocalization { - /// Auth API used to fetch access token for spreadsheet API - private let authAPI: AuthAPIServicing - /// Spreadsheet API used to fetch spreadsheet content private let sheetsAPI: SheetsAPIServicing @@ -23,8 +14,7 @@ public final class ACKLocalization { // MARK: - Initializers - public init(authAPI: AuthAPIServicing = AuthAPIService(), sheetsAPI: SheetsAPIServicing = SheetsAPIService()) { - self.authAPI = authAPI + public init(sheetsAPI: SheetsAPIServicing = SheetsAPIService()) { self.sheetsAPI = sheetsAPI } @@ -60,10 +50,14 @@ public final class ACKLocalization { /// Fetches content of given sheet from spreadsheet using given `serviceAccount` /// /// If not `spreadsheetTabName` is provided, the first in the spreadsheet is used - public func fetchSheetValues(_ spreadsheetTabName: String?, spreadsheetId: String, serviceAccount: Data) -> AnyPublisher { + public func fetchSheetValues( + _ spreadsheetTabName: String?, + spreadsheetId: String, + serviceAccountPath: String? + ) -> AnyPublisher { let sheetsAPI = self.sheetsAPI - - return authAPI.fetchAccessToken(serviceAccount: serviceAccount) + + return fetchGoogleAccessToken(serviceAccountPath: serviceAccountPath) .handleEvents(receiveOutput: { sheetsAPI.credentials = $0 }) .map { _ in } .flatMap { sheetsAPI.fetchSpreadsheet(spreadsheetId) } @@ -71,7 +65,7 @@ public final class ACKLocalization { .mapError(LocalizationError.init) .eraseToAnyPublisher() } - + public func fetchSheetValues(_ spreadsheetTabName: String?, spreadsheetId: String, apiKey: APIKey) -> AnyPublisher { let sheetsAPI = self.sheetsAPI sheetsAPI.credentials = apiKey @@ -323,52 +317,37 @@ public final class ACKLocalization { /// Fetches sheet values from given `config` public func fetchSheetValues(_ config: Configuration) -> AnyPublisher { - do { - if let serviceAccountPath = config.serviceAccount { - let serviceAccount = try loadServiceAccount(from: serviceAccountPath) - return fetchSheetValues( - config.spreadsheetTabName, - spreadsheetId: config.spreadsheetID, - serviceAccount: serviceAccount - ) - } else if let apiKey = config.apiKey { - return fetchSheetValues( - config.spreadsheetTabName, - spreadsheetId: config.spreadsheetID, - apiKey: apiKey - ) - } else if let serviceAccountPath = ProcessInfo.processInfo.environment[Constants.serviceAccountPath] { - let serviceAccount = try loadServiceAccount(from: serviceAccountPath) - return fetchSheetValues( - config.spreadsheetTabName, - spreadsheetId: config.spreadsheetID, - serviceAccount: serviceAccount - ) - } else if let apiKey = ProcessInfo.processInfo.environment[Constants.apiKey] { - let apiKey = APIKey(value: apiKey) - return fetchSheetValues( - config.spreadsheetTabName, - spreadsheetId: config.spreadsheetID, - apiKey: apiKey - ) - } else { - let errorMessage = """ - Unable to load API key or service account path. Please check if: - - - `apiKey` or `serviceAccount` attribute is provided in `localization.json` file - or - - `\(Constants.apiKey)` or `\(Constants.serviceAccountPath)` environment variable is set - """ - - throw LocalizationError(message: errorMessage) - } - } catch { - switch error { - case let localizationError as LocalizationError: - return Fail(error: localizationError).eraseToAnyPublisher() - default: - return Fail(error: LocalizationError(message: error.localizedDescription)).eraseToAnyPublisher() - } + if let serviceAccountPath = config.serviceAccount { + return fetchSheetValues( + config.spreadsheetTabName, + spreadsheetId: config.spreadsheetID, + serviceAccountPath: serviceAccountPath + ) + } else if let apiKey = config.apiKey { + return fetchSheetValues( + config.spreadsheetTabName, + spreadsheetId: config.spreadsheetID, + apiKey: apiKey + ) + } else if let serviceAccountPath = ProcessInfo.processInfo.environment[Constants.serviceAccountPath] { + return fetchSheetValues( + config.spreadsheetTabName, + spreadsheetId: config.spreadsheetID, + serviceAccountPath: serviceAccountPath + ) + } else if let apiKey = ProcessInfo.processInfo.environment[Constants.apiKey] { + let apiKey = APIKey(value: apiKey) + return fetchSheetValues( + config.spreadsheetTabName, + spreadsheetId: config.spreadsheetID, + apiKey: apiKey + ) + } else { + return fetchSheetValues( + config.spreadsheetTabName, + spreadsheetId: config.spreadsheetID, + serviceAccountPath: nil + ) } } @@ -396,19 +375,6 @@ public final class ACKLocalization { } } - /// Loads service account from given `config` - private func loadServiceAccount(from path: String) throws -> Data { - guard let serviceAccountData = FileManager.default.contents(atPath: path) else { - throw LocalizationError(message: "Unable to load service account at " + path) - } - - guard !serviceAccountData.isEmpty else { - throw LocalizationError(message: "Invalid service account data") - } - - return serviceAccountData - } - /// Actually writes given `rows` to given `file` private func writeRows(_ rows: [LocRow], to file: String) throws { guard rows.count > 0 else { return } @@ -457,6 +423,35 @@ public final class ACKLocalization { exit(1) } } + + private func fetchGoogleAccessToken(serviceAccountPath: String?) -> AnyPublisher { + Future { promise in + Task { + do { + let tokenProvider: TokenProvider + let scopes = ["https://www.googleapis.com/auth/spreadsheets.readonly"] + + if let serviceAccountPath { + tokenProvider = try await ServiceAccountTokenProvider( + serviceAccountPath: serviceAccountPath, + scopes: scopes + ) + } else if let tp = await DefaultCredentialsTokenProvider(scopes: scopes) { + tokenProvider = tp + } else { + throw RequestError(message: "Unable to instantiate token provider") + } + + let token = try await tokenProvider.token() + promise(.success(token)) + } catch let error as TokenProviderError { + promise(.failure(RequestError(underlyingError: error))) + } catch let error as RequestError { + promise(.failure(error)) + } + } + }.eraseToAnyPublisher() + } } extension String { diff --git a/Sources/ACKLocalizationCore/Extensions/TokenExtensions.swift b/Sources/ACKLocalizationCore/Extensions/TokenExtensions.swift new file mode 100644 index 0000000..ca678e2 --- /dev/null +++ b/Sources/ACKLocalizationCore/Extensions/TokenExtensions.swift @@ -0,0 +1,10 @@ +import Foundation +import GoogleAuth + +extension Token: CredentialsType { + /// Adds `Authorization` header to given `request` + public func addToRequest(_ request: inout URLRequest) { + request.addValue(tokenType + " " + accessToken, forHTTPHeaderField: "Authorization") + } +} + diff --git a/Sources/ACKLocalizationCore/Extensions/TokenProvider+Combine.swift b/Sources/ACKLocalizationCore/Extensions/TokenProvider+Combine.swift new file mode 100644 index 0000000..519672f --- /dev/null +++ b/Sources/ACKLocalizationCore/Extensions/TokenProvider+Combine.swift @@ -0,0 +1,17 @@ +import Combine +import GoogleAuth + +extension TokenProvider { + func tokenPublisher() -> Future { + .init { promise in + Task { + do { + let token = try await token() + promise(.success(token)) + } catch let error as TokenProviderError { + promise(.failure(error)) + } + } + } + } +} diff --git a/Sources/ACKLocalizationCore/Model/AccessToken.swift b/Sources/ACKLocalizationCore/Model/AccessToken.swift deleted file mode 100644 index d17c037..0000000 --- a/Sources/ACKLocalizationCore/Model/AccessToken.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// AccessToken.swift -// -// -// Created by Jakub Olejník on 11/12/2019. -// - -import Foundation - -/// Struct holding response of auth token request -public struct AccessToken: Decodable { - enum CodingKeys: String, CodingKey { - case accessToken = "access_token" - case expiration = "expires_in" - case type = "token_type" - } - - public let accessToken: String - public let expiration: TimeInterval - public let type: String - - /// Value that can be used in HTTP request header - private var headerValue: String { type + " " + accessToken } -} - -internal struct AccessTokenRequest: Encodable { - enum CodingKeys: String, CodingKey { - case assertion - case grantType = "grant_type" - } - - /// Previously generated JWT token - let assertion: String - - /// Requested grant type - let grantType = "urn:ietf:params:oauth:grant-type:jwt-bearer" -} - -extension AccessToken: CredentialsType { - /// Adds `Authorization` header to given `request` - public func addToRequest(_ request: inout URLRequest) { - request.addValue(headerValue, forHTTPHeaderField: "Authorization") - } -} diff --git a/Sources/ACKLocalizationCore/Model/Configuration.swift b/Sources/ACKLocalizationCore/Model/Configuration.swift index 919b9f7..9ba479d 100644 --- a/Sources/ACKLocalizationCore/Model/Configuration.swift +++ b/Sources/ACKLocalizationCore/Model/Configuration.swift @@ -66,7 +66,7 @@ public typealias Configuration = ConfigurationV2 public struct ConfigurationV2: Decodable { /// API key that will be used to comunicate with Google Sheets API /// - /// Either `apiKey` or `serviceAccount` must be provided, if both are provided, then `serviceAccount` will be used + /// If `apiKey` nor `serviceAccount` is provided [Application default credentials](https://cloud.google.com/docs/authentication/application-default-credentials#personal) would be used public let apiKey: APIKey? /// Path to destination directory where generated strings files should be saved @@ -80,7 +80,7 @@ public struct ConfigurationV2: Decodable { /// Path to service account file that will be used to access spreadsheet /// - /// Either `apiKey` or `serviceAccount` must be provided, if both are provided, then `serviceAccount` will be used + /// If `apiKey` nor `serviceAccount` is provided [Application default credentials](https://cloud.google.com/docs/authentication/application-default-credentials#personal) would be used public let serviceAccount: String? /// Identifier of spreadsheet that should be downloaded diff --git a/Sources/ACKLocalizationCore/Model/ServiceAccount.swift b/Sources/ACKLocalizationCore/Model/ServiceAccount.swift deleted file mode 100644 index 0542d08..0000000 --- a/Sources/ACKLocalizationCore/Model/ServiceAccount.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// ServiceAccount.swift -// -// -// Created by Jakub Olejník on 11/12/2019. -// - -import Foundation - -/// Struct holding necessary information about service account which should access spreadsheet -public struct ServiceAccount: Decodable { - enum CodingKeys: String, CodingKey { - case clientEmail = "client_email" - case privateKey = "private_key" - case tokenURL = "token_uri" - } - - /// Email associated with the service account - let clientEmail: String - - /// Private key used to generate JWT token - let privateKey: String - - /// URL for fetching OAuth token - let tokenURL: URL -} diff --git a/Sources/ACKLocalizationCore/Services/AuthAPIService.swift b/Sources/ACKLocalizationCore/Services/AuthAPIService.swift deleted file mode 100644 index 02b6b9f..0000000 --- a/Sources/ACKLocalizationCore/Services/AuthAPIService.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// AuthAPIService.swift -// -// -// Created by Jakub Olejník on 11/12/2019. -// - -import Combine -import Foundation -import OAuth2 - -/// Protocol wrapping a service that fetches an access token from further communication -public protocol AuthAPIServicing { - /// Fetch access token for given `serviceAccount` - func fetchAccessToken(serviceAccount: Data) -> AnyPublisher -} - -/// Service that fetches an access token from further communication -public struct AuthAPIService: AuthAPIServicing { - private let session: URLSession - - // MARK: - Initializers - - public init(session: URLSession = .shared) { - self.session = session - } - - // MARK: - API calls - - /// Fetch access token for given `serviceAccount` - public func fetchAccessToken(serviceAccount: Data) -> AnyPublisher { - Deferred { - Future { promise in - guard let tokenProvider = ServiceAccountTokenProvider( - credentialsData: serviceAccount, - scopes: ["https://www.googleapis.com/auth/spreadsheets.readonly"] - ) else { - promise(.failure(.init(message: "Creating provider failed"))) - return - } - - do { - try tokenProvider.withToken { token, error in - if let token = token, let accessToken = token.AccessToken { - let token = AccessToken( - accessToken: accessToken, - expiration: TimeInterval(token.ExpiresIn ?? 0), - type: token.TokenType ?? "" - ) - promise(.success(token)) - } else if let error { - promise(.failure(.init(underlyingError: error, message: "Retrieving access token failed"))) - } else { - promise(.failure(.init(message: "Access token was not provided"))) - } - } - } catch { - promise(.failure(.init(underlyingError: error, message: "Retrieving access token failed"))) - } - } - }.eraseToAnyPublisher() - } -} From ce1afa41e0e55010266abbe8d707205fc5609d68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Olejni=CC=81k?= Date: Mon, 11 Nov 2024 12:06:58 +0100 Subject: [PATCH 02/11] Update GoogleAuth lib --- Package.resolved | 115 +++++++++--------- Package.swift | 4 +- .../Extensions/TokenExtensions.swift | 2 +- 3 files changed, 60 insertions(+), 61 deletions(-) diff --git a/Package.resolved b/Package.resolved index e07ee35..d340402 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,61 +1,60 @@ { - "object": { - "pins": [ - { - "package": "GoogleAuth", - "repositoryURL": "https://github.com/olejnjak/google-auth-swift", - "state": { - "branch": null, - "revision": "630ebdd31375b62e71565cfea97cf322ba79a2a6", - "version": "0.1.0" - } - }, - { - "package": "jwt-kit", - "repositoryURL": "https://github.com/vapor/jwt-kit", - "state": { - "branch": null, - "revision": "02a0fa600eee1bdc892013d62fc795fc623a5cc3", - "version": "5.1.0" - } - }, - { - "package": "swift-asn1", - "repositoryURL": "https://github.com/apple/swift-asn1.git", - "state": { - "branch": null, - "revision": "7faebca1ea4f9aaf0cda1cef7c43aecd2311ddf6", - "version": "1.3.0" - } - }, - { - "package": "swift-certificates", - "repositoryURL": "https://github.com/apple/swift-certificates.git", - "state": { - "branch": null, - "revision": "1fbb6ef21f1525ed5faf4c95207b9c11bea27e94", - "version": "1.6.1" - } - }, - { - "package": "swift-crypto", - "repositoryURL": "https://github.com/apple/swift-crypto.git", - "state": { - "branch": null, - "revision": "06dc63c6d8da54ee11ceb268cde1fa68161afc96", - "version": "3.9.1" - } - }, - { - "package": "swift-log", - "repositoryURL": "https://github.com/apple/swift-log.git", - "state": { - "branch": null, - "revision": "9cb486020ebf03bfa5b5df985387a14a98744537", - "version": "1.6.1" - } + "originHash" : "f6844888ec22893d862a6d50b01a09ebc625f4e3501a03169b6156668ab7e9fa", + "pins" : [ + { + "identity" : "google-auth-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/olejnjak/google-auth-swift", + "state" : { + "revision" : "de09f4d1581549b81e4c228060d7a310f453318c", + "version" : "0.1.1" } - ] - }, - "version": 1 + }, + { + "identity" : "jwt-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/jwt-kit", + "state" : { + "revision" : "02a0fa600eee1bdc892013d62fc795fc623a5cc3", + "version" : "5.1.0" + } + }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "7faebca1ea4f9aaf0cda1cef7c43aecd2311ddf6", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-certificates", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-certificates.git", + "state" : { + "revision" : "1fbb6ef21f1525ed5faf4c95207b9c11bea27e94", + "version" : "1.6.1" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "06dc63c6d8da54ee11ceb268cde1fa68161afc96", + "version" : "3.9.1" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "9cb486020ebf03bfa5b5df985387a14a98744537", + "version" : "1.6.1" + } + } + ], + "version" : 3 } diff --git a/Package.swift b/Package.swift index 3b02de5..c57503c 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,7 @@ let package = Package( dependencies: [ .package( url: "https://github.com/olejnjak/google-auth-swift", - from: "0.1.0" + from: "0.1.1" ), ], targets: [ @@ -31,7 +31,7 @@ let package = Package( ), ] ), - .target( + .executableTarget( name: "ACKLocalization", dependencies: ["ACKLocalizationCore"]), .testTarget( diff --git a/Sources/ACKLocalizationCore/Extensions/TokenExtensions.swift b/Sources/ACKLocalizationCore/Extensions/TokenExtensions.swift index ca678e2..a2ec4b7 100644 --- a/Sources/ACKLocalizationCore/Extensions/TokenExtensions.swift +++ b/Sources/ACKLocalizationCore/Extensions/TokenExtensions.swift @@ -4,7 +4,7 @@ import GoogleAuth extension Token: CredentialsType { /// Adds `Authorization` header to given `request` public func addToRequest(_ request: inout URLRequest) { - request.addValue(tokenType + " " + accessToken, forHTTPHeaderField: "Authorization") + add(to: &request) } } From 0ae5a37e86b3870fbe8cb00609de0615c8fdf811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Olejni=CC=81k?= Date: Mon, 11 Nov 2024 17:30:24 +0100 Subject: [PATCH 03/11] Update CI --- .github/workflows/checks.yml | 6 +++--- .github/workflows/deploy.yml | 9 ++++----- .github/workflows/stale.yml | 2 +- .github/workflows/tests.yml | 5 ++--- .github/xcode-version | 1 - 5 files changed, 10 insertions(+), 13 deletions(-) delete mode 100644 .github/xcode-version diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 6996216..5fc6be0 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -8,7 +8,7 @@ jobs: name: Changelog runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.0 + - uses: actions/checkout@v4 - name: Changelog Reminder uses: peterjgrainger/action-changelog-reminder@v1.3.0 with: @@ -17,9 +17,9 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} podspec: name: Podspec - runs-on: macos-13 + runs-on: macos-15 steps: - - uses: actions/checkout@v4.1.0 + - uses: actions/checkout@v4 - name: Install Bundler dependencies run: bundle install - name: Lint podspec diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bf853b3..2bbeaef 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -8,12 +8,11 @@ on: jobs: deploy: name: Deploy - runs-on: macos-13 + runs-on: macos-15 steps: - - uses: actions/checkout@v4.1.0 - - uses: AckeeCZ/load-xcode-version@1.1.0 + - uses: actions/checkout@v4 - name: Build - run: swift build --static-swift-stdlib --configuration release + run: swift build --configuration release - name: Get tag name id: get_version run: echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT @@ -21,7 +20,7 @@ jobs: run: | mv "`swift build --show-bin-path --configuration release`/ACKLocalization" . zip -r "acklocalization-${{ steps.get_version.outputs.VERSION }}.zip" LICENSE ACKLocalization - - uses: xresloader/upload-to-github-release@v1.3.12 + - uses: xresloader/upload-to-github-release@v1.6.0 if: startsWith(github.ref, 'refs/tags/') env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 1db40c4..2f28de4 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -7,7 +7,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v8.0.0 + - uses: actions/stale@v9 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a90dd5e..008d5f9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,9 +5,8 @@ on: [pull_request, push] jobs: tests: name: Run tests - runs-on: macos-13 + runs-on: macos-15 steps: - - uses: actions/checkout@v4.1.0 - - uses: AckeeCZ/load-xcode-version@1.1.0 + - uses: actions/checkout@v4 - name: Run tests run: swift test \ No newline at end of file diff --git a/.github/xcode-version b/.github/xcode-version deleted file mode 100644 index 3d3be3c..0000000 --- a/.github/xcode-version +++ /dev/null @@ -1 +0,0 @@ -15.0 \ No newline at end of file From 9c77039c2fadd6c008fc7322b959711befb9a269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Olejni=CC=81k?= Date: Mon, 11 Nov 2024 17:35:43 +0100 Subject: [PATCH 04/11] Make tests compilable --- .../ACKLocalization+Plurals.swift | 6 ++---- .../ACKLocalizationTests.swift | 2 +- .../Mocks/AuthAPIServiceMock.swift | 16 ---------------- 3 files changed, 3 insertions(+), 21 deletions(-) delete mode 100644 Tests/ACKLocalizationCoreTests/Mocks/AuthAPIServiceMock.swift diff --git a/Tests/ACKLocalizationCoreTests/ACKLocalization+Plurals.swift b/Tests/ACKLocalizationCoreTests/ACKLocalization+Plurals.swift index 66c9e30..d2ec991 100644 --- a/Tests/ACKLocalizationCoreTests/ACKLocalization+Plurals.swift +++ b/Tests/ACKLocalizationCoreTests/ACKLocalization+Plurals.swift @@ -3,17 +3,15 @@ import XCTest final class ACKLocalizationPluralsTests: XCTestCase { private var ackLocalization: ACKLocalization! - private var authAPI: AuthAPIServiceMock! private var sheetsAPI: SheetsAPIServiceMock! // MARK: - Setup override func setUp() { super.setUp() - - authAPI = AuthAPIServiceMock() + sheetsAPI = SheetsAPIServiceMock() - ackLocalization = ACKLocalization(authAPI: authAPI, sheetsAPI: sheetsAPI) + ackLocalization = ACKLocalization(sheetsAPI: sheetsAPI) } // MARK: - Tests diff --git a/Tests/ACKLocalizationCoreTests/ACKLocalizationTests.swift b/Tests/ACKLocalizationCoreTests/ACKLocalizationTests.swift index 7a13e7b..f8cccf8 100644 --- a/Tests/ACKLocalizationCoreTests/ACKLocalizationTests.swift +++ b/Tests/ACKLocalizationCoreTests/ACKLocalizationTests.swift @@ -42,7 +42,7 @@ final class ACKLocalizationTests: XCTestCase { } func testRemovingSuffix() { - var fileName = "Localizable.strings" + let fileName = "Localizable.strings" XCTAssertEqual("Localizable", fileName.removingSuffix(".strings")) } } diff --git a/Tests/ACKLocalizationCoreTests/Mocks/AuthAPIServiceMock.swift b/Tests/ACKLocalizationCoreTests/Mocks/AuthAPIServiceMock.swift deleted file mode 100644 index 6860b40..0000000 --- a/Tests/ACKLocalizationCoreTests/Mocks/AuthAPIServiceMock.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// AuthAPIServiceMock.swift -// -// -// Created by Lukáš Hromadník on 24/08/2020. -// - -import ACKLocalizationCore -import Combine -import Foundation - -final class AuthAPIServiceMock: AuthAPIServicing { - func fetchAccessToken(serviceAccount: Data) -> AnyPublisher { - Empty().eraseToAnyPublisher() - } -} From a5e0b72a4a2c3502d61ced34b4fdd5abef0e029a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Olejni=CC=81k?= Date: Mon, 11 Nov 2024 18:33:19 +0100 Subject: [PATCH 05/11] Fix sorted keys in tests --- .../ACKLocalization+Plurals.swift | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/Tests/ACKLocalizationCoreTests/ACKLocalization+Plurals.swift b/Tests/ACKLocalizationCoreTests/ACKLocalization+Plurals.swift index d2ec991..782ba42 100644 --- a/Tests/ACKLocalizationCoreTests/ACKLocalization+Plurals.swift +++ b/Tests/ACKLocalizationCoreTests/ACKLocalization+Plurals.swift @@ -100,22 +100,22 @@ final class ACKLocalizationPluralsTests: XCTestCase { let rows = [ LocRow(key: "key##{many}", value: "%d many"), ] - - let expectedResult: [String: Encodable] = [ - "NSStringLocalizedFormatKey": "%#@inner@", - "inner": [ + let encoder = JSONEncoder() + encoder.outputFormatting = .sortedKeys + + let expectedResult = ExpectedPluralDict( + NSStringLocalizedFormatKey: "%#@inner@", + inner: [ "NSStringFormatSpecTypeKey": "NSStringPluralRuleType", "NSStringFormatValueTypeKey": "d", "many": "%d many" ] - ] - let expectedResultEncoded = try JSONSerialization - .data(withJSONObject: expectedResult, options: .sortedKeys) + ) + let expectedResultEncoded = try encoder.encode(expectedResult) // When let plurals = try ackLocalization.buildPlurals(from: rows) - let encoder = JSONEncoder() - encoder.outputFormatting = .sortedKeys + let encodedData = try encoder.encode(plurals.first?.value) // Then @@ -127,25 +127,32 @@ final class ACKLocalizationPluralsTests: XCTestCase { let rows = [ LocRow(key: "key##{many}", value: "%s many"), ] - - let expectedResult: [String: Encodable] = [ - "NSStringLocalizedFormatKey": "%1$#@inner@", - "inner": [ + let encoder = JSONEncoder() + encoder.outputFormatting = .sortedKeys + + let expectedResult = ExpectedPluralDict( + NSStringLocalizedFormatKey: "%1$#@inner@", + inner: [ "NSStringFormatSpecTypeKey": "NSStringPluralRuleType", "NSStringFormatValueTypeKey": "d", "many": "%2$@ many" ] - ] - let expectedResultEncoded = try JSONSerialization - .data(withJSONObject: expectedResult, options: .sortedKeys) - + ) + let expectedResultEncoded = try encoder.encode(expectedResult) + // When let plurals = try ackLocalization.buildPlurals(from: rows) - let encoder = JSONEncoder() - encoder.outputFormatting = .sortedKeys let encodedData = try encoder.encode(plurals.first?.value) // Then XCTAssertEqual(encodedData, expectedResultEncoded) } } + +// Well we used JSONSerialization for comparison of dict literal with expected Codable data, +// but since Swift 6 it seems that JSONSerialization has inverse sorting for keys than JSONEncoder, +// so we help ourselves this way +private struct ExpectedPluralDict: Encodable { + let NSStringLocalizedFormatKey: String + let inner: [String: String] +} From efe4491dc71ac9efc921bd4edd7b8fd91607c59e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Olejni=CC=81k?= Date: Mon, 11 Nov 2024 18:37:15 +0100 Subject: [PATCH 06/11] Remove Cocoapods support --- .github/workflows/checks.yml | 11 +--- .github/workflows/deploy.yml | 6 +-- ACKLocalization.podspec | 21 -------- Gemfile | 7 --- Gemfile.lock | 97 ------------------------------------ Tests/LinuxMain.swift | 7 --- 6 files changed, 2 insertions(+), 147 deletions(-) delete mode 100644 ACKLocalization.podspec delete mode 100644 Gemfile delete mode 100644 Gemfile.lock delete mode 100644 Tests/LinuxMain.swift diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 5fc6be0..ff51672 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -14,13 +14,4 @@ jobs: with: changelog_regex: 'CHANGELOG.md' env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - podspec: - name: Podspec - runs-on: macos-15 - steps: - - uses: actions/checkout@v4 - - name: Install Bundler dependencies - run: bundle install - - name: Lint podspec - run: bundle exec pod lib lint --skip-import-validation --skip-tests --allow-warnings \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2bbeaef..6922c19 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -29,8 +29,4 @@ jobs: tags: true draft: false - name: Install gems - run: bundle install - - name: Push podspec - run: bundle exec pod trunk push --skip-import-validation --skip-tests --allow-warnings - env: - COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} + run: bundle install \ No newline at end of file diff --git a/ACKLocalization.podspec b/ACKLocalization.podspec deleted file mode 100644 index 7b0ec19..0000000 --- a/ACKLocalization.podspec +++ /dev/null @@ -1,21 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'ACKLocalization' - s.version = '1.6.1' - s.summary = 'Localize app from Google Spreadsheet' - -# This description is used to generate tags and improve search results. -# * Think: What does it do? Why did you write it? What is the focus? -# * Try to keep it short, snappy and to the point. -# * Write the description between the DESC delimiters below. -# * Finally, don't worry about the indent, CocoaPods strips it! - - s.description = <<-DESC -Localize your app from Google Spreadsheet using swift tool. - DESC - - s.homepage = 'https://github.com/AckeeCZ/ACKLocalization' - s.license = { :type => 'MIT', :file => 'LICENSE' } - s.author = { 'Ackee' => 'info@ackee.cz' } - s.source = { http: "https://github.com/AckeeCZ/ACKLocalization/releases/download/#{s.version}/acklocalization-#{s.version}.zip" } - s.preserve_paths = '*' -end diff --git a/Gemfile b/Gemfile deleted file mode 100644 index f90d114..0000000 --- a/Gemfile +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -source "https://rubygems.org" - -git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } - -gem "cocoapods", "~> 1.9" diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index a136469..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,97 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - CFPropertyList (3.0.5) - rexml - activesupport (6.1.7) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 1.6, < 2) - minitest (>= 5.1) - tzinfo (~> 2.0) - zeitwerk (~> 2.3) - addressable (2.8.1) - public_suffix (>= 2.0.2, < 6.0) - algoliasearch (1.27.5) - httpclient (~> 2.8, >= 2.8.3) - json (>= 1.5.1) - atomos (0.1.3) - claide (1.1.0) - cocoapods (1.11.3) - addressable (~> 2.8) - claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.11.3) - cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 1.4.0, < 2.0) - cocoapods-plugins (>= 1.0.0, < 2.0) - cocoapods-search (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.4.0, < 2.0) - cocoapods-try (>= 1.1.0, < 2.0) - colored2 (~> 3.1) - escape (~> 0.0.4) - fourflusher (>= 2.3.0, < 3.0) - gh_inspector (~> 1.0) - molinillo (~> 0.8.0) - nap (~> 1.0) - ruby-macho (>= 1.0, < 3.0) - xcodeproj (>= 1.21.0, < 2.0) - cocoapods-core (1.11.3) - activesupport (>= 5.0, < 7) - addressable (~> 2.8) - algoliasearch (~> 1.0) - concurrent-ruby (~> 1.1) - fuzzy_match (~> 2.0.4) - nap (~> 1.0) - netrc (~> 0.11) - public_suffix (~> 4.0) - typhoeus (~> 1.0) - cocoapods-deintegrate (1.0.5) - cocoapods-downloader (1.6.3) - cocoapods-plugins (1.0.0) - nap - cocoapods-search (1.0.1) - cocoapods-trunk (1.6.0) - nap (>= 0.8, < 2.0) - netrc (~> 0.11) - cocoapods-try (1.2.0) - colored2 (3.1.2) - concurrent-ruby (1.1.10) - escape (0.0.4) - ethon (0.15.0) - ffi (>= 1.15.0) - ffi (1.15.5) - fourflusher (2.3.1) - fuzzy_match (2.0.4) - gh_inspector (1.1.3) - httpclient (2.8.3) - i18n (1.12.0) - concurrent-ruby (~> 1.0) - json (2.6.2) - minitest (5.16.3) - molinillo (0.8.0) - nanaimo (0.3.0) - nap (1.1.0) - netrc (0.11.0) - public_suffix (4.0.7) - rexml (3.2.5) - ruby-macho (2.5.1) - typhoeus (1.4.0) - ethon (>= 0.9.0) - tzinfo (2.0.5) - concurrent-ruby (~> 1.0) - xcodeproj (1.22.0) - CFPropertyList (>= 2.3.3, < 4.0) - atomos (~> 0.1.3) - claide (>= 1.0.2, < 2.0) - colored2 (~> 3.1) - nanaimo (~> 0.3.0) - rexml (~> 3.2.4) - zeitwerk (2.6.1) - -PLATFORMS - ruby - -DEPENDENCIES - cocoapods (~> 1.9) - -BUNDLED WITH - 2.1.4 diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index 3fac494..0000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,7 +0,0 @@ -import XCTest - -import ACKLocalizationTests - -var tests = [XCTestCaseEntry]() -tests += ACKLocalizationTests.allTests() -XCTMain(tests) From 4ae03228040841decb6514d4bbc86f800f20df0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Olejni=CC=81k?= Date: Mon, 11 Nov 2024 18:47:01 +0100 Subject: [PATCH 07/11] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fde99dd..cb1c6a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ ## main +### Added +- Add support for [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials) ([#42](https://github.com/AckeeCZ/ACKLocalization/pull/42), kudos to @olejnjak) + ## 1.6.1 ### Fixed From 2448ed4fc024c0f83e01c45ea692d4e4711dc492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Olejni=CC=81k?= Date: Mon, 11 Nov 2024 18:49:58 +0100 Subject: [PATCH 08/11] Remove pods from README --- README.md | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 27dfce4..49ab555 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ ![Tests](https://github.com/AckeeCZ/ACKLocalization/workflows/Tests/badge.svg) -[![Version](https://img.shields.io/cocoapods/v/ACKLocalization.svg?style=flat)](http://cocoapods.org/pods/ACKLocalization) -[![License](https://img.shields.io/cocoapods/l/ACKLocalization.svg?style=flat)](http://cocoapods.org/pods/ACKLocalization) # ACKLocalization @@ -8,23 +6,12 @@ Simply localize your app with translations stored in Google Spreadsheet. ## Installation -### Cocoapods +Preferred installation method is using [Mint](https://github.com/yonaskolb/Mint), just add it to your Mintfile -1. Add **ACKLocalization** to your Podfile - -```ruby -pod 'ACKLocalization` ``` - -2. Install pods -```bash -pod install +AckeeCZ/ACKLocalization ``` -### Manually - -Just download binary from Github release - ## Usage You can use ACKLocalization in two ways: @@ -107,10 +94,6 @@ If both are provided then `ACKLOCALIZATION_SERVICE_ACCOUNT_PATH` will be used. Just call the binary, remember that the configuration file has to be in the same directory where you call ACKLocalization. -```bash -Pods/ACKLocalization/Localization -``` - ### Example We love to call **ACKLocalization** from Xcode (we have a separate aggregate target which calls the script) so I'll stick with that with this example. @@ -120,7 +103,6 @@ We love to call **ACKLocalization** from Xcode (we have a separate aggregate tar This is example folder structure of the project ``` |-localization.json -|-Podfile |-Project.xcodeproj |-ServiceAccount.json |-App From 22243e39db77894e57b2991b4cff47f99d89435e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Olejni=CC=81k?= Date: Mon, 11 Nov 2024 21:39:33 +0100 Subject: [PATCH 09/11] Add ADC to README --- README.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 49ab555..2a8c0f4 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,24 @@ AckeeCZ/ACKLocalization ## Usage -You can use ACKLocalization in two ways: -1. safer and recommended [use with Service Account](#use-with-service-account) -2. [use with API key](#use-with-api-key) +You can use ACKLocalization in three ways: +1. [application default credentials](#application-default-credentials) +2. safer and recommended [use with Service Account](#use-with-service-account) +3. [use with API key](#use-with-api-key) + +### Application default credentials (ADC) + +For authorization to Google Spreadsheet we recommend using [Application default credentials](https://cloud.google.com/docs/authentication/application-default-credentials). This way you can safely authorize locally using your personal Google account and use a service account in CI environment. + +To setup application default credentials you need to install gcloud CLI utility and run + +``` +gcloud auth application-default login --billing-project --scopes https://www.googleapis.com/auth/spreadsheets.readonly,https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/sqlservice.login,https://www.googleapis.com/auth/userinfo.email,openid +``` + +You need to add additional scope `https://www.googleapis.com/auth/spreadsheets.readonly` that will allow your ADC to read spreadsheet API on your behalf. + +As Spreadsheet API is [Client-based](https://cloud.google.com/docs/quotas/quota-project#project-client-based) you need to provide a quota project. Spreadsheet API is free so you do not need to worry about that. The project just needs to have Spreadsheet API enabled. ### Use with Service Account From e1db3e5aaf373c7fbb2f4656eb05c71d692b4fb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Olejni=CC=81k?= Date: Mon, 11 Nov 2024 21:41:58 +0100 Subject: [PATCH 10/11] Remove unused TokenProvider extension --- .../Extensions/TokenProvider+Combine.swift | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 Sources/ACKLocalizationCore/Extensions/TokenProvider+Combine.swift diff --git a/Sources/ACKLocalizationCore/Extensions/TokenProvider+Combine.swift b/Sources/ACKLocalizationCore/Extensions/TokenProvider+Combine.swift deleted file mode 100644 index 519672f..0000000 --- a/Sources/ACKLocalizationCore/Extensions/TokenProvider+Combine.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Combine -import GoogleAuth - -extension TokenProvider { - func tokenPublisher() -> Future { - .init { promise in - Task { - do { - let token = try await token() - promise(.success(token)) - } catch let error as TokenProviderError { - promise(.failure(error)) - } - } - } - } -} From 8412b207c44e8bd019440edfdf8612c23206bab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Olejni=CC=81k?= Date: Mon, 11 Nov 2024 22:22:49 +0100 Subject: [PATCH 11/11] Remove extra line --- Sources/ACKLocalizationCore/Extensions/TokenExtensions.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/ACKLocalizationCore/Extensions/TokenExtensions.swift b/Sources/ACKLocalizationCore/Extensions/TokenExtensions.swift index a2ec4b7..e11c8e8 100644 --- a/Sources/ACKLocalizationCore/Extensions/TokenExtensions.swift +++ b/Sources/ACKLocalizationCore/Extensions/TokenExtensions.swift @@ -7,4 +7,3 @@ extension Token: CredentialsType { add(to: &request) } } -