From 7b01ec6364203f420e71599eec13c1e2b760c1a0 Mon Sep 17 00:00:00 2001 From: Jakob Jensen Date: Wed, 3 Jul 2024 22:43:41 +0200 Subject: [PATCH] Become a golang command line tool (#41) --- .devcontainer/Dockerfile | 8 - .devcontainer/devcontainer.json | 26 - .editorconfig | 17 +- .github/dependabot.yml | 4 +- .github/test-mocks/.editorconfig | 2 - .github/test-mocks/curl/curl | 19 - .../assets.swift | 8 - .../assets/dk2.imageset/.gitkeep | 0 .../assets/test.imageset/.gitkeep | 0 .../empty.swift | 5 - .../shell-run-github-workflow-tests/test.yaml | 29 - .github/workflows/release.yaml | 61 +- .github/workflows/tests.yaml | 66 ++- .github/workflows/unit-tests.yaml | 522 ------------------ .gitignore | 101 ++++ .tool-versions | 5 +- .vscode/extensions.json | 8 + .vscode/settings.json | 6 + Makefile | 49 ++ README.md | 24 +- TODO.md | 12 +- build/.gitignore | 1 + cmd/documentation.go | 20 + cmd/root.go | 28 + cmd/translations.go | 114 ++++ docs/generated/lane.md | 19 + docs/generated/lane_completion.md | 24 + docs/generated/lane_completion_bash.md | 43 ++ docs/generated/lane_completion_fish.md | 34 ++ docs/generated/lane_completion_powershell.md | 31 ++ docs/generated/lane_completion_zsh.md | 45 ++ docs/generated/lane_translations.md | 20 + docs/generated/lane_translations_download.md | 39 ++ docs/generated/lane_translations_generate.md | 69 +++ go.mod | 45 ++ go.sum | 165 ++++++ internal/downloader/downloader.go | 88 +++ internal/downloader/downloader_test.go | 120 ++++ internal/downloader/flags.go | 50 ++ internal/downloader/flags_test.go | 131 +++++ internal/downloader/service.go | 34 ++ .../downloader/testdata/empty.json | 0 internal/translations/common_test.go | 22 + internal/translations/flags.go | 53 ++ internal/translations/flags_test.go | 125 +++++ internal/translations/generator.go | 41 ++ internal/translations/generator_test.go | 55 ++ .../translations/language_file.android.go | 46 ++ internal/translations/language_file.go | 103 ++++ internal/translations/language_file.ios.go | 32 ++ .../translations/language_file.ios.support.go | 62 +++ internal/translations/language_file.json.go | 31 ++ internal/translations/language_file_test.go | 183 ++++++ .../translations/testdata/android-da.expected | 0 .../testdata/android-empty.expected | 2 + .../translations/testdata/android-en.expected | 0 .../translations/testdata}/input.csv | 0 .../translations/testdata/ios-da.expected | 0 .../translations/testdata/ios-en.expected | 0 .../testdata/ios-swift-empty.expected | 4 + .../translations/testdata/ios-swift.expected | 0 .../translations/testdata/json-empty.expected | 1 + .../translations/testdata/json-en.expected | 7 + internal/translations/translation.go | 25 + internal/translations/translation_test.go | 43 ++ internal/translations/translations.go | 39 ++ internal/translations/translations_test.go | 33 ++ lane | 87 --- lane.d/completion.bash.dotrc | 9 - lane.d/completion.zsh.dotrc | 13 - .../google-api-docs-sheets-download/help.md | 36 -- .../options.md | 17 - lane.d/google-api-docs-sheets-download/run.sh | 36 -- lane.d/google-api-jwt-generate/help.md | 43 -- lane.d/google-api-jwt-generate/options.md | 14 - lane.d/google-api-jwt-generate/run.sh | 65 --- lane.d/help.md | 35 -- lane.d/mobile-static-resources-images/help.md | 46 -- .../mobile-static-resources-images/options.md | 11 - lane.d/mobile-static-resources-images/run.sh | 41 -- lane.d/mobile-update-translations/extract.py | 21 - lane.d/mobile-update-translations/help.md | 93 ---- lane.d/mobile-update-translations/options.md | 23 - lane.d/mobile-update-translations/run.sh | 128 ----- .../help.md | 45 -- .../options.md | 14 - .../shell-github-action-semver-compare/run.sh | 39 -- .../shell-run-github-workflow-tests/help.md | 73 --- .../options.md | 11 - lane.d/shell-run-github-workflow-tests/run.sh | 114 ---- lanes/test | 3 - main.go | 7 + 92 files changed, 2280 insertions(+), 1743 deletions(-) delete mode 100644 .devcontainer/Dockerfile delete mode 100644 .devcontainer/devcontainer.json delete mode 100644 .github/test-mocks/.editorconfig delete mode 100755 .github/test-mocks/curl/curl delete mode 100644 .github/test-resources/mobile-static-resources-images/assets.swift delete mode 100644 .github/test-resources/mobile-static-resources-images/assets/dk2.imageset/.gitkeep delete mode 100644 .github/test-resources/mobile-static-resources-images/assets/test.imageset/.gitkeep delete mode 100644 .github/test-resources/mobile-static-resources-images/empty.swift delete mode 100644 .github/test-resources/shell-run-github-workflow-tests/test.yaml delete mode 100644 .github/workflows/unit-tests.yaml create mode 100644 .gitignore create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 Makefile create mode 100644 build/.gitignore create mode 100644 cmd/documentation.go create mode 100644 cmd/root.go create mode 100644 cmd/translations.go create mode 100644 docs/generated/lane.md create mode 100644 docs/generated/lane_completion.md create mode 100644 docs/generated/lane_completion_bash.md create mode 100644 docs/generated/lane_completion_fish.md create mode 100644 docs/generated/lane_completion_powershell.md create mode 100644 docs/generated/lane_completion_zsh.md create mode 100644 docs/generated/lane_translations.md create mode 100644 docs/generated/lane_translations_download.md create mode 100644 docs/generated/lane_translations_generate.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/downloader/downloader.go create mode 100644 internal/downloader/downloader_test.go create mode 100644 internal/downloader/flags.go create mode 100644 internal/downloader/flags_test.go create mode 100644 internal/downloader/service.go rename .github/test-resources/mobile-static-resources-images/assets/dk1.imageset/.gitkeep => internal/downloader/testdata/empty.json (100%) create mode 100644 internal/translations/common_test.go create mode 100644 internal/translations/flags.go create mode 100644 internal/translations/flags_test.go create mode 100644 internal/translations/generator.go create mode 100644 internal/translations/generator_test.go create mode 100644 internal/translations/language_file.android.go create mode 100644 internal/translations/language_file.go create mode 100644 internal/translations/language_file.ios.go create mode 100644 internal/translations/language_file.ios.support.go create mode 100644 internal/translations/language_file.json.go create mode 100644 internal/translations/language_file_test.go rename .github/test-resources/mobile-update-translations/expected-android/result.DA.strings => internal/translations/testdata/android-da.expected (100%) create mode 100644 internal/translations/testdata/android-empty.expected rename .github/test-resources/mobile-update-translations/expected-android/result.EN.strings => internal/translations/testdata/android-en.expected (100%) rename {.github/test-resources/mobile-update-translations/configuration => internal/translations/testdata}/input.csv (100%) rename .github/test-resources/mobile-update-translations/expected-ios/result.DA.strings => internal/translations/testdata/ios-da.expected (100%) rename .github/test-resources/mobile-update-translations/expected-ios/result.EN.strings => internal/translations/testdata/ios-en.expected (100%) create mode 100644 internal/translations/testdata/ios-swift-empty.expected rename .github/test-resources/mobile-update-translations/expected-ios/result.swift => internal/translations/testdata/ios-swift.expected (100%) create mode 100644 internal/translations/testdata/json-empty.expected create mode 100644 internal/translations/testdata/json-en.expected create mode 100644 internal/translations/translation.go create mode 100644 internal/translations/translation_test.go create mode 100644 internal/translations/translations.go create mode 100644 internal/translations/translations_test.go delete mode 100755 lane delete mode 100644 lane.d/completion.bash.dotrc delete mode 100644 lane.d/completion.zsh.dotrc delete mode 100644 lane.d/google-api-docs-sheets-download/help.md delete mode 100644 lane.d/google-api-docs-sheets-download/options.md delete mode 100644 lane.d/google-api-docs-sheets-download/run.sh delete mode 100644 lane.d/google-api-jwt-generate/help.md delete mode 100644 lane.d/google-api-jwt-generate/options.md delete mode 100644 lane.d/google-api-jwt-generate/run.sh delete mode 100644 lane.d/help.md delete mode 100644 lane.d/mobile-static-resources-images/help.md delete mode 100644 lane.d/mobile-static-resources-images/options.md delete mode 100755 lane.d/mobile-static-resources-images/run.sh delete mode 100644 lane.d/mobile-update-translations/extract.py delete mode 100644 lane.d/mobile-update-translations/help.md delete mode 100644 lane.d/mobile-update-translations/options.md delete mode 100755 lane.d/mobile-update-translations/run.sh delete mode 100644 lane.d/shell-github-action-semver-compare/help.md delete mode 100644 lane.d/shell-github-action-semver-compare/options.md delete mode 100755 lane.d/shell-github-action-semver-compare/run.sh delete mode 100644 lane.d/shell-run-github-workflow-tests/help.md delete mode 100644 lane.d/shell-run-github-workflow-tests/options.md delete mode 100755 lane.d/shell-run-github-workflow-tests/run.sh delete mode 100644 lanes/test create mode 100644 main.go diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index 3722434..0000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM mcr.microsoft.com/vscode/devcontainers/base:ubuntu - -COPY --from=mikefarah/yq:4.30.2 /usr/bin/yq /usr/local/bin/yq -COPY --from=koalaman/shellcheck:v0.9.0 /bin/shellcheck /usr/local/bin/shellcheck - -USER vscode -RUN echo "source <(lane completion bash)" >> ~/.bashrc -RUN echo "source <(lane completion zsh)" >> ~/.zshrc diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index f8f7709..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "Alpine", - "build": { - "dockerfile": "Dockerfile" - }, - "remoteUser": "vscode", - "remoteEnv": { - "PATH": "${containerEnv:PATH}:/workspaces/lane" - }, - "customizations": { - "vscode": { - "extensions": [ - "EditorConfig.EditorConfig", - "foxundermoon.shell-format", - "redhat.vscode-yaml", - "ms-azuretools.vscode-docker" - ], - "settings": { - "editor.formatOnSave": true, - "yaml.schemas": { - "https://json.schemastore.org/github-workflow.json": "file:///workspaces/lane/.github/**.yaml" - } - } - } - } -} diff --git a/.editorconfig b/.editorconfig index bdc850a..d1e1753 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,28 +3,23 @@ root = true [*] end_of_line = lf charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true [.*] indent_size = 2 indent_style = space -[*.sh,dotrc] +[*.yml] indent_size = 2 indent_style = space -[*.{yml,yaml}] +[*.yaml] indent_size = 2 indent_style = space -[lane] -indent_size = 2 -indent_style = space - -[version] -insert_final_newline = false - [*.md] indent_size = unset indent_style = unset + +[Makefile] +indent_style = tab +indent_size = 4 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ccb7425..4e2ab07 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,7 +9,7 @@ updates: patterns: - "actions/*pages*" - "actions/*-artifact" - - package-ecosystem: docker - directory: /.devcontainer/ + - package-ecosystem: gomod + directory: / schedule: interval: weekly diff --git a/.github/test-mocks/.editorconfig b/.github/test-mocks/.editorconfig deleted file mode 100644 index b429316..0000000 --- a/.github/test-mocks/.editorconfig +++ /dev/null @@ -1,2 +0,0 @@ -[*] -indent_size = 2 diff --git a/.github/test-mocks/curl/curl b/.github/test-mocks/curl/curl deleted file mode 100755 index 89b4b31..0000000 --- a/.github/test-mocks/curl/curl +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh - -[ "$CURL_OVERRIDE" = "fail" ] && exit 22 -[ -f "$CURL_OVERRIDE" ] || { - echo "CURL_OVERRIDE is not a file: $CURL_OVERRIDE" - exit 1 -} - -while getopts ":o:" OPTION; do - case "$OPTION" in - o) - cat "$CURL_OVERRIDE" >"$OPTARG" - exit 0 - ;; - *) ;; - esac -done - -cat "$CURL_OVERRIDE" diff --git a/.github/test-resources/mobile-static-resources-images/assets.swift b/.github/test-resources/mobile-static-resources-images/assets.swift deleted file mode 100644 index 0a4c168..0000000 --- a/.github/test-resources/mobile-static-resources-images/assets.swift +++ /dev/null @@ -1,8 +0,0 @@ -// swiftlint:disable all -import UIKit -struct Images { - static let dk1 = UIImage(named:"dk1")! - static let dk2 = UIImage(named:"dk2")! - static let test = UIImage(named:"test")! -} -// swiftlint:enable all diff --git a/.github/test-resources/mobile-static-resources-images/assets/dk2.imageset/.gitkeep b/.github/test-resources/mobile-static-resources-images/assets/dk2.imageset/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/.github/test-resources/mobile-static-resources-images/assets/test.imageset/.gitkeep b/.github/test-resources/mobile-static-resources-images/assets/test.imageset/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/.github/test-resources/mobile-static-resources-images/empty.swift b/.github/test-resources/mobile-static-resources-images/empty.swift deleted file mode 100644 index d1d7073..0000000 --- a/.github/test-resources/mobile-static-resources-images/empty.swift +++ /dev/null @@ -1,5 +0,0 @@ -// swiftlint:disable all -import UIKit -struct Images { -} -// swiftlint:enable all diff --git a/.github/test-resources/shell-run-github-workflow-tests/test.yaml b/.github/test-resources/shell-run-github-workflow-tests/test.yaml deleted file mode 100644 index 1df2aab..0000000 --- a/.github/test-resources/shell-run-github-workflow-tests/test.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: Tests - -on: - workflow_dispatch: {} - pull_request: {} - push: - branches: - - main - -jobs: - passing-tests: - name: Passing tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Test that passes - run: | - echo 'Test-in-tests' - - failing-tests: - name: Failing tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Test that fails - run: | - exit 2 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index c832d0f..3f4a457 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -24,58 +24,49 @@ jobs: name: Create Artifacts needs: version runs-on: ubuntu-latest + strategy: + matrix: + include: + - {GOOS: linux, GOARCH: amd64} + - {GOOS: linux, GOARCH: arm, GOARM: 6} + - {GOOS: linux, GOARCH: arm64} + - {GOOS: darwin, GOARCH: amd64} + - {GOOS: darwin, GOARCH: arm64} + - {GOOS: freebsd, GOARCH: amd64} steps: - uses: actions/checkout@v4 - - - name: Bundle source files - run: | - mkdir bundle - cp -r lane.d bundle/ - cp LICENSE bundle/LICENSE.txt - - - name: Bundle version-injected-lane - run: | - VERSION='${{ needs.version.outputs.version }}' - printf '#!/bin/sh\nVERSION=''%s''\n' "$VERSION" > bundle/lane - tail +2 lane >> bundle/lane - chmod +x bundle/lane - - - name: Verify version-injected-lane - run: | - sh bundle/lane -v | grep -iv unreleased - [ $? -eq 0 ] || exit 1 - - - name: Package files - run: | - cd bundle - tar -cJf "../lane-${{ needs.version.outputs.version }}.tar.xz" * - - - name: Calculate checksums - run: | - for file in ./lane-*.tar.xz; do - [ -f "$file" ] || continue - sha512sum "$file" > "$file.sha512sum" - done + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - run: make package VERSION='${{ needs.version.outputs.version }}' SUFFIX='-${{ matrix.GOOS }}-${{ matrix.GOARCH }}' + shell: bash + env: + GOOS: ${{ matrix.GOOS }} + GOARCH: ${{ matrix.GOARCH }} + GOARM: ${{ matrix.GOARM }} - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: release-artifacts - path: lane-*.tar.* + name: release-artifacts-${{ matrix.GOOS }}-${{ matrix.GOARCH }} + path: build/lane-*.tar.* + if-no-files-found: error create-release: name: Create Release - needs: [version, create-artifacts] + needs: + - version + - create-artifacts runs-on: ubuntu-latest steps: - name: Download artifacts uses: actions/download-artifact@v4 with: - name: release-artifacts + merge-multiple: true path: artifacts - name: Display structure of files - run: ls -Rl + run: ls -Rlah - name: Attach files if: github.event_name == 'release' diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 0c66ed3..60fe446 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -6,32 +6,43 @@ on: jobs: unit-tests: - uses: ./.github/workflows/unit-tests.yaml - - editorconfig-check: - name: Editorconfig check + name: Unit tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: editorconfig-checker/action-editorconfig-checker@main - - run: editorconfig-checker + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - run: make test + shell: bash + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: coverage-artifacts + path: build/coverage.* + if-no-files-found: error - shellcheck: - name: Shellcheck + documentation-tests: + name: Documentation tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - - name: Make files executable for shellcheck action to check them - run: | - chmod +x lane - find lane.d -name "run.sh" -type f -exec chmod +x {} \; - - - name: Run ShellCheck - uses: ludeeus/action-shellcheck@master + - uses: actions/setup-go@v5 with: - scandir: "./lane.d" - additional_files: "lane" + go-version-file: go.mod + - run: make update-docs + shell: bash + - run: git diff --quiet --exit-code + shell: bash + + editorconfig-check: + name: Editorconfig check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: editorconfig-checker/action-editorconfig-checker@main + - run: editorconfig-checker + shell: bash dependabot-validate: name: Validate dependabot @@ -55,12 +66,13 @@ jobs: action-validator 0.6.0 - name: Lint workflows run: find .github/workflows -type f \( -iname \*.yaml -o -iname \*.yml \) | xargs -I {} action-validator --verbose {} + shell: bash tests-succeeded: name: Tests Succeeded needs: - unit-tests - - shellcheck + - documentation-tests - editorconfig-check - dependabot-validate - workflow-validate @@ -69,19 +81,3 @@ jobs: steps: - name: All clear run: exit 0 - - auto-merge-dependabot: - name: Automerge Dependabot PR - needs: - - tests-succeeded - runs-on: ubuntu-latest - if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request' }} - permissions: - pull-requests: write - contents: write - steps: - - name: Enable auto-merge for Dependabot PRs - run: gh pr merge --auto --squash "$PR_URL" - env: - PR_URL: ${{ github.event.pull_request.html_url }} - GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml deleted file mode 100644 index 085aa15..0000000 --- a/.github/workflows/unit-tests.yaml +++ /dev/null @@ -1,522 +0,0 @@ -name: Unit tests - -on: - workflow_call: {} - -jobs: - lane: - name: Test Lane - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Test missing argument - run: | - rm -rf lanes - mkdir lanes - - set +e - sh ./lane 2> result - [ $? -eq 1 ] || exit 1 - printf 'Warning: lane is deprecated, see help for details\nThe available lanes are:\n' > expected - diff -q expected result || { echo "Unexpected difference:"; diff expected result; exit 1; } - - - name: Test help argument - run: | - rm -rf lanes - mkdir lanes - - set +e - sh ./lane -h > 1 - [ $? -eq 0 ] || exit 1 - - sh ./lane --help > 2 - [ $? -eq 0 ] || exit 1 - - sh ./lane help > 3 - [ $? -eq 0 ] || exit 1 - - diff -q 1 2 || { echo "Unexpected difference:"; diff 1 2; exit 1; } - diff -q 2 3 || { echo "Unexpected difference:"; diff 2 3; exit 1; } - - - name: Test version argument - run: | - rm -rf lanes - - set +e - sh ./lane -v > 1 - [ $? -eq 0 ] || exit 1 - - sh ./lane --version > 2 - [ $? -eq 0 ] || exit 1 - - sh ./lane version > 3 - [ $? -eq 0 ] || exit 1 - - diff -q 1 2 || { echo "Unexpected difference:"; diff 1 2; exit 1; } - diff -q 2 3 || { echo "Unexpected difference:"; diff 2 3; exit 1; } - - - name: Test completion argument - run: | - rm -rf lanes - - set +e - sh ./lane completion bash > /dev/null - [ $? -eq 0 ] || exit 1 - - sh ./lane completion zsh > /dev/null - [ $? -eq 0 ] || exit 1 - - sh ./lane completion > /dev/null - [ $? -eq 1 ] || exit 1 - - - name: Test missing lane - run: | - rm -rf lanes - mkdir lanes - echo 'echo hi' > lanes/say-hi - - set +e - sh ./lane say-bye > /dev/null - [ $? -eq 10 ] || exit 1 - - - name: Test configured lane - run: | - rm -rf lanes - mkdir lanes - echo 'echo hi' > lanes/say-hi - - set +e - sh ./lane say-hi > result - [ $? -eq 0 ] || exit 1 - echo 'hi' > expected - diff -q expected result || { echo "Unexpected difference:"; diff expected result; exit 1; } - - - name: Test builtin lane - run: | - rm -rf lanes - mkdir lane.d/say-hi - echo 'echo hi' > lane.d/say-hi/run.sh - - set +e - sh ./lane say-hi > result - [ $? -eq 0 ] || exit 1 - echo 'hi' > expected - diff -q expected result || { echo "Unexpected difference:"; diff expected result; exit 1; } - - - name: Test builtin lane over configured lane - run: | - rm -rf lanes - mkdir lanes - mkdir lane.d/say - echo 'echo false' > lanes/say - echo 'echo true' > lane.d/say/run.sh - - set +e - sh ./lane say > result - [ $? -eq 0 ] || exit 1 - echo 'true' > expected - diff -q expected result || { echo "Unexpected difference:"; diff expected result; exit 1; } - - - name: Test listing lanes - run: | - rm -rf lanes - mkdir lanes - touch lanes/say - touch lanes/touch - - set +e - sh ./lane 2> result - [ $? -eq 1 ] || exit 1 - printf 'Warning: lane is deprecated, see help for details\nThe available lanes are:\n say\n touch\n' > expected - diff -q expected result || { echo "Unexpected difference:"; diff expected result; exit 1; } - - lane-d-requirements: - name: Test lane.d Requirements - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Validate builtins has an entry point - run: | - find lane.d -mindepth 1 -maxdepth 1 -type d -exec sh -c 'test -f "$1/run.sh" || echo "$1"' -- {} \; > missing - count=$(wc -l < missing | sed 's/ //g') - [ "$count" = 0 ] || { cat missing; exit 1; } - - - name: Validate builtins has options descriptions - run: | - find lane.d -mindepth 1 -maxdepth 1 -type d -exec sh -c 'test -f "$1/options.md" || echo "$1"' -- {} \; > missing - count=$(wc -l < missing | sed 's/ //g') - [ "$count" = 0 ] || { cat missing; exit 1; } - - - name: Validate builtins has help available - run: | - find lane.d -mindepth 1 -maxdepth 1 -type d -exec sh -c 'test -f "$1/help.md" || echo "$1"' -- {} \; > missing - count=$(wc -l < missing | sed 's/ //g') - [ "$count" = 0 ] || { cat missing; exit 1; } - - google-api-docs-sheets-download: - name: Test Google API Docs Sheets Download - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Test missing arguments - run: | - set +e - sh lane.d/google-api-docs-sheets-download/run.sh > /dev/null - [ $? -eq 111 ] || exit 1 - - - name: Test missing output file - run: | - set +e - sh lane.d/google-api-docs-sheets-download/run.sh -t a-token -i an-id > /dev/null - [ $? -eq 111 ] || exit 1 - - - name: Test missing document id - run: | - set +e - sh lane.d/google-api-docs-sheets-download/run.sh -t a-token -o result > /dev/null - [ $? -eq 111 ] || exit 1 - - - name: Test missing token - run: | - set +e - sh lane.d/google-api-docs-sheets-download/run.sh -i an-id -o result > /dev/null - [ $? -eq 111 ] || exit 1 - - - name: Test google api curl error - run: | - export PATH=.github/test-mocks/curl/:$PATH - export CURL_OVERRIDE=fail - - set +e - sh lane.d/google-api-docs-sheets-download/run.sh -t a-token -i an-id -o result - [ $? -eq 22 ] || exit 1 - - - name: Test generated output matches expectation - run: | - echo 'KEY;UPDATE NEEDED;EN;DA;COMMENT' > expected - echo 'SOMETHING;;Something;Noget;' >> expected - - export PATH=.github/test-mocks/curl/:$PATH - export CURL_OVERRIDE=expected - - set +e - sh lane.d/google-api-docs-sheets-download/run.sh -t a-token -i an-id -o result - [ $? -eq 0 ] || exit 1 - diff -q expected result > /dev/null || { echo "Unexpected difference:"; diff expected result; exit 1; } - - - name: Test creating output folder - run: | - echo '' > expected - - export PATH=.github/test-mocks/curl/:$PATH - export CURL_OVERRIDE=expected - - set +e - sh lane.d/google-api-docs-sheets-download/run.sh -t a-token -i an-id -o will/be/created - [ $? -eq 0 ] || exit 1 - [ -d "./will/be" ] || { echo "Output folder was not created"; exit 1; } - - google-api-jwt-generate: - name: Test Google API JWT Generate - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup - run: | - openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 1 -nodes -subj "/C=US/ST=Void/L=Void/O=Void/CN=www.example.com" - openssl pkcs12 -export -passout pass:notasecret -out key.p12 -inkey key.pem -in cert.pem - rm *.pem - - - name: Test missing arguments - run: | - set +e - sh lane.d/google-api-jwt-generate/run.sh > /dev/null - [ $? -eq 111 ] || exit 1 - - - name: Test missing issuer - run: | - set +e - sh lane.d/google-api-jwt-generate/run.sh -s 'scope-a scope-b' -p key.p12 > /dev/null - [ $? -eq 111 ] || exit 1 - - - name: Test missing scopes - run: | - set +e - sh lane.d/google-api-jwt-generate/run.sh -i an-issuer -p key.p12 > /dev/null - [ $? -eq 111 ] || exit 1 - - - name: Test missing p12 file - run: | - set +e - sh lane.d/google-api-jwt-generate/run.sh -i an-issuer -s 'scope-a scope-b' > /dev/null - [ $? -eq 111 ] || exit 1 - - - name: Test non-existing p12 file - run: | - set +e - sh lane.d/google-api-jwt-generate/run.sh -i an-issuer -s 'scope-a scope-b' -p not-a-key.p12 > /dev/null - [ $? -eq 4 ] || exit 1 - - - name: Test google api curl error - run: | - export PATH=.github/test-mocks/curl/:$PATH - export CURL_OVERRIDE=fail - - set +e - sh lane.d/google-api-jwt-generate/run.sh -i an-issuer -s 'scope-a scope-b' -p key.p12 > result - [ $? -eq 22 ] || exit 1 - - - name: Test generating token - run: | - echo 'a-secret-token' > expected - echo '{"access_token":"a-secret-token"}' > override - - export PATH=.github/test-mocks/curl/:$PATH - export CURL_OVERRIDE=override - - set +e - sh lane.d/google-api-jwt-generate/run.sh -i an-issuer -s 'scope-a scope-b' -p key.p12 > result - [ $? -eq 0 ] || exit 1 - diff -q expected result > /dev/null || { echo "Unexpected difference:"; diff expected result; exit 1; } - - mobile-static-resources-images: - name: Test Static Resources Images - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup - run: | - mkdir ./empty-folder - - - name: Test missing arguments - run: | - set +e - sh lane.d/mobile-static-resources-images/run.sh > /dev/null - [ $? -eq 111 ] || exit 1 - - - name: Test missing input file - run: | - set +e - sh lane.d/mobile-static-resources-images/run.sh -o result > /dev/null - [ $? -eq 111 ] || exit 1 - - - name: Test missing output file - run: | - set +e - sh lane.d/mobile-static-resources-images/run.sh -i input > /dev/null - [ $? -eq 111 ] || exit 1 - - - name: Test non-existing input folder - run: | - set +e - sh lane.d/mobile-static-resources-images/run.sh -i ./non-existing-folder -o result > /dev/null - [ $? -eq 3 ] || exit 1 - - - name: Test empty asset folder - run: | - set +e - sh lane.d/mobile-static-resources-images/run.sh -i empty-folder -o result - [ $? -eq 0 ] || exit 1 - diff -q .github/test-resources/mobile-static-resources-images/empty.swift result > /dev/null || { echo "Unexpected difference:"; diff .github/test-resources/mobile-static-resources-images/empty.swift result; exit 1; } - - - name: Test with asset folder - run: | - set +e - sh lane.d/mobile-static-resources-images/run.sh -i .github/test-resources/mobile-static-resources-images/assets -o result - [ $? -eq 0 ] || exit 1 - diff -q .github/test-resources/mobile-static-resources-images/assets.swift result > /dev/null || { echo "Unexpected difference:"; diff .github/test-resources/mobile-static-resources-images/assets.swift result; exit 1; } - - - name: Test creating output folder - run: | - set +e - sh lane.d/mobile-static-resources-images/run.sh -i empty-folder -o will/be/created - [ $? -eq 0 ] || exit 1 - [ -d "./will/be" ] || { echo "Output folder was not created"; exit 1; } - - mobile-update-translations: - name: Test Update Translations - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Test missing argument - run: | - set +e - sh lane.d/mobile-update-translations/run.sh > /dev/null - [ $? -eq 111 ] || exit 1 - - - name: Test missing configuration for missing input - run: | - set +e - sh lane.d/mobile-update-translations/run.sh -t ios -k 1 -m 3 -o result.swift -c "4 result.DA.strings" -c "3 result.EN.strings" - [ $? -eq 111 ] || exit 1 - - - name: Test missing configuration for input not a file - run: | - set +e - sh lane.d/mobile-update-translations/run.sh -t ios -k 1 -m 3 -o result.swift -c "4 result.DA.strings" -c "3 result.EN.strings" -i .github/test-resources/ - [ $? -eq 111 ] || exit 1 - - - name: Test missing configuration for missing main language - run: | - set +e - sh lane.d/mobile-update-translations/run.sh -t ios -k 1 -o result.swift -c "4 result.DA.strings" -c "3 result.EN.strings" -i .github/test-resources/mobile-update-translations/configuration/input.csv - [ $? -eq 111 ] || exit 1 - - - name: Test missing configuration for missing key row - run: | - set +e - sh lane.d/mobile-update-translations/run.sh -t ios -m 3 -o result.swift -c "4 result.DA.strings" -c "3 result.EN.strings" -i .github/test-resources/mobile-update-translations/configuration/input.csv - [ $? -eq 111 ] || exit 1 - - - name: Test missing configuration for missing output - run: | - set +e - sh lane.d/mobile-update-translations/run.sh -t ios -k 1 -m 3 -c "4 result.DA.strings" -c "3 result.EN.strings" -i .github/test-resources/mobile-update-translations/configuration/input.csv - [ $? -eq 111 ] || exit 1 - - - name: Test ios output is created as expected - run: | - set +e - sh lane.d/mobile-update-translations/run.sh -t ios -k 1 -m 3 -o result.swift -c "4 result.DA.strings" -c "3 result.EN.strings" -i .github/test-resources/mobile-update-translations/configuration/input.csv - [ $? -eq 0 ] || exit 1 - diff -q .github/test-resources/mobile-update-translations/expected-ios/result.swift result.swift > /dev/null || { echo "Unexpected difference:"; diff .github/test-resources/mobile-update-translations/expected-ios/result.swift result.swift; exit 1; } - diff -q .github/test-resources/mobile-update-translations/expected-ios/result.EN.strings result.EN.strings > /dev/null || { echo "Unexpected difference:"; diff .github/test-resources/mobile-update-translations/expected-ios/result.EN.strings result.EN.strings; exit 1; } - diff -q .github/test-resources/mobile-update-translations/expected-ios/result.DA.strings result.DA.strings > /dev/null || { echo "Unexpected difference:"; diff .github/test-resources/mobile-update-translations/expected-ios/result.DA.strings result.DA.strings; exit 1; } - - - name: Test android output is created as expected - run: | - set +e - sh lane.d/mobile-update-translations/run.sh -t android -k 1 -c "4 result.DA.strings" -c "3 result.EN.strings" -i .github/test-resources/mobile-update-translations/configuration/input.csv - [ $? -eq 0 ] || exit 1 - diff -q .github/test-resources/mobile-update-translations/expected-android/result.EN.strings result.EN.strings > /dev/null || { echo "Unexpected difference:"; diff .github/test-resources/mobile-update-translations/expected-android/result.EN.strings result.EN.strings; exit 1; } - diff -q .github/test-resources/mobile-update-translations/expected-android/result.DA.strings result.DA.strings > /dev/null || { echo "Unexpected difference:"; diff .github/test-resources/mobile-update-translations/expected-android/result.DA.strings result.DA.strings; exit 1; } - - - name: Test csv escaping - run: | - printf 'key,en\nTHIS,"This is a longer sentence, which includes a comma."\nTHAT,Another string including a | even.' > input - printf '\n\tAnother string including a | even.\n\tThis is a longer sentence, which includes a comma.\n\n' > expected - - set +e - sh lane.d/mobile-update-translations/run.sh -t android -k 1 -c "2 result" -i input - [ $? -eq 0 ] || exit 1 - diff -q result expected > /dev/null || { echo "Unexpected difference:"; diff result expected; exit 1; } - - - name: Test csv escaping on every column - run: | - printf '"key","en"\n"THIS","This is a longer sentence, which includes a comma."\n"THAT","Another string including a | even."' > input - printf '\n\tAnother string including a | even.\n\tThis is a longer sentence, which includes a comma.\n\n' > expected - - set +e - sh lane.d/mobile-update-translations/run.sh -t android -k 1 -c "2 result" -i input - [ $? -eq 0 ] || exit 1 - diff -q result expected > /dev/null || { echo "Unexpected difference:"; diff result expected; exit 1; } - - shell-run-github-workflow-tests: - name: Test Shell Run Github Workflow Tests (always skipped on GitHub) - runs-on: ubuntu-latest - if: ${{ false }} - steps: - - uses: actions/checkout@v4 - - - name: Test non-existing file - run: | - set +e - sh lane.d/shell-run-github-workflow-tests/run.sh -i ./non-existing-file > /dev/null - [ $? -eq 111 ] || exit 1 - - - name: Test running tests - run: | - set +e - sh lane.d/shell-run-github-workflow-tests/run.sh -i .github/test-resources/shell-run-github-workflow-tests/test.yaml - [ $? -eq 1 ] || exit 1 - - - name: Test running unknown test section (has no tests and thus no errors) - run: | - sh lane.d/shell-run-github-workflow-tests/run.sh -i .github/test-resources/shell-run-github-workflow-tests/test.yaml -j unknown-tests - - - name: Test running passing tests - run: | - sh lane.d/shell-run-github-workflow-tests/run.sh -i .github/test-resources/shell-run-github-workflow-tests/test.yaml -j passing-tests - - - name: Test running failing tests - run: | - set +e - sh lane.d/shell-run-github-workflow-tests/run.sh -i .github/test-resources/shell-run-github-workflow-tests/test.yaml -j failing-tests - [ $? -eq 1 ] || exit 1 - - shell-github-action-semver-compare: - name: Test Shell Github Action Semver Compare - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Test missing arguments - run: | - set +e - sh lane.d/shell-github-action-semver-compare/run.sh > /dev/null - [ $? -eq 111 ] || exit 1 - - - name: Test missing main version argument - run: | - set +e - sh lane.d/shell-github-action-semver-compare/run.sh -c "0.0.0" > /dev/null - [ $? -eq 111 ] || exit 1 - - - name: Test missing current version argument - run: | - set +e - sh lane.d/shell-github-action-semver-compare/run.sh -m "0.0.0" > /dev/null - [ $? -eq 111 ] || exit 1 - - - name: Test matching versions - run: | - set +e - sh lane.d/shell-github-action-semver-compare/run.sh -m "0.0.0" -c "0.0.0" > /dev/null - [ $? -eq 10 ] || exit 1 - - - name: Test descending versions - run: | - set +e - sh lane.d/shell-github-action-semver-compare/run.sh -m "1.2.3" -c "0.0.0" > /dev/null - [ $? -eq 20 ] || exit 1 - - - name: Test ascending versions - run: | - set +e - sh lane.d/shell-github-action-semver-compare/run.sh -m "1.2.3" -c "3.2.1" > /dev/null - [ $? -eq 0 ] || exit 1 - - - name: Test quiet mode - run: | - touch expected - - set +e - sh lane.d/shell-github-action-semver-compare/run.sh -m "1.2.3" -c "3.2.1" > result - [ $? -eq 0 ] || exit 1 - diff -q expected result > /dev/null && { echo "There should be output without quiet mode"; exit 1; } - - sh lane.d/shell-github-action-semver-compare/run.sh -m "1.2.3" -c "3.2.1" -q > result - [ $? -eq 0 ] || exit 1 - diff -q expected result > /dev/null || { echo "Unexpected difference:"; diff expected result; exit 1; } - - unit-tests-succeeded: - name: Unit Tests Succeeded - needs: - - lane - - lane-d-requirements - - google-api-docs-sheets-download - - google-api-jwt-generate - - mobile-static-resources-images - - mobile-update-translations - - shell-github-action-semver-compare - - runs-on: ubuntu-latest - steps: - - name: All clear - run: exit 0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f3da0f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,101 @@ +# Created by https://www.toptal.com/developers/gitignore/api/macos,linux,windows,go +# Edit at https://www.toptal.com/developers/gitignore?templates=macos,linux,windows,go + +### Go ### +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/macos,linux,windows,go diff --git a/.tool-versions b/.tool-versions index d705ae2..df4d136 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,4 +1 @@ -yq 4.30.2 -jq 1.6 -shellcheck 0.9.0 -lane 0.2.0 +golang 1.22.4 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..2185087 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "golang.go", + "editorconfig.editorconfig", + "ms-vscode.makefile-tools", + "redhat.vscode-yaml" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6f64d69 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "editor.formatOnSave": true, + "yaml.schemas": { + "https://json.schemastore.org/github-workflow.json": ".github/workflows/*.yaml" + } +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5ebd5fe --- /dev/null +++ b/Makefile @@ -0,0 +1,49 @@ +.default: test +.phony: test + +CI = $(shell env | grep ^CI=) +VERSION = 0.0.0 +SUFFIX = +TOOL_VERSION = $(shell grep '^golang ' .tool-versions | sed 's/golang //') +MOD_VERSION = $(shell grep '^go ' go.mod | sed 's/go //') + +clean: + @find build -type f ! -name .gitignore -exec rm {} + + @find build -type d -mindepth 1 -exec rmdir {} + + +build: clean + go build \ + -trimpath \ + -ldflags "-s -w -X github.com/codereaper/lane/cmd.versionString=$(VERSION)" \ + -o build/bin/ + cp LICENSE build/bin/LICENSE.txt + +update-docs: build + @mkdir -p docs/generated + build/bin/lane documentation -o docs/generated + +package: build + tar -cJvf build/lane-$(VERSION)$(SUFFIX).tar.xz build/bin/ + cd build && sha512sum lane-$(VERSION)$(SUFFIX).tar.xz > lane-$(VERSION)$(SUFFIX).tar.xz.sha512sum + +tidy: clean + go fmt + go mod tidy +ifeq ($(strip $(CI)),) + @git diff --quiet --exit-code || echo 'Warning: Workplace is dirty' +else + @git diff --quiet --exit-code || (echo 'Error: Workplace is dirty'; exit 1) +endif + +unit-tests: + go test -timeout 10s -p 1 -coverprofile=build/coverage.out ./internal/... + go tool cover -html=build/coverage.out -o build/coverage.html + +verify-version: +ifneq ($(TOOL_VERSION),$(MOD_VERSION)) + @echo 'Mismatched go versions' + @exit 1 +endif + @exit 0 + +test: verify-version tidy unit-tests diff --git a/README.md b/README.md index 8c22300..c767f78 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,11 @@ The prefered method of installation is through [asdf](http://asdf-vm.com/). A lane plugin to install has been set up at [asdf-lane](https://github.com/CodeReaper/asdf-lane). +Alternatively this tool can be run directly: +```go +go run github.com/codereaper/lane@1.0.0 +``` + ## Completion You can set up auto completion by adding the following to your dot rc file: @@ -26,21 +31,6 @@ source <(lane completion zsh) source <(lane completion bash) ``` -## Manuals - -[lane](lane.d/help.md) - -### Google APIs - -- [lane google-api-docs-sheets-download](lane.d/google-api-docs-sheets-download/help.md) -- [lane google-api-jwt-generate](lane.d/google-api-jwt-generate/help.md) - -### Mobile - -- [lane mobile-static-resources-images](lane.d/mobile-static-resources-images/help.md) -- [lane mobile-update-translations](lane.d/mobile-update-translations/help.md) - -### Shell +## Documentation -- [lane shell-github-action-semver-compare](lane.d/shell-github-action-semver-compare/help.md) -- [lane shell-run-github-workflow-tests](lane.d/shell-run-github-workflow-tests/help.md) +[Auto-generated documentation](docs/generated/lane.md) is available, but is also included in `lane`. diff --git a/TODO.md b/TODO.md index 8d0c718..5fa763b 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,12 @@ ## TODO +# FIXME: required tasks here too + - Add shields for version and tests passing -- Add autocomplete for: -- - builtin lanes -- Add help for version check and installation +- Update README and generated docs +- Update release flow +- Update asdf installation repo +- Deprecate image generation cmd sherlocked +- Deprecate semver cmd not in use +- Deprecate workflow testing cmd in favor of makefile/3musketeer +- Deprecate google api cmds builtin for new translation cmd diff --git a/build/.gitignore b/build/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/build/.gitignore @@ -0,0 +1 @@ +* diff --git a/cmd/documentation.go b/cmd/documentation.go new file mode 100644 index 0000000..294e993 --- /dev/null +++ b/cmd/documentation.go @@ -0,0 +1,20 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" +) + +func newDocumentationCommand(root *cobra.Command) *cobra.Command { + var output string + var cmd = &cobra.Command{ + Use: "documentation", + Hidden: true, + RunE: func(cmd *cobra.Command, args []string) error { + return doc.GenMarkdownTree(root, output) + }, + } + cmd.Flags().StringVarP(&output, "output", "o", "", "Path to save documentation files (Required)") + cmd.MarkFlagRequired("output") + return cmd +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..eb36bff --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,28 @@ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" +) + +var versionString string +var rootCmd = &cobra.Command{ + Use: "lane", + Short: "Automates common tasks", + Long: `lane is a task automation helper that works well with tools like make.`, +} + +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + rootCmd.Version = versionString + rootCmd.DisableAutoGenTag = true + rootCmd.AddCommand(newDocumentationCommand(rootCmd)) + rootCmd.AddCommand(newTranslationsCommand()) +} diff --git a/cmd/translations.go b/cmd/translations.go new file mode 100644 index 0000000..3d52b12 --- /dev/null +++ b/cmd/translations.go @@ -0,0 +1,114 @@ +package cmd + +import ( + "context" + + "github.com/codereaper/lane/internal/downloader" + "github.com/codereaper/lane/internal/translations" + "github.com/spf13/cobra" +) + +func newTranslationsCommand() *cobra.Command { + var cmd = &cobra.Command{ + Use: "translations", + Short: "Manage translations", + Long: "Download translations from google sheets and/or generate translations from local csv files.", + } + cmd.AddCommand(newTranslationsDownloadCommand()) + cmd.AddCommand(newTranslationsGenerateCommand()) + return cmd +} + +func newTranslationsDownloadCommand() *cobra.Command { + var additionalHelp = `Authentication is done using a json file issued by Google. You get this json file by creating a "Service Account Key", which if you do not have a service account, requires you to first create a service account. + +Creating both an account and a key is explaining here: https://developers.google.com/identity/protocols/oauth2/service-account#creatinganaccount + +You may have to enable Google Drive API access when using it for the first time. The error message(s) should provide a direct link to enabling access. + +Make sure to share the sheet with the 'client_email' assigned to your service account. +` + var flags downloader.Flags + var cmd = &cobra.Command{ + Use: "download", + Short: "Download translations", + Long: additionalHelp, + Example: " lane translations download -o output.csv -c googleapi.json -d 11p...ev7lc -f csv", + RunE: func(cmd *cobra.Command, args []string) error { + return downloader.Download(context.Background(), &flags) + }, + } + cmd.Flags().StringVarP(&flags.Output, "output", "o", "", "Path to save output file (Required)") + cmd.Flags().StringVarP(&flags.Credentials, "credentials", "c", "", "A path to the credentails json file issued by Google (Required). More details under help") + cmd.Flags().StringVarP(&flags.DocumentId, "document", "d", "", "The document id of the sheet to download (Required). Found in its url, e.g. https://docs.google.com/spreadsheets/d//edit#gid=0") + cmd.Flags().StringVarP(&flags.Format, "format", "f", "", "The format of the output, defaults to csv") + cmd.MarkFlagRequired("output") + cmd.MarkFlagRequired("credentials") + cmd.MarkFlagRequired("document") + return cmd +} + +func newTranslationsGenerateCommand() *cobra.Command { + var additionalHelp = `Reads a CSV file and uses configuration strings to generate static resource files for android or ios. + +Each translated string can have '%'-style placeholders, however the number of placeholder for each translated language must be the same. +The placeholders in the generated output will always take a string as input. + +The purpose is to enable compilation checks for translated strings with an external source for the actual strings. + +EXAMPLES: + +If the contents of 'input.csv' is: + + KEY,UPDATE NEEDED,English,Danish,COMMENT + SOMETHING,,Something,Noget, + SOMETHING_WITH_ARGUMENTS,,Something with %1 and %2,Noget med %1 og %2, + +- Android + +The output using '-t android -i input.csv -c "3 en.xml" -k 1' would be: + + + Something + Something with %1$s and %2$s + + +- iOS + +The output using '-t ios -i input.csv -c "3 en.strings" -k 1 -m 3 -o translations.swift' would be: + +en.strings: + + "SOMETHING" = "Something"; + "SOMETHING_WITH_ARGUMENTS" = "Something with %1 and %2"; + +translations.swift: + + // swiftlint:disable all + import Foundation + struct Translations { + static let SOMETHING = NSLocalizedString("SOMETHING", comment: "") + static func SOMETHING_WITH_ARGUMENTS(_ p1: String, _ p2: String) -> String { return NSLocalizedString("SOMETHING_WITH_ARGUMENTS", comment: "").replacingOccurrences(of: "%1", with: p1).replacingOccurrences(of: "%2", with: p2) } + } +` + var flags translations.Flags + var configurations []string + var cmd = &cobra.Command{ + Use: "generate", + Short: "Generate translations files from a csv file", + Long: additionalHelp, + RunE: func(cmd *cobra.Command, args []string) error { + return translations.Generate(context.Background(), &flags, configurations) + }, + } + cmd.Flags().StringVarP(&flags.Input, "input", "i", "", "Path to a CSV file containing a key row and a row for each language (Required)") + cmd.Flags().StringVarP(&flags.Kind, "type", "t", "", "The type of output to generate, valid options are 'ios' or 'android' (Required)") + cmd.Flags().IntVarP(&flags.KeyIndex, "index", "k", 0, "The index of the key row, defaults to 0") + cmd.Flags().StringArrayVarP(&configurations, "configuration", "c", make([]string, 0), "A configuration string consisting of space separated row index and output path. Multiple configurations can be added, but one is required") + cmd.Flags().IntVarP(&flags.DefaultValueIndex, "main-index", "m", 0, "Required for ios. The index of the main/default language row, defaults to 0") + cmd.Flags().StringVarP(&flags.Output, "output", "o", "", "Required for ios. A path for the generated output") + cmd.MarkFlagRequired("input") + cmd.MarkFlagRequired("kind") + cmd.MarkFlagRequired("configuration") + return cmd +} diff --git a/docs/generated/lane.md b/docs/generated/lane.md new file mode 100644 index 0000000..70d848a --- /dev/null +++ b/docs/generated/lane.md @@ -0,0 +1,19 @@ +## lane + +Automates common tasks + +### Synopsis + +lane is a task automation helper that works well with tools like make. + +### Options + +``` + -h, --help help for lane +``` + +### SEE ALSO + +* [lane completion](lane_completion.md) - Generate the autocompletion script for the specified shell +* [lane translations](lane_translations.md) - Manage translations + diff --git a/docs/generated/lane_completion.md b/docs/generated/lane_completion.md new file mode 100644 index 0000000..7e50755 --- /dev/null +++ b/docs/generated/lane_completion.md @@ -0,0 +1,24 @@ +## lane completion + +Generate the autocompletion script for the specified shell + +### Synopsis + +Generate the autocompletion script for lane for the specified shell. +See each sub-command's help for details on how to use the generated script. + + +### Options + +``` + -h, --help help for completion +``` + +### SEE ALSO + +* [lane](lane.md) - Automates common tasks +* [lane completion bash](lane_completion_bash.md) - Generate the autocompletion script for bash +* [lane completion fish](lane_completion_fish.md) - Generate the autocompletion script for fish +* [lane completion powershell](lane_completion_powershell.md) - Generate the autocompletion script for powershell +* [lane completion zsh](lane_completion_zsh.md) - Generate the autocompletion script for zsh + diff --git a/docs/generated/lane_completion_bash.md b/docs/generated/lane_completion_bash.md new file mode 100644 index 0000000..9916588 --- /dev/null +++ b/docs/generated/lane_completion_bash.md @@ -0,0 +1,43 @@ +## lane completion bash + +Generate the autocompletion script for bash + +### Synopsis + +Generate the autocompletion script for the bash shell. + +This script depends on the 'bash-completion' package. +If it is not installed already, you can install it via your OS's package manager. + +To load completions in your current shell session: + + source <(lane completion bash) + +To load completions for every new session, execute once: + +#### Linux: + + lane completion bash > /etc/bash_completion.d/lane + +#### macOS: + + lane completion bash > $(brew --prefix)/etc/bash_completion.d/lane + +You will need to start a new shell for this setup to take effect. + + +``` +lane completion bash +``` + +### Options + +``` + -h, --help help for bash + --no-descriptions disable completion descriptions +``` + +### SEE ALSO + +* [lane completion](lane_completion.md) - Generate the autocompletion script for the specified shell + diff --git a/docs/generated/lane_completion_fish.md b/docs/generated/lane_completion_fish.md new file mode 100644 index 0000000..0a27c8d --- /dev/null +++ b/docs/generated/lane_completion_fish.md @@ -0,0 +1,34 @@ +## lane completion fish + +Generate the autocompletion script for fish + +### Synopsis + +Generate the autocompletion script for the fish shell. + +To load completions in your current shell session: + + lane completion fish | source + +To load completions for every new session, execute once: + + lane completion fish > ~/.config/fish/completions/lane.fish + +You will need to start a new shell for this setup to take effect. + + +``` +lane completion fish [flags] +``` + +### Options + +``` + -h, --help help for fish + --no-descriptions disable completion descriptions +``` + +### SEE ALSO + +* [lane completion](lane_completion.md) - Generate the autocompletion script for the specified shell + diff --git a/docs/generated/lane_completion_powershell.md b/docs/generated/lane_completion_powershell.md new file mode 100644 index 0000000..a89d91c --- /dev/null +++ b/docs/generated/lane_completion_powershell.md @@ -0,0 +1,31 @@ +## lane completion powershell + +Generate the autocompletion script for powershell + +### Synopsis + +Generate the autocompletion script for powershell. + +To load completions in your current shell session: + + lane completion powershell | Out-String | Invoke-Expression + +To load completions for every new session, add the output of the above command +to your powershell profile. + + +``` +lane completion powershell [flags] +``` + +### Options + +``` + -h, --help help for powershell + --no-descriptions disable completion descriptions +``` + +### SEE ALSO + +* [lane completion](lane_completion.md) - Generate the autocompletion script for the specified shell + diff --git a/docs/generated/lane_completion_zsh.md b/docs/generated/lane_completion_zsh.md new file mode 100644 index 0000000..c4c91cd --- /dev/null +++ b/docs/generated/lane_completion_zsh.md @@ -0,0 +1,45 @@ +## lane completion zsh + +Generate the autocompletion script for zsh + +### Synopsis + +Generate the autocompletion script for the zsh shell. + +If shell completion is not already enabled in your environment you will need +to enable it. You can execute the following once: + + echo "autoload -U compinit; compinit" >> ~/.zshrc + +To load completions in your current shell session: + + source <(lane completion zsh) + +To load completions for every new session, execute once: + +#### Linux: + + lane completion zsh > "${fpath[1]}/_lane" + +#### macOS: + + lane completion zsh > $(brew --prefix)/share/zsh/site-functions/_lane + +You will need to start a new shell for this setup to take effect. + + +``` +lane completion zsh [flags] +``` + +### Options + +``` + -h, --help help for zsh + --no-descriptions disable completion descriptions +``` + +### SEE ALSO + +* [lane completion](lane_completion.md) - Generate the autocompletion script for the specified shell + diff --git a/docs/generated/lane_translations.md b/docs/generated/lane_translations.md new file mode 100644 index 0000000..fd50a7f --- /dev/null +++ b/docs/generated/lane_translations.md @@ -0,0 +1,20 @@ +## lane translations + +Manage translations + +### Synopsis + +Download translations from google sheets and/or generate translations from local csv files. + +### Options + +``` + -h, --help help for translations +``` + +### SEE ALSO + +* [lane](lane.md) - Automates common tasks +* [lane translations download](lane_translations_download.md) - Download translations +* [lane translations generate](lane_translations_generate.md) - Generate translations files from a csv file + diff --git a/docs/generated/lane_translations_download.md b/docs/generated/lane_translations_download.md new file mode 100644 index 0000000..8659561 --- /dev/null +++ b/docs/generated/lane_translations_download.md @@ -0,0 +1,39 @@ +## lane translations download + +Download translations + +### Synopsis + +Authentication is done using a json file issued by Google. You get this json file by creating a "Service Account Key", which if you do not have a service account, requires you to first create a service account. + +Creating both an account and a key is explaining here: https://developers.google.com/identity/protocols/oauth2/service-account#creatinganaccount + +You may have to enable Google Drive API access when using it for the first time. The error message(s) should provide a direct link to enabling access. + +Make sure to share the sheet with the 'client_email' assigned to your service account. + + +``` +lane translations download [flags] +``` + +### Examples + +``` + lane translations download -o output.csv -c googleapi.json -d 11p...ev7lc -f csv +``` + +### Options + +``` + -c, --credentials string A path to the credentails json file issued by Google (Required). More details under help + -d, --document string The document id of the sheet to download (Required). Found in its url, e.g. https://docs.google.com/spreadsheets/d//edit#gid=0 + -f, --format string The format of the output, defaults to csv + -h, --help help for download + -o, --output string Path to save output file (Required) +``` + +### SEE ALSO + +* [lane translations](lane_translations.md) - Manage translations + diff --git a/docs/generated/lane_translations_generate.md b/docs/generated/lane_translations_generate.md new file mode 100644 index 0000000..d1b85f5 --- /dev/null +++ b/docs/generated/lane_translations_generate.md @@ -0,0 +1,69 @@ +## lane translations generate + +Generate translations files from a csv file + +### Synopsis + +Reads a CSV file and uses configuration strings to generate static resource files for android or ios. + +Each translated string can have '%'-style placeholders, however the number of placeholder for each translated language must be the same. +The placeholders in the generated output will always take a string as input. + +The purpose is to enable compilation checks for translated strings with an external source for the actual strings. + +EXAMPLES: + +If the contents of 'input.csv' is: + + KEY,UPDATE NEEDED,English,Danish,COMMENT + SOMETHING,,Something,Noget, + SOMETHING_WITH_ARGUMENTS,,Something with %1 and %2,Noget med %1 og %2, + +- Android + +The output using '-t android -i input.csv -c "3 en.xml" -k 1' would be: + + + Something + Something with %1$s and %2$s + + +- iOS + +The output using '-t ios -i input.csv -c "3 en.strings" -k 1 -m 3 -o translations.swift' would be: + +en.strings: + + "SOMETHING" = "Something"; + "SOMETHING_WITH_ARGUMENTS" = "Something with %1 and %2"; + +translations.swift: + + // swiftlint:disable all + import Foundation + struct Translations { + static let SOMETHING = NSLocalizedString("SOMETHING", comment: "") + static func SOMETHING_WITH_ARGUMENTS(_ p1: String, _ p2: String) -> String { return NSLocalizedString("SOMETHING_WITH_ARGUMENTS", comment: "").replacingOccurrences(of: "%1", with: p1).replacingOccurrences(of: "%2", with: p2) } + } + + +``` +lane translations generate [flags] +``` + +### Options + +``` + -c, --configuration stringArray A configuration string consisting of space separated row index and output path. Multiple configurations can be added, but one is required + -h, --help help for generate + -k, --index int The index of the key row, defaults to 0 + -i, --input string Path to a CSV file containing a key row and a row for each language (Required) + -m, --main-index int Required for ios. The index of the main/default language row, defaults to 0 + -o, --output string Required for ios. A path for the generated output + -t, --type string The type of output to generate, valid options are 'ios' or 'android' (Required) +``` + +### SEE ALSO + +* [lane translations](lane_translations.md) - Manage translations + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..756eb2d --- /dev/null +++ b/go.mod @@ -0,0 +1,45 @@ +module github.com/codereaper/lane + +go 1.22.4 + +require ( + github.com/spf13/cobra v1.8.1 + github.com/stretchr/testify v1.9.0 + google.golang.org/api v0.187.0 +) + +require ( + cloud.google.com/go/auth v0.6.1 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect + cloud.google.com/go/compute/metadata v0.4.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/s2a-go v0.1.7 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.5 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect + go.opentelemetry.io/otel v1.27.0 // indirect + go.opentelemetry.io/otel/metric v1.27.0 // indirect + go.opentelemetry.io/otel/trace v1.27.0 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect + google.golang.org/grpc v1.65.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d718e0b --- /dev/null +++ b/go.sum @@ -0,0 +1,165 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go/auth v0.6.1 h1:T0Zw1XM5c1GlpN2HYr2s+m3vr1p2wy+8VN+Z1FKxW38= +cloud.google.com/go/auth v0.6.1/go.mod h1:eFHG7zDzbXHKmjJddFG/rBlcGp6t25SwRUiEQSlO4x4= +cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= +cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= +cloud.google.com/go/compute/metadata v0.4.0 h1:vHzJCWaM4g8XIcm8kopr3XmDA4Gy/lblD3EhhSux05c= +cloud.google.com/go/compute/metadata v0.4.0/go.mod h1:SIQh1Kkb4ZJ8zJ874fqVkslA29PRXuleyj6vOzlbK7M= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA= +github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0= +go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= +go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= +go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= +go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= +go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= +go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.187.0 h1:Mxs7VATVC2v7CY+7Xwm4ndkX71hpElcvx0D1Ji/p1eo= +google.golang.org/api v0.187.0/go.mod h1:KIHlTc4x7N7gKKuVsdmfBXN13yEEWXWFURWY6SBp2gk= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d h1:Aqf0fiIdUQEj0Gn9mKFFXoQfTTEaNopWpfVyYADxiSg= +google.golang.org/genproto/googleapis/api v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Od4k8V1LQSizPRUK4OzZ7TBE/20k+jPczUDAEyvn69Y= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/downloader/downloader.go b/internal/downloader/downloader.go new file mode 100644 index 0000000..da79213 --- /dev/null +++ b/internal/downloader/downloader.go @@ -0,0 +1,88 @@ +package downloader + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + "strings" +) + +var validFormats = map[string]string{ + "csv": "text/csv", + "tsv": "text/tab-separated-values", + "ods": "application/x-vnd.oasis.opendocument.spreadsheet", + "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", +} + +func Download(ctx context.Context, flags *Flags) error { + service, err := newService(ctx, flags.Credentials) + if err != nil { + return err + } + + return download(flags, service) +} + +func download(flags *Flags, service Service) error { + if err := flags.validate(); err != nil { + return err + } + + mimeType, err := lookupMimeType(flags.Format) + if err != nil { + return err + } + + resp, err := service.download(flags.DocumentId, mimeType) + if err != nil { + return err + } + + return handleResponse(resp, flags.Output) +} + +func lookupMimeType(format string) (string, error) { + mimeType, ok := validFormats[strings.ToLower(format)] + if !ok { + return "", fmt.Errorf("invalid format: %s. Valid formats are %v", format, keys(validFormats)) + } + return mimeType, nil +} + +func handleResponse(resp *http.Response, outputPath string) error { + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to download file with http status %d", resp.StatusCode) + } + + tempPath := outputPath + ".tmp" + defer os.Remove(tempPath) + + tempFile, err := os.Create(tempPath) + if err != nil { + return err + } + defer tempFile.Close() + + _, err = io.Copy(tempFile, resp.Body) + if err != nil { + return err + } + + err = os.Rename(tempPath, outputPath) + if err != nil { + return err + } + return nil +} + +func keys(m map[string]string) []string { + k := make([]string, 0, len(m)) + for key := range m { + k = append(k, key) + } + return k +} diff --git a/internal/downloader/downloader_test.go b/internal/downloader/downloader_test.go new file mode 100644 index 0000000..8d9ed07 --- /dev/null +++ b/internal/downloader/downloader_test.go @@ -0,0 +1,120 @@ +package downloader + +import ( + "bytes" + "fmt" + "io" + "log" + "net/http" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +type MockService struct { + resp *http.Response + err error +} + +func (s MockService) download(documentId string, mimeType string) (*http.Response, error) { + return s.resp, s.err +} + +var expectedDownloadPath = "../../build/out.csv" + +func cleanup() { + log.Println("setup test") + os.Remove(expectedDownloadPath) +} + +func TestDownloadFailure(t *testing.T) { + defer cleanup() + flags := Flags{ + Output: expectedDownloadPath, + Credentials: "testdata/empty.json", + DocumentId: "1234567890", + Format: "csv", + } + svc := MockService{ + resp: nil, + err: fmt.Errorf("always fails"), + } + err := download(&flags, svc) + assert.Error(t, err) +} + +func TestDownloadFailedResponse(t *testing.T) { + defer cleanup() + flags := Flags{ + Output: expectedDownloadPath, + Credentials: "testdata/empty.json", + DocumentId: "1234567890", + Format: "csv", + } + svc := MockService{ + resp: &http.Response{ + Status: "Forbidden", + StatusCode: 401, + Body: io.NopCloser(&bytes.Buffer{}), + ContentLength: 0, + }, + err: nil, + } + err := download(&flags, svc) + assert.Error(t, err) + assert.NoFileExists(t, expectedDownloadPath) +} + +func TestDownloadInvalidFlags(t *testing.T) { + defer cleanup() + flags := Flags{ + Output: expectedDownloadPath, + Credentials: "testdata/empty.json", + DocumentId: "1234567890", + Format: "unknown", + } + svc := MockService{ + resp: nil, + err: nil, + } + err := download(&flags, svc) + assert.Error(t, err) +} + +func TestDownload(t *testing.T) { + defer cleanup() + var expected = []byte("test,data") + + flags := Flags{ + Output: expectedDownloadPath, + Credentials: "testdata/empty.json", + DocumentId: "1234567890", + Format: "csv", + } + svc := MockService{ + resp: &http.Response{ + Status: "OK", + StatusCode: 200, + Body: io.NopCloser(bytes.NewBuffer(expected)), + ContentLength: int64(len(expected)), + }, + err: nil, + } + err := download(&flags, svc) + assert.NoError(t, err) + + b, err := os.ReadFile(expectedDownloadPath) + assert.NoError(t, err) + assert.Equal(t, expected, b) +} + +func TestMimetypeLookup(t *testing.T) { + m, err := lookupMimeType("csv") + assert.NotEmpty(t, m) + assert.NoError(t, err) + + m, err = lookupMimeType("unknown") + assert.Empty(t, m) + assert.Error(t, err) +} diff --git a/internal/downloader/flags.go b/internal/downloader/flags.go new file mode 100644 index 0000000..9495d2a --- /dev/null +++ b/internal/downloader/flags.go @@ -0,0 +1,50 @@ +package downloader + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +type Flags struct { + Output string + Credentials string + DocumentId string + Format string +} + +func (f *Flags) validate() error { + if len(f.Format) == 0 { + f.Format = "csv" + } + if len(f.Output) == 0 { + return fmt.Errorf("output not provided") + } + if len(f.Credentials) == 0 { + return fmt.Errorf("key not provided") + } + if len(f.DocumentId) == 0 { + return fmt.Errorf("document id not provided") + } + + validFormat := false + keys := keys(validFormats) + for _, v := range keys { + if !validFormat && v == strings.ToLower(f.Format) { + validFormat = true + } + } + if !validFormat { + return fmt.Errorf("invalid format: %s. Valid formats are %v", f.Format, keys) + } + + if _, err := os.Stat(f.Credentials); err != nil { + return err + } + if _, err := os.Stat(filepath.Dir(f.Output)); err != nil { + return err + } + + return nil +} diff --git a/internal/downloader/flags_test.go b/internal/downloader/flags_test.go new file mode 100644 index 0000000..c96f603 --- /dev/null +++ b/internal/downloader/flags_test.go @@ -0,0 +1,131 @@ +package downloader + +import ( + "testing" +) + +var validationKases = []struct { + name string + flags Flags + passes bool +}{ + { + "none-set", + Flags{}, + false, + }, + { + "all-empty", + Flags{ + Output: "", + Credentials: "", + DocumentId: "", + Format: "", + }, + false, + }, + { + "all-set", + Flags{ + Output: "testdata/out.csv", + Credentials: "testdata/empty.json", + DocumentId: "1234567890", + Format: "csv", + }, + true, + }, + { + "all-set-multiple-scopes", + Flags{ + Output: "testdata/out.csv", + Credentials: "testdata/empty.json", + DocumentId: "1234567890", + Format: "csv", + }, + true, + }, + { + "all-set-but-output", + Flags{ + Output: "", + Credentials: "testdata/empty.json", + DocumentId: "1234567890", + Format: "csv", + }, + false, + }, + { + "all-set-but-key", + Flags{ + Output: "testdata/out.csv", + Credentials: "", + DocumentId: "1234567890", + Format: "csv", + }, + false, + }, + { + "all-set-but-documentid", + Flags{ + Output: "testdata/out.csv", + Credentials: "testdata/empty.json", + DocumentId: "", + Format: "csv", + }, + false, + }, + { + "all-set-but-format", + Flags{ + Output: "testdata/out.csv", + Credentials: "testdata/empty.json", + DocumentId: "1234567890", + Format: "", + }, + true, + }, + { + "all-set-incorrect-format", + Flags{ + Output: "testdata/out.csv", + Credentials: "testdata/empty.json", + DocumentId: "1234567890", + Format: "unknown", + }, + false, + }, + { + "all-set-missing-key", + Flags{ + Output: "testdata/out.csv", + Credentials: "testdata/not-here.json", + DocumentId: "1234567890", + Format: "csv", + }, + false, + }, + { + "all-set-missing-output-directory", + Flags{ + Output: "not-here/out.csv", + Credentials: "testdata/empty.json", + DocumentId: "1234567890", + Format: "csv", + }, + false, + }, +} + +func TestFlagsValidate(t *testing.T) { + for _, kase := range validationKases { + t.Run(kase.name, func(t *testing.T) { + err := kase.flags.validate() + if kase.passes && err != nil { + t.Errorf("expected to pass, but got %v", err) + } + if !kase.passes && err == nil { + t.Errorf("expected to fail, but got %v", err) + } + }) + } +} diff --git a/internal/downloader/service.go b/internal/downloader/service.go new file mode 100644 index 0000000..e620746 --- /dev/null +++ b/internal/downloader/service.go @@ -0,0 +1,34 @@ +package downloader + +import ( + "context" + "net/http" + + "google.golang.org/api/drive/v3" + "google.golang.org/api/option" +) + +type Service interface { + download(documentId string, mimeType string) (*http.Response, error) +} + +type GoogleAPIService struct { + ctx context.Context + service *drive.Service +} + +func newService(ctx context.Context, credentials string) (Service, error) { + drive, err := drive.NewService(ctx, option.WithCredentialsFile(credentials)) + if err != nil { + return nil, err + } + + return &GoogleAPIService{ + ctx: ctx, + service: drive, + }, nil +} + +func (s *GoogleAPIService) download(documentId string, mimeType string) (*http.Response, error) { + return s.service.Files.Export(documentId, mimeType).Context(s.ctx).Download() +} diff --git a/.github/test-resources/mobile-static-resources-images/assets/dk1.imageset/.gitkeep b/internal/downloader/testdata/empty.json similarity index 100% rename from .github/test-resources/mobile-static-resources-images/assets/dk1.imageset/.gitkeep rename to internal/downloader/testdata/empty.json diff --git a/internal/translations/common_test.go b/internal/translations/common_test.go new file mode 100644 index 0000000..69c0420 --- /dev/null +++ b/internal/translations/common_test.go @@ -0,0 +1,22 @@ +package translations + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func equalFiles(t *testing.T, expectedPath string, actualPath string) bool { + expected, err := os.ReadFile(expectedPath) + if !assert.NoError(t, err) { + return false + } + + actual, err := os.ReadFile(actualPath) + if !assert.NoError(t, err) { + return false + } + + return assert.Equal(t, expected, actual) +} diff --git a/internal/translations/flags.go b/internal/translations/flags.go new file mode 100644 index 0000000..1b5c40d --- /dev/null +++ b/internal/translations/flags.go @@ -0,0 +1,53 @@ +package translations + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +type Flags struct { + Input string + Kind string + KeyIndex int + DefaultValueIndex int + Output string +} + +func (f *Flags) validate() error { + if len(f.Input) == 0 { + return fmt.Errorf("input not provided") + } + if len(f.Kind) == 0 { + return fmt.Errorf("kind not provided") + } + + isIOS := false + + validKind := false + for _, v := range validKinds { + if !validKind && v == strings.ToLower(f.Kind) { + validKind = true + } + isIOS = isIOS || strings.ToLower(f.Kind) == "ios" + } + if !validKind { + return fmt.Errorf("invalid kind: %s. Valid kinds are %v", f.Kind, validKinds) + } + + if _, err := os.Stat(f.Input); err != nil { + return err + } + + if isIOS { + if len(f.Output) == 0 { + return fmt.Errorf("output not provided") + } + if _, err := os.Stat(filepath.Dir(f.Output)); err != nil { + return err + } + } + + return nil +} diff --git a/internal/translations/flags_test.go b/internal/translations/flags_test.go new file mode 100644 index 0000000..8916c9d --- /dev/null +++ b/internal/translations/flags_test.go @@ -0,0 +1,125 @@ +package translations + +import ( + "testing" +) + +var validationKases = []struct { + name string + flags Flags + passes bool +}{ + { + "none-set", + Flags{}, + false, + }, + { + "all-empty", + Flags{ + Input: "", + Kind: "", + KeyIndex: 0, + DefaultValueIndex: 0, + Output: "", + }, + false, + }, + { + "all-set-android", + Flags{ + Input: "testdata/input.csv", + Kind: "android", + KeyIndex: 0, + DefaultValueIndex: 0, + Output: "", + }, + true, + }, + { + "all-set-ios", + Flags{ + Input: "testdata/input.csv", + Kind: "ios", + KeyIndex: 0, + DefaultValueIndex: 0, + Output: "testdata/out.put", + }, + true, + }, + { + "all-set-but-unknown-kind", + Flags{ + Input: "testdata/input.csv", + Kind: "unknown", + KeyIndex: 0, + DefaultValueIndex: 0, + Output: "testdata/out.put", + }, + false, + }, + { + "all-set-ios-but-missing-input", + Flags{ + Kind: "ios", + KeyIndex: 0, + DefaultValueIndex: 0, + Output: "testdata/out.put", + }, + false, + }, + { + "all-set-ios-but-missing-kind", + Flags{ + Input: "testdata/input.csv", + KeyIndex: 0, + DefaultValueIndex: 0, + Output: "testdata/out.put", + }, + false, + }, + { + "all-set-ios-but-missing-output", + Flags{ + Input: "testdata/input.csv", + Kind: "ios", + KeyIndex: 0, + DefaultValueIndex: 0, + }, + false, + }, + { + "all-set-ios-but-missing-key", + Flags{ + Input: "testdata/input.csv", + Kind: "ios", + DefaultValueIndex: 0, + Output: "testdata/out.put", + }, + true, + }, + { + "all-set-ios-but-missing-value", + Flags{ + Input: "testdata/input.csv", + Kind: "ios", + KeyIndex: 0, + Output: "testdata/out.put", + }, + true, + }, +} + +func TestFlagsValidate(t *testing.T) { + for _, kase := range validationKases { + t.Run(kase.name, func(t *testing.T) { + err := kase.flags.validate() + if kase.passes && err != nil { + t.Errorf("expected to pass, but got %v", err) + } + if !kase.passes && err == nil { + t.Errorf("expected to fail, but got %v", err) + } + }) + } +} diff --git a/internal/translations/generator.go b/internal/translations/generator.go new file mode 100644 index 0000000..abd4297 --- /dev/null +++ b/internal/translations/generator.go @@ -0,0 +1,41 @@ +package translations + +import ( + "context" +) + +var androidKind = "android" +var iosKind = "ios" +var jsonKind = "json" + +var validKinds = []string{ + iosKind, + androidKind, + jsonKind, +} + +func Generate(ctx context.Context, flags *Flags, configurations []string) error { + err := flags.validate() + if err != nil { + return err + } + + files, err := newLanguageFiles(flags, configurations) + if err != nil { + return err + } + + translations, err := newTranslations(flags.Input) + if err != nil { + return err + } + + for _, f := range files { + err := f.Write(translations) + if err != nil { + return err + } + } + + return nil +} diff --git a/internal/translations/generator_test.go b/internal/translations/generator_test.go new file mode 100644 index 0000000..d0591c7 --- /dev/null +++ b/internal/translations/generator_test.go @@ -0,0 +1,55 @@ +package translations + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAndroid(t *testing.T) { + flags := Flags{ + Input: "testdata/input.csv", + Kind: "android", + KeyIndex: 1, + } + configurations := []string{"3 ../../build/en.xml", "4 ../../build/da.xml"} + + err := Generate(context.Background(), &flags, configurations) + + assert.Nil(t, err) + equalFiles(t, "testdata/android-en.expected", "../../build/en.xml") + equalFiles(t, "testdata/android-da.expected", "../../build/da.xml") +} + +func TestJson(t *testing.T) { + flags := Flags{ + Input: "testdata/input.csv", + Kind: "json", + KeyIndex: 1, + } + configurations := []string{"3 ../../build/en.json"} + + err := Generate(context.Background(), &flags, configurations) + + assert.Nil(t, err) + equalFiles(t, "testdata/json-en.expected", "../../build/en.json") +} + +func TestIos(t *testing.T) { + flags := Flags{ + Input: "testdata/input.csv", + Kind: "ios", + KeyIndex: 1, + DefaultValueIndex: 3, + Output: "../../build/Translations.swift", + } + configurations := []string{"3 ../../build/en.strings", "4 ../../build/da.strings"} + + err := Generate(context.Background(), &flags, configurations) + + assert.Nil(t, err) + equalFiles(t, "testdata/ios-en.expected", "../../build/en.strings") + equalFiles(t, "testdata/ios-da.expected", "../../build/da.strings") + equalFiles(t, "testdata/ios-swift.expected", "../../build/Translations.swift") +} diff --git a/internal/translations/language_file.android.go b/internal/translations/language_file.android.go new file mode 100644 index 0000000..43c3e17 --- /dev/null +++ b/internal/translations/language_file.android.go @@ -0,0 +1,46 @@ +package translations + +import ( + "fmt" + "io" + "regexp" + "strings" +) + +type androidLanguageFile struct { + file *languageFile +} + +func (f *androidLanguageFile) Write(translations *translationData) error { + return f.file.write(f, translations) +} + +func (f *androidLanguageFile) write(translation *translation, io io.Writer) error { + regex := regexp.MustCompile(`%([0-9]+)`) + + escape := strings.NewReplacer( + "&", "&", + "<", "<", + ">", ">", + "\"", "\\\"", + "'", "\\'", + "\n", "\\n") + + _, err := io.Write([]byte("\n")) + if err != nil { + return err + } + + for _, k := range translation.keys { + key := strings.ToLower(k) + value := regex.ReplaceAllString(translation.get(k), "%${1}$$s") + _, err = io.Write([]byte(fmt.Sprintf("\t%s\n", key, escape.Replace(value)))) + if err != nil { + return err + } + } + + _, err = io.Write([]byte("\n")) + + return err +} diff --git a/internal/translations/language_file.go b/internal/translations/language_file.go new file mode 100644 index 0000000..7785856 --- /dev/null +++ b/internal/translations/language_file.go @@ -0,0 +1,103 @@ +package translations + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strconv" + "strings" +) + +type languageFile struct { + path string + keyIndex int + valueIndex int +} + +type languageFileWriter interface { + write(translation *translation, io io.Writer) error + Write(translations *translationData) error +} + +func (f *languageFile) write(writer languageFileWriter, translations *translationData) error { + translation := translations.translation(f.keyIndex, f.valueIndex) + + tempPath := f.path + ".tmp" + defer os.Remove(tempPath) + + tempFile, err := os.Create(tempPath) + if err != nil { + return err + } + defer tempFile.Close() + + err = writer.write(translation, tempFile) + if err != nil { + return err + } + + return os.Rename(tempPath, f.path) +} + +func newLanguageFiles(flags *Flags, configurations []string) ([]languageFileWriter, error) { + if len(configurations) == 0 { + return nil, fmt.Errorf("no configurations provided") + } + + arguments := make(map[string]int, 0) + for _, configuration := range configurations { + fields := strings.Fields(configuration) + if len(fields) != 2 { + return nil, fmt.Errorf("configuration has invalid format: %s", configuration) + } + + index, err := strconv.Atoi(fields[0]) + if err != nil { + return nil, fmt.Errorf("configuration has invalid index: %s", configuration) + } + + path := fields[1] + if _, err := os.Stat(filepath.Dir(path)); err != nil { + return nil, fmt.Errorf("configuration has invalid path: %s", configuration) + } + + arguments[path] = index + } + + once := true + list := make([]languageFileWriter, 0) + for path, index := range arguments { + var writer languageFileWriter + + file := &languageFile{ + path: path, + keyIndex: flags.KeyIndex, + valueIndex: index, + } + + switch flags.Kind { + case androidKind: + writer = &androidLanguageFile{file: file} + case iosKind: + writer = &iosLanguageFile{file: file} + if once { + once = false + supporter := &iosSupportLanguageFile{file: &languageFile{ + path: flags.Output, + keyIndex: flags.KeyIndex, + valueIndex: flags.DefaultValueIndex, + }} + list = append(list, supporter) + } + case jsonKind: + writer = &jsonLanguageFile{file: file} + default: + return nil, fmt.Errorf("found unknown kind: %v", flags.Kind) + } + + list = append(list, writer) + } + + return list, nil +} diff --git a/internal/translations/language_file.ios.go b/internal/translations/language_file.ios.go new file mode 100644 index 0000000..67ad4f8 --- /dev/null +++ b/internal/translations/language_file.ios.go @@ -0,0 +1,32 @@ +package translations + +import ( + "fmt" + "io" + "strings" +) + +type iosLanguageFile struct { + file *languageFile +} + +func (f *iosLanguageFile) Write(translations *translationData) error { + return f.file.write(f, translations) +} + +func (f *iosLanguageFile) write(translation *translation, io io.Writer) error { + escape := strings.NewReplacer( + "\"", "\\\"", + "\n", "\\n") + + for _, k := range translation.keys { + key := strings.ToUpper(k) + value := translation.get(k) + _, err := io.Write([]byte(fmt.Sprintf("\"%s\" = \"%s\";\n", key, escape.Replace(value)))) + if err != nil { + return err + } + } + + return nil +} diff --git a/internal/translations/language_file.ios.support.go b/internal/translations/language_file.ios.support.go new file mode 100644 index 0000000..18ca625 --- /dev/null +++ b/internal/translations/language_file.ios.support.go @@ -0,0 +1,62 @@ +package translations + +import ( + "fmt" + "io" + "regexp" + "strings" +) + +type iosSupportLanguageFile struct { + file *languageFile +} + +func (f *iosSupportLanguageFile) Write(translations *translationData) error { + return f.file.write(f, translations) +} + +func (f *iosSupportLanguageFile) write(translation *translation, io io.Writer) error { + regex := regexp.MustCompile(`%([0-9]+)`) + + header := `// swiftlint:disable all +import Foundation +struct Translations { +` + footer := `} +` + + _, err := io.Write([]byte(header)) + if err != nil { + return err + } + + for _, k := range translation.keys { + key := strings.ToUpper(k) + value := translation.get(k) + + var line string + matches := regex.FindAllStringSubmatch(value, -1) + if len(matches) > 0 { + arguments := make([]string, 0) + replacements := make([]string, 0) + for _, m := range matches { + match := m[1] + arguments = append(arguments, fmt.Sprintf("_ p%s: String", match)) + replacements = append(replacements, fmt.Sprintf(".replacingOccurrences(of: \"%%%s\", with: p%s)", match, match)) + } + argumentsString := strings.Join(arguments, ", ") + replacementsString := strings.Join(replacements, "") + line = fmt.Sprintf("\tstatic func %s(%s) -> String { return NSLocalizedString(\"%s\", comment: \"\")%s }\n", key, argumentsString, key, replacementsString) + } else { + line = fmt.Sprintf("\tstatic let %s = NSLocalizedString(\"%s\", comment: \"\")\n", key, key) + } + + _, err := io.Write([]byte(line)) + if err != nil { + return err + } + } + + _, err = io.Write([]byte(footer)) + return err +} diff --git a/internal/translations/language_file.json.go b/internal/translations/language_file.json.go new file mode 100644 index 0000000..db514d6 --- /dev/null +++ b/internal/translations/language_file.json.go @@ -0,0 +1,31 @@ +package translations + +import ( + "encoding/json" + "io" + "strings" +) + +type jsonLanguageFile struct { + file *languageFile +} + +func (f *jsonLanguageFile) Write(translations *translationData) error { + return f.file.write(f, translations) +} + +func (f *jsonLanguageFile) write(translation *translation, io io.Writer) error { + data := map[string]string{} + for _, k := range translation.keys { + data[strings.ToLower(k)] = translation.get(k) + } + + b, err := json.MarshalIndent(data, "", " ") + if err != nil { + return err + } + + _, err = io.Write(b) + + return err +} diff --git a/internal/translations/language_file_test.go b/internal/translations/language_file_test.go new file mode 100644 index 0000000..05182c4 --- /dev/null +++ b/internal/translations/language_file_test.go @@ -0,0 +1,183 @@ +package translations + +import ( + "bytes" + "context" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +var interfaces = []interface{}{ + &androidLanguageFile{}, + &iosSupportLanguageFile{}, + &iosLanguageFile{}, + &jsonLanguageFile{}, +} + +func TestLanguageFileInterface(t *testing.T) { + for _, x := range interfaces { + w, ok := x.(languageFileWriter) + assert.True(t, ok) + assert.NotNil(t, w) + } +} + +var emptyWriters = []struct { + writer languageFileWriter + expectedPath string +}{ + { + &androidLanguageFile{}, + "testdata/android-empty.expected", + }, + { + &iosSupportLanguageFile{}, + "testdata/ios-swift-empty.expected", + }, + { + &jsonLanguageFile{}, + "testdata/json-empty.expected", + }, +} + +func TestLanguageFileWriteEmpty(t *testing.T) { + for _, x := range emptyWriters { + tr := newTranslation(map[string]string{}) + var b bytes.Buffer + + err := x.writer.write(tr, &b) + assert.NoError(t, err) + + expected, err := os.ReadFile(x.expectedPath) + if !assert.NoError(t, err) { + return + } + assert.EqualValues(t, expected, b.Bytes()) + } +} + +func TestIosLanguageFileWriteEmpty(t *testing.T) { + x := &iosLanguageFile{} + tr := newTranslation(map[string]string{}) + var b bytes.Buffer + + err := x.write(tr, &b) + assert.NoError(t, err) + assert.Nil(t, b.Bytes()) +} + +var inputWriters = []struct { + writer languageFileWriter + index int + expectedPath string +}{ + { + &androidLanguageFile{}, + 3, + "testdata/android-en.expected", + }, + { + &androidLanguageFile{}, + 4, + "testdata/android-da.expected", + }, + { + &iosLanguageFile{}, + 3, + "testdata/ios-en.expected", + }, + { + &iosLanguageFile{}, + 4, + "testdata/ios-da.expected", + }, + { + &iosSupportLanguageFile{}, + 3, + "testdata/ios-swift.expected", + }, + { + &jsonLanguageFile{}, + 3, + "testdata/json-en.expected", + }, +} + +func TestLanguageFileWriteInputFile(t *testing.T) { + translations, err := newTranslations("testdata/input.csv") + if !assert.NoError(t, err) { + return + } + + for _, x := range inputWriters { + tr := translations.translation(1, x.index) + var b bytes.Buffer + + err := x.writer.write(tr, &b) + assert.NoError(t, err) + + expected, err := os.ReadFile(x.expectedPath) + if !assert.NoError(t, err) { + return + } + assert.EqualValues(t, expected, b.Bytes()) + } +} + +func TestEscapingSingleColumn(t *testing.T) { + output := "\n\tAnother string including a | even.\n\tThis is a longer sentence, which includes a comma.\n\n" + outputPath := "../../build/out.csv" + inputPath := "../../build/test.csv" + err := os.WriteFile("../../build/test.csv", []byte("key,en\nTHIS,\"This is a longer sentence, which includes a comma.\"\nTHAT,Another string including a | even."), 0777) + if !assert.NoError(t, err) { + return + } + + flags := Flags{ + Input: inputPath, + Kind: "android", + KeyIndex: 1, + } + configurations := []string{"2 " + outputPath} + err = Generate(context.Background(), &flags, configurations) + if !assert.NoError(t, err) { + return + } + + b, err := os.ReadFile(outputPath) + if !assert.NoError(t, err) { + return + } + + assert.Equal(t, output, string(b)) +} + +func TestEscapingAllColumns(t *testing.T) { + output := "\n\tAnother string including a | even.\n\tThis is a longer sentence, which includes a comma.\n\n" + outputPath := "../../build/out.csv" + inputPath := "../../build/test.csv" + err := os.WriteFile("../../build/test.csv", []byte("\"key\",\"en\"\n\"THIS\",\"This is a longer sentence, which includes a comma.\"\n\"THAT\",\"Another string including a | even.\""), 0777) + if !assert.NoError(t, err) { + return + } + + flags := Flags{ + Input: inputPath, + Kind: "android", + KeyIndex: 1, + } + configurations := []string{"2 " + outputPath} + err = Generate(context.Background(), &flags, configurations) + if !assert.NoError(t, err) { + return + } + + b, err := os.ReadFile(outputPath) + if !assert.NoError(t, err) { + return + } + + assert.Equal(t, output, string(b)) +} diff --git a/.github/test-resources/mobile-update-translations/expected-android/result.DA.strings b/internal/translations/testdata/android-da.expected similarity index 100% rename from .github/test-resources/mobile-update-translations/expected-android/result.DA.strings rename to internal/translations/testdata/android-da.expected diff --git a/internal/translations/testdata/android-empty.expected b/internal/translations/testdata/android-empty.expected new file mode 100644 index 0000000..8542005 --- /dev/null +++ b/internal/translations/testdata/android-empty.expected @@ -0,0 +1,2 @@ + + diff --git a/.github/test-resources/mobile-update-translations/expected-android/result.EN.strings b/internal/translations/testdata/android-en.expected similarity index 100% rename from .github/test-resources/mobile-update-translations/expected-android/result.EN.strings rename to internal/translations/testdata/android-en.expected diff --git a/.github/test-resources/mobile-update-translations/configuration/input.csv b/internal/translations/testdata/input.csv similarity index 100% rename from .github/test-resources/mobile-update-translations/configuration/input.csv rename to internal/translations/testdata/input.csv diff --git a/.github/test-resources/mobile-update-translations/expected-ios/result.DA.strings b/internal/translations/testdata/ios-da.expected similarity index 100% rename from .github/test-resources/mobile-update-translations/expected-ios/result.DA.strings rename to internal/translations/testdata/ios-da.expected diff --git a/.github/test-resources/mobile-update-translations/expected-ios/result.EN.strings b/internal/translations/testdata/ios-en.expected similarity index 100% rename from .github/test-resources/mobile-update-translations/expected-ios/result.EN.strings rename to internal/translations/testdata/ios-en.expected diff --git a/internal/translations/testdata/ios-swift-empty.expected b/internal/translations/testdata/ios-swift-empty.expected new file mode 100644 index 0000000..3c5e4fc --- /dev/null +++ b/internal/translations/testdata/ios-swift-empty.expected @@ -0,0 +1,4 @@ +// swiftlint:disable all +import Foundation +struct Translations { +} diff --git a/.github/test-resources/mobile-update-translations/expected-ios/result.swift b/internal/translations/testdata/ios-swift.expected similarity index 100% rename from .github/test-resources/mobile-update-translations/expected-ios/result.swift rename to internal/translations/testdata/ios-swift.expected diff --git a/internal/translations/testdata/json-empty.expected b/internal/translations/testdata/json-empty.expected new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/internal/translations/testdata/json-empty.expected @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/internal/translations/testdata/json-en.expected b/internal/translations/testdata/json-en.expected new file mode 100644 index 0000000..3054233 --- /dev/null +++ b/internal/translations/testdata/json-en.expected @@ -0,0 +1,7 @@ +{ + "something": "Something", + "something_escaped": "Something with ,", + "something_for_xml": "It's like \u0026 and \u003cb\u003ebold\u003c/b\u003e \"text\" or 官话", + "something_with_arguments": "Something with %1 and %2", + "watch_out_for_new_lines": "Text\nwith\nnew\nlines" +} \ No newline at end of file diff --git a/internal/translations/translation.go b/internal/translations/translation.go new file mode 100644 index 0000000..97f52d5 --- /dev/null +++ b/internal/translations/translation.go @@ -0,0 +1,25 @@ +package translations + +import "slices" + +type translation struct { + keys []string + items map[string]string +} + +func newTranslation(items map[string]string) *translation { + keys := make([]string, 0, len(items)) + for k := range items { + keys = append(keys, k) + } + slices.Sort(keys) + + return &translation{ + keys: keys, + items: items, + } +} + +func (t *translation) get(key string) string { + return t.items[key] +} diff --git a/internal/translations/translation_test.go b/internal/translations/translation_test.go new file mode 100644 index 0000000..262bbeb --- /dev/null +++ b/internal/translations/translation_test.go @@ -0,0 +1,43 @@ +package translations + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewTranslation(t *testing.T) { + items := map[string]string{ + "c": "", + "d": "", + "f": "", + "e": "", + "a": "", + "b": "", + } + keys := []string{ + "a", "b", "c", "d", "e", "f", + } + + translation := newTranslation(items) + + assert.Equal(t, items, translation.items) + assert.EqualValues(t, keys, translation.keys) +} + +func TestTranslationGet(t *testing.T) { + key := "d" + expected := "4" + items := map[string]string{ + "a": "1", + "b": "2", + "c": "3", + key: expected, + "e": "5", + "f": "6", + } + + translation := newTranslation(items) + + assert.Equal(t, expected, translation.get(key)) +} diff --git a/internal/translations/translations.go b/internal/translations/translations.go new file mode 100644 index 0000000..25dd578 --- /dev/null +++ b/internal/translations/translations.go @@ -0,0 +1,39 @@ +package translations + +import ( + "encoding/csv" + "os" + "strings" +) + +type translationData struct { + data [][]string +} + +func newTranslations(path string) (*translationData, error) { + r, err := os.Open(path) + if err != nil { + return nil, err + } + + reader := csv.NewReader(r) + reader.Comma = ',' + + records, err := reader.ReadAll() + if err != nil { + return nil, err + } + + if len(records) > 0 { + records = records[1:] // skips header row + } + return &translationData{data: records}, nil +} + +func (t *translationData) translation(keyIndex int, valueIndex int) *translation { + items := map[string]string{} + for _, r := range t.data { + items[strings.ToLower(r[keyIndex-1])] = r[valueIndex-1] + } + return newTranslation(items) +} diff --git a/internal/translations/translations_test.go b/internal/translations/translations_test.go new file mode 100644 index 0000000..5f5a2aa --- /dev/null +++ b/internal/translations/translations_test.go @@ -0,0 +1,33 @@ +package translations + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewTranslationsMissingFile(t *testing.T) { + _, err := newTranslations("test/does-not-exist.csv") + + assert.Error(t, err) +} + +func TestNewTranslations(t *testing.T) { + translations, err := newTranslations("testdata/input.csv") + + if !assert.NoError(t, err) { + return + } + assert.NotEqual(t, 0, len(translations.data)) +} + +func TestTranslationsTranslation(t *testing.T) { + translations, err := newTranslations("testdata/input.csv") + + if !assert.NoError(t, err) { + return + } + translation := translations.translation(1, 3) + + assert.NotNil(t, translation) +} diff --git a/lane b/lane deleted file mode 100755 index 93c7685..0000000 --- a/lane +++ /dev/null @@ -1,87 +0,0 @@ -#!/bin/sh - -OLD_PWD=$(pwd) -LANED_PWD=$(realpath "${0}.d") - -echo 'Warning: lane is deprecated, see help for details' >&2 -trap 'set +x; cd "$OLD_PWD" >/dev/null 2>&1;' 0 -trap 'exit 2' 1 2 3 15 - -if [ "$1" = "completion" ]; then - if [ "$2" = "zsh" ] || [ "$2" = "bash" ]; then - cat "${LANED_PWD}/completion.$2.dotrc" - exit $? - else - echo "You must specify either 'zsh' or 'bash' completions" >&2 - exit 1 - fi -fi - -if [ "$1" = "-h" ] || [ "$1" = "--help" ] || [ "$1" = "help" ]; then - grep -v '^```' <"${LANED_PWD}/help.md" - exit 0 -fi - -if [ "$1" = "-v" ] || [ "$1" = "--version" ] || [ "$1" = "version" ]; then - echo "Version: ${VERSION:-unreleased}" - exit 0 -fi - -while [ ! -d lanes ] && [ ! "$(pwd)" = '/' ]; do - cd .. -done - -list_lanes() { - find lanes -mindepth 1 -maxdepth 1 -type f | sed 's|^lanes/||g' | sort -} - -display_lanes() { - echo "The available lanes are:" - if [ -d lanes ]; then - list_lanes | sed 's|^| |g' - else - echo ' No lanes found.' - fi -} - -if [ -z "$1" ]; then - display_lanes >&2 - exit 1 -fi - -if [ "$1" = "lanes" ]; then - list_lanes - exit 0 -fi - -builtin_lane="${LANED_PWD}/$1/" -if [ -d "$builtin_lane" ]; then - shift - - for i in "$@"; do - if [ "$i" = "-h" ]; then - grep -v '^```' <"$builtin_lane/help.md" - exit 0 - fi - done - - set +e - sh "$builtin_lane/run.sh" "$@" - status=$? - if [ $status -eq 111 ]; then - grep -v '^```' <"$builtin_lane/options.md" >&2 - exit 1 - fi - exit $status -fi - -user_lane="./lanes/$1" -if [ -f "$user_lane" ]; then - shift - sh "$user_lane" "$@" - exit 0 -fi - -echo "Unrecognized lane: '${1}'" >&2 -display_lanes -exit 10 diff --git a/lane.d/completion.bash.dotrc b/lane.d/completion.bash.dotrc deleted file mode 100644 index f51fb12..0000000 --- a/lane.d/completion.bash.dotrc +++ /dev/null @@ -1,9 +0,0 @@ -_lane() { - COMPREPLY=() - if [ "$1" = "$3" ]; then - mapfile -t COMPREPLY < <(lane lanes | grep "^$2") - fi - return 0 -} - -complete -F _lane lane diff --git a/lane.d/completion.zsh.dotrc b/lane.d/completion.zsh.dotrc deleted file mode 100644 index d1d72df..0000000 --- a/lane.d/completion.zsh.dotrc +++ /dev/null @@ -1,13 +0,0 @@ -_lane() { - COMPREPLY=() - if [ "$1" = "$3" ]; then - if [ "$2" = "--" ]; then - set -- "$1" "" "$3" - fi - output=$(lane lanes | grep "^$2") - COMPREPLY=(${(f)output}) - fi - return 0 -} - -complete -F _lane lane diff --git a/lane.d/google-api-docs-sheets-download/help.md b/lane.d/google-api-docs-sheets-download/help.md deleted file mode 100644 index d18b280..0000000 --- a/lane.d/google-api-docs-sheets-download/help.md +++ /dev/null @@ -1,36 +0,0 @@ -NAME -``` - google-api-docs-sheets-download - - a lane action -``` - -SYNOPSIS -``` - -o directory -i string -s string [-f string] - -h -``` - -DESCRIPTION -``` - DEPRECATED - in version 1.0.0 this action is available as 'lane translations download [OPTIONS]', but will require a JSON type service account key. - - Downloads a Sheet from Google Docs. -``` - -OPTIONS -``` - -h - Shows the full help. - - -o - A path to write the output to. - - -i - The document id of the sheet to download. Found in its url, e.g. https://docs.google.com/spreadsheets/d//edit#gid=0 - - -t - A JWT, see `lane google-api-jwt-generate -h`. - - -f - The format of the output, defaults to csv. -``` diff --git a/lane.d/google-api-docs-sheets-download/options.md b/lane.d/google-api-docs-sheets-download/options.md deleted file mode 100644 index faa16ca..0000000 --- a/lane.d/google-api-docs-sheets-download/options.md +++ /dev/null @@ -1,17 +0,0 @@ -OPTIONS -``` - -h - Shows the full help. - - -o - A path to write the output to. - - -i - The document id of the sheet to download. Found in its url, e.g. https://docs.google.com/spreadsheets/d//edit#gid=0 - - -t - A JWT, see `lane google-api-jwt-generate -h`. - - -f - The format of the output, defaults to csv. -``` diff --git a/lane.d/google-api-docs-sheets-download/run.sh b/lane.d/google-api-docs-sheets-download/run.sh deleted file mode 100644 index 8595acf..0000000 --- a/lane.d/google-api-docs-sheets-download/run.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/sh - -if [ ! -x "$(command -v curl)" ]; then - echo "Error: curl is not installed." >&2 - exit 3 -fi - -unset -v output id token -format='csv' -while getopts "i:t:f:o:" option; do - case $option in - o) output="$OPTARG" ;; - i) id="$OPTARG" ;; - t) token="$OPTARG" ;; - f) format="$OPTARG" ;; - \?) exit 111 ;; - esac -done -shift $((OPTIND - 1)) - -if [ -z "$id" ] || [ -z "$token" ] || [ -z "$output" ]; then - echo "Must provide output path, document id and bearer token." - exit 111 -fi - -DIR=$(mktemp -dq) - -trap 'set +x; rm -fr $DIR >/dev/null 2>&1' 0 -trap 'exit 2' 1 2 3 15 - -url=$(printf 'https://docs.google.com/spreadsheets/d/%s/export?exportFormat=%s' "$id" "$format") -header=$(printf 'Authorization: Bearer %s' "$token") - -output_parent=$(dirname "$output") -mkdir -p "$output_parent" -curl --silent --fail -L -o "$output" --header "$header" "$url" diff --git a/lane.d/google-api-jwt-generate/help.md b/lane.d/google-api-jwt-generate/help.md deleted file mode 100644 index a054814..0000000 --- a/lane.d/google-api-jwt-generate/help.md +++ /dev/null @@ -1,43 +0,0 @@ -NAME -``` - google-api-jwt-generate - - a lane action -``` - -SYNOPSIS -``` - -i string -s string -p file - -h -``` - -DESCRIPTION -``` - DEPRECATED - this functionality will be built into 'lane translations download [OPTIONS]' in version 1.0.0. - - Constructs JWT generation request for Google APIs and outputs a JWT. - - The purpose is to faciliate token generation for other usages of the Google APIs. -``` - -OPTIONS -``` - -h - Shows the full help. - - -i - The issuer of the JWT, e.g. @.iam.gserviceaccount.com - - -s - The scope(s) applied to the JWT. Apply multiple scopes by space separating them. - - -p - A path to the .p12 file issued by Google. See authentication section. -``` - -AUTHENTICATION - -Authentication happens using a specially created .p12 file issued by Google which must match the issuer. - -You get this .p12 key by creating a "Service Account Key", which if you do not have a service account, requires you to first create a service account. - -Creating both an account and a key is explained here: https://developers.google.com/identity/protocols/oauth2/service-account#creatinganaccount diff --git a/lane.d/google-api-jwt-generate/options.md b/lane.d/google-api-jwt-generate/options.md deleted file mode 100644 index 671ff46..0000000 --- a/lane.d/google-api-jwt-generate/options.md +++ /dev/null @@ -1,14 +0,0 @@ -OPTIONS -``` - -h - Shows the full help. - - -i - The issuer of the JWT, e.g. @.iam.gserviceaccount.com - - -s - The scope(s) applied to the JWT. Apply multiple scopes by space separating them. - - -p - A path to the .p12 file issued by Google. See authentication section in the full help. -``` diff --git a/lane.d/google-api-jwt-generate/run.sh b/lane.d/google-api-jwt-generate/run.sh deleted file mode 100644 index 083f884..0000000 --- a/lane.d/google-api-jwt-generate/run.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/sh - -for command in curl base64 openssl jq; do - if ! [ -x "$(command -v $command)" ]; then - echo "Error: $command is not installed." >&2 - exit 3 - fi -done - -unset -v issuer scopes p12 -while getopts "i:s:p:" option; do - case $option in - i) issuer="$OPTARG" ;; - s) scopes="$OPTARG" ;; - p) p12="$OPTARG" ;; - \?) exit 111 ;; - esac -done -shift $((OPTIND - 1)) - -if [ -z "$issuer" ] || [ -z "$scopes" ] || [ -z "$p12" ]; then - echo "Must provide issuer, scopes and an p12 file." - exit 111 -fi - -if [ ! -f "$p12" ]; then - echo "Must provide a p12 file." - exit 4 -fi - -echo 'Warning: this action is deprecated, see help for details' >&2 -DIR=$(mktemp -dq) - -trap 'set +x; rm -fr $DIR >/dev/null 2>&1' 0 -trap 'exit 2' 1 2 3 15 - -encode() { - base64 | tr -d '\n=' | tr '/+' '_-' -} - -iat=$(jq -n 'now|floor') -exp=$(jq -n 'now|floor| . + 300') -header='{"alg":"RS256","typ":"JWT"}' -claim=$(printf '{ - "iss": "%s", - "scope": "%s", - "aud": "https://oauth2.googleapis.com/token", - "exp": %s, - "iat": %s -}' "$issuer" "$scopes" "$exp" "$iat") - -encoded_header=$(echo "$header" | encode) -encoded_claim=$(echo "$claim" | encode) - -printf '%s.%s' "${encoded_header}" "${encoded_claim}" >"$DIR/request" -printf '%s.%s.' "${encoded_header}" "${encoded_claim}" >"$DIR/token" - -openssl pkcs12 -in "$p12" -out "$DIR/key" -nocerts -nodes -passin pass:notasecret 2>/dev/null -openssl dgst -sha256 -sign "$DIR/key" "$DIR/request" | encode >>"$DIR/token" - -assertion=$(cat "$DIR/token") - -set -e -response=$(curl --silent --fail -d "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=$assertion" https://oauth2.googleapis.com/token) -echo "$response" | jq -r '.access_token' diff --git a/lane.d/help.md b/lane.d/help.md deleted file mode 100644 index 63f34ff..0000000 --- a/lane.d/help.md +++ /dev/null @@ -1,35 +0,0 @@ -NAME -``` - lane -``` - -SYNOPSIS -``` - [@] - -h | --help | help - -v | --version | version -``` - -DESCRIPTION -``` - DEPRECATED - the functionality of basically executing a script can be provided by an actual script, make, ant and similiar tools. Use them instead. - - `lane` is a task automation helper. - - You can organize tasks in lanes. A task is written as a shell script. - - A task named `test` must be saved in 'lanes/test'. You can call a task from another task. - - There are builtin tasks that can be used in your lanes or in a stand-alone command. - - `lane` will search the current directory and all parent directories until it finds a 'lanes' directory, where it will look for user created lanes. -``` - -OPTIONS -``` - -h | --help | help - Shows the full help. - - -v | --version | version - Shows the version of lane. -``` diff --git a/lane.d/mobile-static-resources-images/help.md b/lane.d/mobile-static-resources-images/help.md deleted file mode 100644 index 7d438ca..0000000 --- a/lane.d/mobile-static-resources-images/help.md +++ /dev/null @@ -1,46 +0,0 @@ -NAME -``` - mobile-static-resources-images - - a lane action -``` - -SYNOPSIS -``` - -i directory -o file - -h -``` - -DESCRIPTION -``` - DEPRECATED - this functionality is now built into Xcode 15 and newer. See https://developer.apple.com/documentation/xcode-release-notes/xcode-15-release-notes#Asset-Catalogs - - Searches an assets directory for .imagesets and generates swift code. - - The purpose is to enable compilation checks for image references to embedded assets. -``` - -OPTIONS -``` - -h - Shows the full help. - - -i - A directory to search for .imageset in. - - -o - A path for the generated output file. -``` - -EXAMPLES - -If `Assets.xcassets` contains a single .imageset named test. - -The output using `-i Assets.xcassets -o -` would be: - -```swift - // swiftlint:disable all - import UIKit - struct Images { - static let test = UIImage(named:"test")! - } -``` diff --git a/lane.d/mobile-static-resources-images/options.md b/lane.d/mobile-static-resources-images/options.md deleted file mode 100644 index 05cf94b..0000000 --- a/lane.d/mobile-static-resources-images/options.md +++ /dev/null @@ -1,11 +0,0 @@ -OPTIONS -``` - -h - Shows the full help. - - -i - A directory to search for .imageset in. - - -o - A path for the generated output file. -``` diff --git a/lane.d/mobile-static-resources-images/run.sh b/lane.d/mobile-static-resources-images/run.sh deleted file mode 100755 index 91fe43d..0000000 --- a/lane.d/mobile-static-resources-images/run.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/sh - -unset -v input -unset -v output -while getopts "i:o:" option; do - case $option in - i) input=$OPTARG ;; - o) output=$OPTARG ;; - \?) exit 111 ;; - esac -done -shift $((OPTIND - 1)) - -if [ -z "$input" ] || [ -z "$output" ]; then - printf 'Provide both input and output arguments.\n\n' - exit 111 -fi - -if [ ! -d "$input" ]; then - printf 'Provide a valid input directory.\n\n' - exit 3 -fi - -parent=$(dirname "$output") -mkdir -p "$parent" 2>/dev/null - -echo 'Warning: this action is deprecated, see help for details' >&2 -{ - echo '// swiftlint:disable all' - echo 'import UIKit' - echo 'struct Images {' - - find "$input" -type d -iname "*.imageset" | LC_ALL=C sort | while read -r item; do - name=$(basename "$item" .imageset) - safe_name=$(echo "$name" | sed 's/-/_/g;s/ /_/g') - printf "\tstatic let %s = UIImage(named:\"%s\")!\n" "$safe_name" "$name" - done - - echo '}' - echo '// swiftlint:enable all' -} >"$output" diff --git a/lane.d/mobile-update-translations/extract.py b/lane.d/mobile-update-translations/extract.py deleted file mode 100644 index 0974b62..0000000 --- a/lane.d/mobile-update-translations/extract.py +++ /dev/null @@ -1,21 +0,0 @@ -import sys -import csv - -def escape(str_xml: str): - str_xml = str_xml.replace("&", "&") - str_xml = str_xml.replace("<", "<") - str_xml = str_xml.replace(">", ">") - str_xml = str_xml.replace("\"", "\\\"") - str_xml = str_xml.replace("'", "\\'") - return str_xml - -key = int(sys.argv[1]) - 1 -value = int(sys.argv[2]) - 1 -should_escape = int(sys.argv[3]) == 1 - -with open(sys.stdin.fileno()) as file: - reader = csv.reader(file, delimiter=',') - for row in reader: - if row[key]: - v = row[value].replace("\n", "\\n") - print("|".join([row[key], escape(v) if should_escape else v])) diff --git a/lane.d/mobile-update-translations/help.md b/lane.d/mobile-update-translations/help.md deleted file mode 100644 index 4cad4de..0000000 --- a/lane.d/mobile-update-translations/help.md +++ /dev/null @@ -1,93 +0,0 @@ -NAME -``` - mobile-update-translations - - a lane action -``` - -SYNOPSIS -``` - -t android -i file -c configuration -k index - -t ios -i file -c configuration -k index -m index -o file - -h -``` - -DESCRIPTION -``` - DEPRECATED - in version 1.0.0 this action is available as 'lane translations generate [OPTIONS]' - - Reads a CSV file and uses configuration strings to generate static resource files for android or ios. - - Each translated string can have '%'-style placeholders, however the number of placeholder for each translated language must be the same. - The placeholders in the generated output will always take a string as input. - - The purpose is to enable compilation checks for translated strings with an external source for the actual strings. -``` - -OPTIONS -``` - -h - Shows the full help. - - -t - The type of output to generate, valid options are 'ios' or 'android'. - - -i - A CSV file containing a key row and a row for each language. - - -k - The index of the key row. - - -c - A configuration string consisting of space separated row index and output path. Multiple configurations can be added. - - -m - Relevant for ios only. The index of the main/default language row. - - -o - Relevant for ios only. A path for the generated output. -``` - -EXAMPLES - -If the contents of `input.csv` is: - -```csv - KEY,UPDATE NEEDED,English,Danish,COMMENT - SOMETHING,,Something,Noget, - SOMETHING_WITH_ARGUMENTS,,Something with %1 and %2,Noget med %1 og %2, -``` - -Android ---- - -The output using `-t android -i input.csv -c '3 en.xml' -k 1` would be: - -```xml - - Something - Something with %1$s and %2$s - -``` - -iOS ---- - -The output using `-t ios -i input.csv -c '3 en.strings' -k 1 -m 3 -o translations.swift` would be: - -en.strings: - -```ini - "SOMETHING" = "Something"; - "SOMETHING_WITH_ARGUMENTS" = "Something with %1 and %2"; -``` - -translations.swift: - -```swift - // swiftlint:disable all - import Foundation - struct Translations { - static let SOMETHING = NSLocalizedString("SOMETHING", comment: "") - static func SOMETHING_WITH_ARGUMENTS(_ p1: String, _ p2: String) -> String { return NSLocalizedString("SOMETHING_WITH_ARGUMENTS", comment: "").replacingOccurrences(of: "%1", with: p1).replacingOccurrences(of: "%2", with: p2) } - } -``` diff --git a/lane.d/mobile-update-translations/options.md b/lane.d/mobile-update-translations/options.md deleted file mode 100644 index 1fcbda8..0000000 --- a/lane.d/mobile-update-translations/options.md +++ /dev/null @@ -1,23 +0,0 @@ -OPTIONS -``` - -h - Shows the full help. - - -t - The type of output to generate, valid options are 'ios' or 'android'. - - -i - A CSV file containing a key row and a row for each language. - - -k - The index of the key row. - - -c - A configuration string consisting of space separated row index and output path. Multiple configurations can be added. - - -m - Relevant for ios only. The index of the main/default language row. - - -o - Relevant for ios only. A path for the generated output. -``` diff --git a/lane.d/mobile-update-translations/run.sh b/lane.d/mobile-update-translations/run.sh deleted file mode 100755 index 50bb95c..0000000 --- a/lane.d/mobile-update-translations/run.sh +++ /dev/null @@ -1,128 +0,0 @@ -#!/bin/sh - -TMP=$(mktemp -dq) -BASE=$(dirname "$0") -trap 'set +x; rm -rf $TMP 2>/dev/null 2>&1' 0 -trap 'exit 2' 1 2 3 15 - -unset -v type -unset -v input -unset -v key_row -unset -v main_language -unset -v output -while getopts "t:i:c:k:m:o:" option; do - case $option in - t) type=$OPTARG ;; - i) input=$OPTARG ;; - c) echo "$OPTARG" >>"$TMP/mapping" ;; - k) key_row=$OPTARG ;; - m) main_language=$OPTARG ;; - o) output=$OPTARG ;; - \?) exit 111 ;; - esac -done -shift $((OPTIND - 1)) - -if [ ! "$type" = "ios" ] && [ ! "$type" = "android" ]; then - printf 'Provide ios or android as type.\n\n' - exit 111 -fi - -if [ "$type" = "ios" ]; then - if [ -z "$main_language" ]; then - printf 'Provide main language.\n\n' - exit 111 - fi - if [ -z "$output" ]; then - printf 'Provide output file.\n\n' - exit 111 - fi -fi - -if [ ! -f "$input" ]; then - printf 'Provide input file.\n\n' - exit 111 -fi - -if [ -z "$key_row" ]; then - printf 'Provide key row.\n\n' - exit 111 -fi - -makedir() { - parent=$(dirname "$1") - mkdir -p "$parent" 2>/dev/null -} - -python=$(command -v python3 | head -n1) -if [ ! -x "$python" ]; then - echo "Error: python3 is not installed." >&2 - exit 1 -fi - -while read -r item; do - offset=$(echo "$item" | cut -d\ -f1 | tr -d "[:blank:]") - [ "$type" = "android" ] && escape=1 || escape=0 - tail +2 "$input" | sed 's/|/\\\\\\/g' | $python "${BASE}/extract.py" "$key_row" "$offset" "$escape" | sed 's/\\\\\\/|/g' | LC_ALL=C sort -t \| -k1,1 >"${TMP}/${offset}.csv" -done <"${TMP}/mapping" - -if [ "$type" = "ios" ]; then - while read -r item; do - offset=$(echo "$item" | cut -d\ -f1 | tr -d "[:blank:]") - file=$(echo "$item" | cut -d\ -f2- | tr -d "[:blank:]") - - makedir "$file" - - printf "" >"$file" - while read -r line; do - key=$(printf "%s" "$line" | cut -d\| -f1 | sed 's|^"||g;s|"$||g') - value=$(printf "%s" "$line" | cut -d\| -f2- | sed 's|^"||;s|"$||;s|\"|\\"|g') - printf "\"%s\" = \"%s\";\n" "$key" "$value" >>"$file" - done <"${TMP}/${offset}.csv" - - done <"${TMP}/mapping" - - makedir "$output" - { - echo '// swiftlint:disable all' - echo 'import Foundation' - echo 'struct Translations {' - - while read -r item; do - key=$(printf "%s" "$item" | cut -d\| -f1 | sed 's|^"||g;s|"$||g') - value=$(printf "%s" "$item" | cut -d\| -f2-) - parameters=$(echo "$value" | grep -o -E '%[0-9]+' | wc -l | tr -d ' \n') - - if [ "$parameters" = "0" ]; then - printf "\tstatic let %s = NSLocalizedString(\"%s\", comment: \"\")\n" "$key" "$key" - else - arguments=$(for i in $(seq 1 "$parameters"); do printf "p%s: String, _ " "$i"; done | rev | cut -c5- | rev) - replacements=$(for i in $(seq 1 "$parameters"); do printf ".replacingOccurrences(of: \"%%%s\", with: p%s)" "$i" "$i"; done) - printf "\tstatic func %s(_ %s) -> String {" "$key" "$arguments" - printf " return NSLocalizedString(\"%s\", comment: \"\")" "$key" - printf "%s" "$replacements" - echo " }" - fi - done <"${TMP}/${main_language}.csv" - - echo '}' - } >"$output" -fi - -if [ "$type" = "android" ]; then - while read -r item; do - offset=$(echo "$item" | cut -d\ -f1 | tr -d "[:blank:]") - file=$(echo "$item" | cut -d\ -f2- | tr -d "[:blank:]") - - makedir "$file" - - echo "" >"$file" - while read -r line; do - key=$(printf "%s" "$line" | cut -d\| -f1 | sed 's|^"||g;s|"$||g' | tr "[:upper:]" "[:lower:]") - value=$(printf "%s" "$line" | cut -d\| -f2- | sed -E 's|%|%%|g;s|%%([0-9]+)|%\1$s|g;s|^"||;s|"$||') - printf "\t%s\n" "$key" "$value" >>"$file" - done <"${TMP}/${offset}.csv" - echo "" >>"$file" - - done <"${TMP}/mapping" -fi diff --git a/lane.d/shell-github-action-semver-compare/help.md b/lane.d/shell-github-action-semver-compare/help.md deleted file mode 100644 index 945e1a3..0000000 --- a/lane.d/shell-github-action-semver-compare/help.md +++ /dev/null @@ -1,45 +0,0 @@ -NAME -``` - shell-github-action-semver-compare - - a lane action -``` - -SYNOPSIS -``` - -m main-version -c current-version [-q] - -h -``` - -DESCRIPTION -``` - DEPRECATED - this action basically wraps a few lines of shell commands with almost no complexity. Use them directly. - - Compares two semver-style versions. - The exit code will indicate if the current version is considered higher than the main version. - The output includes a GitHub-Action-style group text for easier debugging, and an error message when exit code > 0. - - The purpose is to enable sanity testing required version changes. -``` - -OPTIONS -``` - -h - Shows the full help. - - -m - The main version - - -c - The current version - - -q - Quiet mode will not output debugging messages -``` - -EXIT CODES -``` - 10 - Indicates the versions match each other. - 20 - Indicates the main version is considered higher than the current version. -``` diff --git a/lane.d/shell-github-action-semver-compare/options.md b/lane.d/shell-github-action-semver-compare/options.md deleted file mode 100644 index fcfd095..0000000 --- a/lane.d/shell-github-action-semver-compare/options.md +++ /dev/null @@ -1,14 +0,0 @@ -OPTIONS -``` - -h - Shows the full help. - - -m - The main version - - -c - The current version - - -q - Quiet mode will not output debugging messages -``` diff --git a/lane.d/shell-github-action-semver-compare/run.sh b/lane.d/shell-github-action-semver-compare/run.sh deleted file mode 100755 index 0538da3..0000000 --- a/lane.d/shell-github-action-semver-compare/run.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/sh - -unset -v main -unset -v current -quiet=0 -while getopts "m:c:q" option; do - case $option in - m) main=$OPTARG ;; - c) current=$OPTARG ;; - q) quiet=1 ;; - \?) exit 111 ;; - esac -done -shift $((OPTIND - 1)) - -if [ -z "$main" ] || [ -z "$current" ]; then - printf 'Provide both main and current versions.\n\n' - exit 111 -fi - -if [ $quiet -eq 0 ]; then - echo '::group::Resolved versions' - printf '%s - main version\n%s - current version\n' "$main" "$current" - echo '::endgroup::' -fi - -[ "$main" = "$current" ] && { - echo 'Version must be changed.' - exit 10 -} - -echo 'Warning: this action is deprecated, see help for details' >&2 -verify=$(printf '%s\n%s' "$main" "$current" | sort -t '.' -k 1,1 -k 2,2 -k 3,3 -k 4,4 -k 5,5 -g | tail -n1) -[ "$main" = "$verify" ] && { - echo 'Version must be greater than version on main.' - exit 20 -} - -exit 0 diff --git a/lane.d/shell-run-github-workflow-tests/help.md b/lane.d/shell-run-github-workflow-tests/help.md deleted file mode 100644 index 1d9369f..0000000 --- a/lane.d/shell-run-github-workflow-tests/help.md +++ /dev/null @@ -1,73 +0,0 @@ -NAME -``` - shell-run-github-workflow-tests - - a lane action -``` - -SYNOPSIS -``` - -i file [-j job] - -h -``` - -DESCRIPTION -``` - Reads a yaml file with the structure of a GitHub workflow and runs the 'run' steps. - Each step will reports if its exit code indicated success or failure and counts towards a tally. - The steps in each job is run on a fresh copy of the workspace. - - The purpose is to enable unit-test-style tests for shell-based tooling. -``` - -OPTIONS -``` - -h - Shows this help. - - -i - A path to a GitHub workflow file. - - -j - An ID of a job in the provided workflow file. Will limit the execution to just the steps in this job. -``` - -EXAMPLES - -If the contents of `test.yaml` is: - - -```yaml - name: Tests - - on: - workflow_dispatch: {} - - jobs: - test-run: - name: Test run - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 # steps without the 'run' is ignored - - - name: Test that passes - run: | - echo 'Test-in-tests' - - - name: Test that fails - run: | - exit 1 -``` - -The output would be: - -``` - Preparing runnner... done! - Preparing workspace... done! - Executing runner... - - Test run (test-run) - - Test that passes: Pass - - Test that fails: Failed with exit code 1 - - Tests; Total: 2 Passes: 1 Fails: 1 -``` diff --git a/lane.d/shell-run-github-workflow-tests/options.md b/lane.d/shell-run-github-workflow-tests/options.md deleted file mode 100644 index 8f7ca31..0000000 --- a/lane.d/shell-run-github-workflow-tests/options.md +++ /dev/null @@ -1,11 +0,0 @@ -OPTIONS -``` - -h - Shows the full help. - - -i - A path to a GitHub workflow file. - - -j - An ID of a job in the provided workflow file. Will limit the execution to just the steps in this job. -``` diff --git a/lane.d/shell-run-github-workflow-tests/run.sh b/lane.d/shell-run-github-workflow-tests/run.sh deleted file mode 100755 index 2f314a0..0000000 --- a/lane.d/shell-run-github-workflow-tests/run.sh +++ /dev/null @@ -1,114 +0,0 @@ -#!/bin/sh - -for command in git yq jq; do - if ! [ -x "$(command -v $command)" ]; then - echo "Error: $command is not installed." >&2 - exit 1 - fi -done - -unset -v file -unset -v jobid -while getopts "i:j:" option; do - case $option in - i) file="$OPTARG" ;; - j) jobid="$OPTARG" ;; - \?) exit 111 ;; - esac -done -shift $((OPTIND - 1)) - -if [ ! -f "$file" ]; then - echo "Must provide a valid path to a GitHub workflow file with script-based tests." - if [ -n "$file" ]; then - printf "Given: %s\n\n" "$file" - else - echo - fi - exit 111 -fi - -echo 'Warning: this action is deprecated, see help for details' >&2 -DIR=$(mktemp -dq) - -trap 'set +x; rm -fr $DIR >/dev/null 2>&1' 0 -trap 'exit 2' 1 2 3 15 - -set -e - -printf 'Preparing runnner...' - -echo 'TOTAL_PASS=0; TOTAL_FAIL=0' >"$DIR/runner.sh" - -yq -o json "$file" | jq -rc '.jobs | to_entries[] | [{group: .key, groupName: .value.name, script: .value.steps}] | .[]' | while read -r job; do - group=$(printf '%s\n' "$job" | jq -rj '.group') - group_name=$(printf '%s\n' "$job" | jq -rj '.groupName') - - { - echo "cd '${DIR}/workspace/'; git reset HEAD --hard > /dev/null; git clean -fdx . > /dev/null; sh '${DIR}/${group}.sh'" - echo "TOTAL_PASS=\$((TOTAL_PASS+\$(cat ${DIR}/PASS))); TOTAL_FAIL=\$((TOTAL_FAIL+\$(cat ${DIR}/FAIL)))" - } >>"$DIR/runner.sh" - - { - echo "echo; echo '$group_name ($group)'" - printf "GREEN='\e[0;32m'; RED='\e[0;31m'; NC='\e[0m'; PASS=0; FAIL=0\n" - } >"${DIR}/${group}.sh" - - if [ -n "$jobid" ] && [ ! "$jobid" = "$group" ]; then - { - echo "echo ' - Skipped'" - echo "printf 0 > '${DIR}/PASS'; printf 0 > '${DIR}/FAIL'" - } >>"${DIR}/${group}.sh" - continue - fi - - I=0 - printf '%s\n' "$job" | jq -rc '.script[] | select(.run != null)' | while read -r step; do - step_name=$(printf '%s\n' "$step" | jq -rj '.name') - run=$(printf '%s\n' "$step" | jq -rj '.run') - - echo "$run" >"${DIR}/${group}.${I}.sh" - - { - echo "printf ' - ${step_name}: '" - echo "set +e; sh '${DIR}/${group}.${I}.sh' > messages 2>&1" - echo "ERROR=\$?" - printf "if [ \$ERROR -eq 0 ]; then printf \${GREEN}'Pass\n'\${NC}; PASS=\$((PASS+1)); else printf \${RED}'Failed with exit code %%s\n'\${NC} \$ERROR; FAIL=\$((FAIL+1)); cat messages; fi;\n" - } >>"${DIR}/${group}.sh" - - I=$((I + 1)) - done - - echo "printf \$PASS > '${DIR}/PASS'; printf \$FAIL > '${DIR}/FAIL'" >>"${DIR}/${group}.sh" - -done - -{ - # shellcheck disable=SC2016 - echo 'TOTAL=$((TOTAL_PASS+TOTAL_FAIL))' - # shellcheck disable=SC2016 - printf 'echo; printf "Tests; Total: \033[1m${TOTAL}\033[0m Passes: \033[1m${TOTAL_PASS}\033[0m Fails: \033[1m${TOTAL_FAIL}\033[0m\n"\n' - # shellcheck disable=SC2016 - echo 'if [ $TOTAL_FAIL -ne 0 ]; then exit 1; fi' -} >>"$DIR/runner.sh" - -echo ' done!' - -printf 'Preparing workspace... ' -mkdir "$DIR/workspace" -tar -c --exclude .git . | tar -x -C "$DIR/workspace/" - -cd "$DIR/workspace/" -git init >/dev/null 2>&1 -git config --local user.email "test@runner.local" -git config --local user.name "Test Runner" -git config --local commit.gpgsign false -git add . >/dev/null -git commit -am "known state" >/dev/null -echo ' done!' - -trap 'set +x; cd - > /dev/null; rm -fr $DIR >/dev/null 2>&1' 0 -trap 'exit 2' 1 2 3 15 - -echo 'Executing runner...' -sh "$DIR/runner.sh" diff --git a/lanes/test b/lanes/test deleted file mode 100644 index 8c46da2..0000000 --- a/lanes/test +++ /dev/null @@ -1,3 +0,0 @@ -lane shell-run-github-workflow-tests -i .github/workflows/unit-tests.yaml -shellcheck lane.d/*/run.sh lane -shellcheck --exclude=SC2148,SC2034,SC2206,SC2296 lane.d/*.dotrc diff --git a/main.go b/main.go new file mode 100644 index 0000000..930747e --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/codereaper/lane/cmd" + +func main() { + cmd.Execute() +}