diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 6996216..ff51672 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -8,19 +8,10 @@ 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: changelog_regex: 'CHANGELOG.md' env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - podspec: - name: Podspec - runs-on: macos-13 - steps: - - uses: actions/checkout@v4.1.0 - - 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 bf853b3..6922c19 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 }} @@ -30,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/.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 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/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 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/Package.resolved b/Package.resolved index d66f81f..d340402 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,61 +1,60 @@ { - "object": { - "pins": [ - { - "package": "BigInt", - "repositoryURL": "https://github.com/attaswift/BigInt", - "state": { - "branch": null, - "revision": "0ed110f7555c34ff468e72e1686e59721f2b0da6", - "version": "5.3.0" - } - }, - { - "package": "CryptoSwift", - "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git", - "state": { - "branch": null, - "revision": "af1b58fc569bfde777462349b9f7314b61762be0", - "version": "1.3.2" - } - }, - { - "package": "Auth", - "repositoryURL": "https://github.com/googleapis/google-auth-library-swift.git", - "state": { - "branch": null, - "revision": "4b510d91fc74f1415eae6dabc9836b8c3e1f44f6", - "version": "0.5.3" - } - }, - { - "package": "swift-atomics", - "repositoryURL": "https://github.com/apple/swift-atomics.git", - "state": { - "branch": null, - "revision": "919eb1d83e02121cdb434c7bfc1f0c66ef17febe", - "version": "1.0.2" - } - }, - { - "package": "swift-collections", - "repositoryURL": "https://github.com/apple/swift-collections.git", - "state": { - "branch": null, - "revision": "f504716c27d2e5d4144fa4794b12129301d17729", - "version": "1.0.3" - } - }, - { - "package": "swift-nio", - "repositoryURL": "https://github.com/apple/swift-nio.git", - "state": { - "branch": null, - "revision": "edfceecba13d68c1c993382806e72f7e96feaa86", - "version": "2.44.0" - } + "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 9ce97e4..c57503c 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.1" ), ], 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"]), - .target( + dependencies: [ + .product( + name: "GoogleAuth", + package: "google-auth-swift" + ), + ] + ), + .executableTarget( name: "ACKLocalization", dependencies: ["ACKLocalizationCore"]), .testTarget( name: "ACKLocalizationCoreTests", dependencies: ["ACKLocalizationCore"]), - ] + ], + swiftLanguageModes: [.v5] ) diff --git a/README.md b/README.md index 27dfce4..2a8c0f4 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,28 +6,32 @@ 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 +## Usage + +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) -Just download binary from Github release +### Application default credentials (ADC) -## Usage +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 -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) +``` +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 @@ -107,10 +109,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 +118,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 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..e11c8e8 --- /dev/null +++ b/Sources/ACKLocalizationCore/Extensions/TokenExtensions.swift @@ -0,0 +1,9 @@ +import Foundation +import GoogleAuth + +extension Token: CredentialsType { + /// Adds `Authorization` header to given `request` + public func addToRequest(_ request: inout URLRequest) { + add(to: &request) + } +} 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() - } -} diff --git a/Tests/ACKLocalizationCoreTests/ACKLocalization+Plurals.swift b/Tests/ACKLocalizationCoreTests/ACKLocalization+Plurals.swift index 66c9e30..782ba42 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 @@ -102,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 @@ -129,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] +} 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() - } -} 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)