From 15ca0ad05854aa7b8f0849a1d6ec74bf91f5f404 Mon Sep 17 00:00:00 2001 From: Kevin Franklin Kim Date: Thu, 7 Mar 2024 13:43:53 +0100 Subject: [PATCH] feat: initial commit --- .editorconfig | 12 + .github/dependabot.yml | 15 + .github/workflows/release.yml | 37 ++ .github/workflows/test.yml | 32 ++ .gitignore | 14 + .golangci.yml | 58 +++ .goreleaser.yml | 41 ++ .husky.yaml | 15 + .husky/applypatch-msg | 3 + .husky/commit-msg | 3 + .husky/fsmonitor-watchman | 3 + .husky/post-update | 3 + .husky/pre-applypatch | 3 + .husky/pre-commit | 3 + .husky/pre-merge-commit | 3 + .husky/pre-push | 3 + .husky/pre-rebase | 3 + .husky/pre-receive | 3 + .husky/prepare-commit-msg | 3 + .husky/push-to-checkout | 3 + .husky/sendemail-validate | 3 + .husky/update | 3 + CODE_OF_CONDUCT.md | 128 ++++++ LICENSE | 21 + Makefile | 100 +++++ README.md | 49 +++ _example/client/types.d.ts | 55 +++ _example/server/addtocart.go | 7 + _example/server/item.go | 22 + _example/server/login.go | 5 + _example/server/search.go | 5 + _example/server/signup.go | 5 + _example/sesamy.yaml | 23 + cmd/root.go | 102 +++++ cmd/tagmanager.go | 25 ++ cmd/tagmanagerserver.go | 84 ++++ cmd/tagmanagerweb.go | 80 ++++ cmd/typescript.go | 37 ++ cmd/version.go | 17 + go.mod | 67 +++ go.sum | 304 +++++++++++++ internal/events.go | 80 ++++ main.go | 28 ++ pkg/config/config.go | 56 +++ pkg/tagmanager/client.go | 800 ++++++++++++++++++++++++++++++++++ pkg/tagmanager/client_test.go | 420 ++++++++++++++++++ 46 files changed, 2786 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 .goreleaser.yml create mode 100644 .husky.yaml create mode 100755 .husky/applypatch-msg create mode 100755 .husky/commit-msg create mode 100755 .husky/fsmonitor-watchman create mode 100755 .husky/post-update create mode 100755 .husky/pre-applypatch create mode 100755 .husky/pre-commit create mode 100755 .husky/pre-merge-commit create mode 100755 .husky/pre-push create mode 100755 .husky/pre-rebase create mode 100755 .husky/pre-receive create mode 100755 .husky/prepare-commit-msg create mode 100755 .husky/push-to-checkout create mode 100755 .husky/sendemail-validate create mode 100755 .husky/update create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 _example/client/types.d.ts create mode 100644 _example/server/addtocart.go create mode 100644 _example/server/item.go create mode 100644 _example/server/login.go create mode 100644 _example/server/search.go create mode 100644 _example/server/signup.go create mode 100644 _example/sesamy.yaml create mode 100644 cmd/root.go create mode 100644 cmd/tagmanager.go create mode 100644 cmd/tagmanagerserver.go create mode 100644 cmd/tagmanagerweb.go create mode 100644 cmd/typescript.go create mode 100644 cmd/version.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/events.go create mode 100644 main.go create mode 100644 pkg/config/config.go create mode 100644 pkg/tagmanager/client.go create mode 100644 pkg/tagmanager/client_test.go diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0977931 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = tab +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{yaml,yml,md,mdx}] +indent_style = space diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1ba1f3b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..297e3e0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,37 @@ +name: Release Tag + +on: + push: + tags: + - v*.*.* + workflow_dispatch: + +env: + GOFLAGS: -mod=readonly + GOPROXY: https://proxy.golang.org + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-go@v5 + with: + check-latest: true + go-version-file: go.mod + + - id: app_token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.TOKEN_APP_ID }} + private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }} + + - uses: goreleaser/goreleaser-action@v5 + with: + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ steps.app_token.outputs.token }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..5e379a0 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,32 @@ +name: checks + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + merge_group: + branches: [ main ] + workflow_dispatch: + +env: + GOFLAGS: -mod=readonly + GOPROXY: https://proxy.golang.org + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v4 + with: + check-latest: true + go-version-file: 'go.mod' + + - uses: golangci/golangci-lint-action@v3 + + - name: Run tests + run: go test -v ./... + env: + SHELL: "/bin/zsh" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e99ac8f --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +.* +*.log +!.github/ +!.husky/ +!.editorconfig +!.gitignore +!.golangci.yml +!.goreleaser.yml +!.husky.yaml +!.yamllint.yaml +/bin/ +/coverage.out +/coverage.html +/tmp/ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..2de2817 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,58 @@ +run: + timeout: 5m + +linters-settings: + gocritic: + disabled-checks: + - ifElseChain + - commentFormatting + +linters: + enable: + # Enabled by default linters: + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + + # Disabled by default linters: + - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers [fast: true, auto-fix: false] + - bidichk # Checks for dangerous unicode character sequences [fast: true, auto-fix: false] + #- dupl # Tool for code clone detection [fast: true, auto-fix: false] + - forcetypeassert # finds forced type assertions [fast: true, auto-fix: false] + #- gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false] + #- goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false] + - gocritic # Provides diagnostics that check for bugs, performance and style issues. [fast: false, auto-fix: false] + - goimports # In addition to fixing imports, goimports also formats your code in the same style as gofmt. [fast: true, auto-fix: true] + #- gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. [fast: true, auto-fix: false] + - gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. [fast: true, auto-fix: false] + - goprintffuncname # Checks that printf-like functions are named with `f` at the end [fast: true, auto-fix: false] + - gosec # (gas): Inspects source code for security problems [fast: false, auto-fix: false] + - grouper # An analyzer to analyze expression groups. [fast: true, auto-fix: false] + - importas # Enforces consistent import aliases [fast: false, auto-fix: false] + #- maintidx # maintidx measures the maintainability index of each function. [fast: true, auto-fix: false] + #- makezero # Finds slice declarations with non-zero initial length [fast: false, auto-fix: false] + - misspell # Finds commonly misspelled English words in comments [fast: true, auto-fix: true] + - nakedret # Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false] + #- nestif # Reports deeply nested if statements [fast: true, auto-fix: false] + - nilerr # Finds the code that returns nil even if it checks that the error is not nil. [fast: false, auto-fix: false] + - nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value. [fast: false, auto-fix: false] + - noctx # noctx finds sending http request without context.Context [fast: false, auto-fix: false] + - nolintlint # Reports ill-formed or insufficient nolint directives [fast: true, auto-fix: false] + - nonamedreturns # Reports all named returns [fast: false, auto-fix: false] + - nosprintfhostport # Checks for misuse of Sprintf to construct a host with port in a URL. [fast: true, auto-fix: false] + - prealloc # Finds slice declarations that could potentially be pre-allocated [fast: true, auto-fix: false] + - predeclared # find code that shadows one of Go's predeclared identifiers [fast: true, auto-fix: false] + - promlinter # Check Prometheus metrics naming via promlint [fast: true, auto-fix: false] + #- revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. [fast: false, auto-fix: false] + #- tagliatelle # Checks the struct tags. [fast: true, auto-fix: false] + - testpackage # linter that makes you use a separate _test package [fast: true, auto-fix: false] + - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers [fast: false, auto-fix: false] + - unconvert # Remove unnecessary type conversions [fast: false, auto-fix: false] + - unparam # Reports unused function parameters [fast: false, auto-fix: false] + - usestdlibvars # A linter that detect the possibility to use variables/constants from the Go standard library. [fast: true, auto-fix: false] + - wastedassign # wastedassign finds wasted assignment statements. [fast: false, auto-fix: false] + - whitespace # Tool for detection of leading and trailing whitespace [fast: true, auto-fix: true] + disable: + - unused diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..50e1fc6 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,41 @@ +# .goreleaser.yml +# Build customization +builds: + - binary: sesamy + main: ./cmd/main.go + env: + - CGO_ENABLED=0 + goos: + - windows + - darwin + - linux + goarch: + - amd64 + - arm64 + goarm: + - 7 + flags: + - -trimpath + ldflags: + - -s -w -X github.com/foomo/sesamy-cli/cmd/actions.version={{.Version}} + +release: + prerelease: auto + +archives: + - format: tar.gz + format_overrides: + - goos: windows + format: zip + +changelog: + use: github-native + +brews: + # Repository to push the tap to. + - repository: + owner: foomo + name: homebrew + caveats: "sesamy --help" + homepage: "https://github.com/foomo/sesamy-cli" + description: "CLI utitlity to manage Server Side Tag Management" diff --git a/.husky.yaml b/.husky.yaml new file mode 100644 index 0000000..2bbd935 --- /dev/null +++ b/.husky.yaml @@ -0,0 +1,15 @@ +hooks: + pre-commit: + - golangci-lint run --fast + - husky lint-staged + commit-msg: + # only execute if not in a merge + - if [[ -z $(git rev-parse -q --verify MERGE_HEAD) ]]; then husky lint-commit; fi + +lint-staged: + '*.go': + - goimports -l -w + +lint-commit: + types: '^(feat|fix|build|chore|docs|perf|refactor|revert|style|test|wip)$' + header: '^(?P\w+)(\((?P[\w/.-]+)\))?(?P!)?:( +)?(?P
.+)' diff --git a/.husky/applypatch-msg b/.husky/applypatch-msg new file mode 100755 index 0000000..32d0649 --- /dev/null +++ b/.husky/applypatch-msg @@ -0,0 +1,3 @@ +#!/bin/sh + +husky hook $(basename "$0") $* diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 0000000..32d0649 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,3 @@ +#!/bin/sh + +husky hook $(basename "$0") $* diff --git a/.husky/fsmonitor-watchman b/.husky/fsmonitor-watchman new file mode 100755 index 0000000..32d0649 --- /dev/null +++ b/.husky/fsmonitor-watchman @@ -0,0 +1,3 @@ +#!/bin/sh + +husky hook $(basename "$0") $* diff --git a/.husky/post-update b/.husky/post-update new file mode 100755 index 0000000..32d0649 --- /dev/null +++ b/.husky/post-update @@ -0,0 +1,3 @@ +#!/bin/sh + +husky hook $(basename "$0") $* diff --git a/.husky/pre-applypatch b/.husky/pre-applypatch new file mode 100755 index 0000000..32d0649 --- /dev/null +++ b/.husky/pre-applypatch @@ -0,0 +1,3 @@ +#!/bin/sh + +husky hook $(basename "$0") $* diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..32d0649 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,3 @@ +#!/bin/sh + +husky hook $(basename "$0") $* diff --git a/.husky/pre-merge-commit b/.husky/pre-merge-commit new file mode 100755 index 0000000..32d0649 --- /dev/null +++ b/.husky/pre-merge-commit @@ -0,0 +1,3 @@ +#!/bin/sh + +husky hook $(basename "$0") $* diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000..32d0649 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,3 @@ +#!/bin/sh + +husky hook $(basename "$0") $* diff --git a/.husky/pre-rebase b/.husky/pre-rebase new file mode 100755 index 0000000..32d0649 --- /dev/null +++ b/.husky/pre-rebase @@ -0,0 +1,3 @@ +#!/bin/sh + +husky hook $(basename "$0") $* diff --git a/.husky/pre-receive b/.husky/pre-receive new file mode 100755 index 0000000..32d0649 --- /dev/null +++ b/.husky/pre-receive @@ -0,0 +1,3 @@ +#!/bin/sh + +husky hook $(basename "$0") $* diff --git a/.husky/prepare-commit-msg b/.husky/prepare-commit-msg new file mode 100755 index 0000000..32d0649 --- /dev/null +++ b/.husky/prepare-commit-msg @@ -0,0 +1,3 @@ +#!/bin/sh + +husky hook $(basename "$0") $* diff --git a/.husky/push-to-checkout b/.husky/push-to-checkout new file mode 100755 index 0000000..32d0649 --- /dev/null +++ b/.husky/push-to-checkout @@ -0,0 +1,3 @@ +#!/bin/sh + +husky hook $(basename "$0") $* diff --git a/.husky/sendemail-validate b/.husky/sendemail-validate new file mode 100755 index 0000000..32d0649 --- /dev/null +++ b/.husky/sendemail-validate @@ -0,0 +1,3 @@ +#!/bin/sh + +husky hook $(basename "$0") $* diff --git a/.husky/update b/.husky/update new file mode 100755 index 0000000..32d0649 --- /dev/null +++ b/.husky/update @@ -0,0 +1,3 @@ +#!/bin/sh + +husky hook $(basename "$0") $* diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..34aabf4 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +info@bestbytes.de. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..000433a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Foomo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f592787 --- /dev/null +++ b/Makefile @@ -0,0 +1,100 @@ +.DEFAULT_GOAL:=help +-include .makerc + +# --- Targets ----------------------------------------------------------------- + +# This allows us to accept extra arguments +%: .husky + @: + +.PHONY: .husky +# Configure git hooks for husky +.husky: + @if ! command -v husky &> /dev/null; then \ + echo "ERROR: missing executeable 'husky', please run:"; \ + echo "\n$ go install github.com/go-courier/husky/cmd/husky@latest\n"; \ + fi + @git config core.hooksPath .husky + +## === Tasks === + +## === Tasks === + +.PHONY: doc +## Run tests +doc: + @open "http://localhost:6060/pkg/github.com/foomo/sesamy-cli/" + @godoc -http=localhost:6060 -play + +.PHONY: test +## Run tests +test: + @go test -coverprofile=coverage.out -race -json ./... | gotestfmt + +.PHONY: test.update +## Run tests and update snapshots +test.update: + @go test -update -coverprofile=coverage.out -race -json ./... | gotestfmt + +.PHONY: lint +## Run linter +lint: + @golangci-lint run + +.PHONY: lint.fix +## Fix lint violations +lint.fix: + @golangci-lint run --fix + +.PHONY: tidy +## Run go mod tidy +tidy: + @go mod tidy + +.PHONY: outdated +## Show outdated direct dependencies +outdated: + @go list -u -m -json all | go-mod-outdated -update -direct + +## Install binary +install: + @go build -o ${GOPATH}/bin/sesamy main.go + +## Build binary +build: + @mkdir -p bin + @go build -o bin/sesamy main.go + +## === Utils === + +## Show help text +help: + @awk '{ \ + if ($$0 ~ /^.PHONY: [a-zA-Z\-\_0-9]+$$/) { \ + helpCommand = substr($$0, index($$0, ":") + 2); \ + if (helpMessage) { \ + printf "\033[36m%-23s\033[0m %s\n", \ + helpCommand, helpMessage; \ + helpMessage = ""; \ + } \ + } else if ($$0 ~ /^[a-zA-Z\-\_0-9.]+:/) { \ + helpCommand = substr($$0, 0, index($$0, ":")); \ + if (helpMessage) { \ + printf "\033[36m%-23s\033[0m %s\n", \ + helpCommand, helpMessage"\n"; \ + helpMessage = ""; \ + } \ + } else if ($$0 ~ /^##/) { \ + if (helpMessage) { \ + helpMessage = helpMessage"\n "substr($$0, 3); \ + } else { \ + helpMessage = substr($$0, 3); \ + } \ + } else { \ + if (helpMessage) { \ + print "\n "helpMessage"\n" \ + } \ + helpMessage = ""; \ + } \ + }' \ + $(MAKEFILE_LIST) diff --git a/README.md b/README.md new file mode 100644 index 0000000..d0b41d3 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# Sesamy CLI + +[![Go Report Card](https://goreportcard.com/badge/github.com/foomo/sesamy-cli)](https://goreportcard.com/report/github.com/foomo/sesamy-cli) +[![godoc](https://godoc.org/github.com/foomo/sesamy-cli?status.svg)](https://godoc.org/github.com/foomo/sesamy-cli) +[![goreleaser](https://github.com/foomo/sesamy-cli/workflows/goreleaser/badge.svg)](https://github.com/foomo/sesamy-cli/actions) + +> CLI to keep you sane while working with GTM. + +## Quickstart + +Add a `sesamy.yaml` configurtion + +```yaml +google: + ga4: + measurement_id: G-PZ5ELRCR31 + + gtm: + account_id: 6099238525 + server: + container_id: 175348980 + workspace_id: 10 + measurement_id: GTM-5NWPR4QW + + web: + container_id: 175355532 + workspace_id: 23 + measurement_id: GTM-57BHX34G + + credentials_file: ./tmp/google_service_account_creds.json + +events: + packages: + - path: "github.com/foomo/sesamy-cli/_example/server" + output_path: "./_example/client/types.d.ts" + indent: "\t" +``` + +## Caveats + +You might need to increase your Google Tag Manager API quotas, since they are limited to 15 r/m by default. + +## How to Contribute + +Make a pull request... + +## License + +Distributed under MIT License, please see license file within the code for more details. diff --git a/_example/client/types.d.ts b/_example/client/types.d.ts new file mode 100644 index 0000000..3b08960 --- /dev/null +++ b/_example/client/types.d.ts @@ -0,0 +1,55 @@ +// Code generated by tygo. DO NOT EDIT. + +////////// +// source: addtocart.go + +export interface AddToCart { + currency?: string; + value?: number /* float64 */; + items?: (Item | undefined)[]; +} + +////////// +// source: item.go + +export interface Item { + affiliation?: string; + coupon?: string; + discount?: number /* float64 */; + index?: number /* int */; + item_brand?: string; + item_category?: string; + item_category2?: string; + item_category3?: string; + item_category4?: string; + item_category5?: string; + item_id?: string; + item_list_name?: string; + item_name?: string; + item_variant?: string; + item_list_id?: string; + location_id?: string; + price?: string; + quantity?: number /* float64 */; +} + +////////// +// source: login.go + +export interface Login { + method?: string; +} + +////////// +// source: search.go + +export interface Search { + search_term?: string; +} + +////////// +// source: signup.go + +export interface SignUp { + method?: string; +} diff --git a/_example/server/addtocart.go b/_example/server/addtocart.go new file mode 100644 index 0000000..5d044ed --- /dev/null +++ b/_example/server/addtocart.go @@ -0,0 +1,7 @@ +package server + +type AddToCart struct { + Currency string `json:"currency,omitempty"` + Value float64 `json:"value,omitempty"` + Items []*Item `json:"items,omitempty"` +} diff --git a/_example/server/item.go b/_example/server/item.go new file mode 100644 index 0000000..385b6df --- /dev/null +++ b/_example/server/item.go @@ -0,0 +1,22 @@ +package server + +type Item struct { + Affiliation string `json:"affiliation,omitempty"` + Coupon string `json:"coupon,omitempty"` + Discount float64 `json:"discount,omitempty"` + Index int `json:"index,omitempty"` + ItemBrand string `json:"item_brand,omitempty"` + ItemCategory string `json:"item_category,omitempty"` + ItemCategory2 string `json:"item_category2,omitempty"` + ItemCategory3 string `json:"item_category3,omitempty"` + ItemCategory4 string `json:"item_category4,omitempty"` + ItemCategory5 string `json:"item_category5,omitempty"` + ItemID string `json:"item_id,omitempty"` + ItemListName string `json:"item_list_name,omitempty"` + ItemName string `json:"item_name,omitempty"` + ItemVariant string `json:"item_variant,omitempty"` + ItemListID string `json:"item_list_id,omitempty"` + LocationID string `json:"location_id,omitempty"` + Price string `json:"price,omitempty"` + Quantity float64 `json:"quantity,omitempty"` +} diff --git a/_example/server/login.go b/_example/server/login.go new file mode 100644 index 0000000..876e19b --- /dev/null +++ b/_example/server/login.go @@ -0,0 +1,5 @@ +package server + +type Login struct { + Method string `json:"method,omitempty"` +} diff --git a/_example/server/search.go b/_example/server/search.go new file mode 100644 index 0000000..5d5a01f --- /dev/null +++ b/_example/server/search.go @@ -0,0 +1,5 @@ +package server + +type Search struct { + SearchTerm string `json:"search_term,omitempty"` +} diff --git a/_example/server/signup.go b/_example/server/signup.go new file mode 100644 index 0000000..5407b70 --- /dev/null +++ b/_example/server/signup.go @@ -0,0 +1,5 @@ +package server + +type SignUp struct { + Method string `json:"method,omitempty"` +} diff --git a/_example/sesamy.yaml b/_example/sesamy.yaml new file mode 100644 index 0000000..958bb45 --- /dev/null +++ b/_example/sesamy.yaml @@ -0,0 +1,23 @@ +google: + ga4: + measurement_id: G-PZ5ELRCR31 + + gtm: + account_id: 6099238525 + server: + container_id: 175348980 + workspace_id: 10 + measurement_id: GTM-5NWPR4QW + + web: + container_id: 175355532 + workspace_id: 23 + measurement_id: GTM-57BHX34G + + credentials_file: ./tmp/google_service_account_creds.json + +events: + packages: + - path: "github.com/foomo/sesamy-cli/_example/server" + output_path: "./_example/client/types.d.ts" + indent: "\t" diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..598b550 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,102 @@ +package cmd + +import ( + "bytes" + "io" + "os" + + "github.com/foomo/sesamy-cli/pkg/config" + "github.com/mitchellh/mapstructure" + "github.com/pterm/pterm" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var ( + logger *pterm.Logger + verbose bool + cfgFilename string + cfg *config.Config +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "sesamy", + Short: "Server Side Tag Management System", +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + cobra.OnInitialize(initConfig) + + // Here you will define your flags and configuration settings. + // Cobra supports persistent flags, which, if defined here, + // will be global for your application. + + rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "output debug information") + rootCmd.PersistentFlags().StringVarP(&cfgFilename, "config", "c", "", "config file (default is sesamy.yaml)") + + // Cobra also supports local flags, which will only run + // when this action is called directly. + //rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + viper.SetConfigType("yaml") + if cfgFilename == "-" { + // do nothing + } else if cfgFilename != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFilename) + } else { + // Search config in home directory with name ".sesamy" (without extension). + viper.AddConfigPath(".") + viper.SetConfigName("sesamy") + } + + // read in environment variables that match + //viper.EnvKeyReplacer(strings.NewReplacer(".", "_")) + //viper.SetEnvPrefix("SESAMY") + //viper.AutomaticEnv() + + logger = pterm.DefaultLogger.WithTime(false) + if verbose { + logger = logger.WithLevel(pterm.LogLevelTrace).WithCaller(true) + } +} + +func preRunReadConfig(cmd *cobra.Command, args []string) error { + if cfgFilename == "-" { + logger.Debug("using config from stdin") + b, err := io.ReadAll(cmd.InOrStdin()) + if err != nil { + return err + } + if err := viper.ReadConfig(bytes.NewBuffer(b)); err != nil { + return err + } + } else { + logger.Debug("using config file", logger.Args("filename", viper.ConfigFileUsed())) + if err := viper.ReadInConfig(); err != nil { + return err + } + } + logger.Debug("config", logger.ArgsFromMap(viper.AllSettings())) + + if err := viper.Unmarshal(&cfg, func(decoderConfig *mapstructure.DecoderConfig) { + decoderConfig.TagName = "yaml" + }); err != nil { + return err + } + + return nil +} diff --git a/cmd/tagmanager.go b/cmd/tagmanager.go new file mode 100644 index 0000000..c0e3964 --- /dev/null +++ b/cmd/tagmanager.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// tagmanagerCmd represents the tagmanager command +var tagmanagerCmd = &cobra.Command{ + Use: "tagmanager", + Short: "Provision Google Tag Manager containers", +} + +func init() { + rootCmd.AddCommand(tagmanagerCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // tagmanagerCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // tagmanagerCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/tagmanagerserver.go b/cmd/tagmanagerserver.go new file mode 100644 index 0000000..3b68063 --- /dev/null +++ b/cmd/tagmanagerserver.go @@ -0,0 +1,84 @@ +package cmd + +import ( + "github.com/foomo/sesamy-cli/pkg/tagmanager" + "github.com/spf13/cobra" + "google.golang.org/api/option" +) + +// tagmanagerServerCmd represents the server command +var tagmanagerServerCmd = &cobra.Command{ + Use: "server", + Short: "Provision Google Tag Manager Server Container", + PersistentPreRunE: preRunReadConfig, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + var opt option.ClientOption + if cfg.Google.CredentialsFile != "" { + opt = option.WithCredentialsFile(cfg.Google.CredentialsFile) + } else { + opt = option.WithCredentialsFile(cfg.Google.CredentialsJSON) + } + + c, err := tagmanager.NewClient( + cfg.Google.GTM.AccountID, + cfg.Google.GTM.Server.ContainerID, + cfg.Google.GTM.Server.WorkspaceID, + cfg.Google.GA4.MeasurementID, + tagmanager.ClientWithClientOptions(opt), + ) + if err != nil { + return err + } + + logger.Info("- Folder:", logger.Args("name", c.FolderName())) + if _, err := c.UpsertFolder(ctx, c.FolderName()); err != nil { + return err + } + + logger.Info("- Variable:", logger.Args("name", "ga4-measurement-id")) + measurementID, err := c.UpsertConstantVariable(ctx, "ga4-measurement-id", c.MeasurementID()) + if err != nil { + return err + } + + logger.Info("- GTM client:", logger.Args("name", "Google Tag Manager Web Container")) + if _, err := c.UpsertGTMClient(ctx, "Google Tag Manager Web Container", cfg.Google.GTM.Web.MeasurementID); err != nil { + return err + } + + logger.Info("- GA4 client:", logger.Args("name", "Google Analytics GA4")) + ga4Client, err := c.UpsertGA4Client(ctx, "Google Analytics GA4") + if err != nil { + return err + } + + logger.Info("- GA4 trigger:", logger.Args("name", "Google Analytics GA4")) + ga4ClientTrigger, err := c.UpsertClientTrigger(ctx, "ga4", ga4Client.Name) + if err != nil { + return err + } + + logger.Info("- GA4 tag:", logger.Args("name", "Google Analytics GA4")) + if _, err := c.UpsertGA4ServerTag(ctx, "Google Analytics GA4", measurementID, ga4ClientTrigger); err != nil { + return err + } + + return nil + }, +} + +func init() { + tagmanagerCmd.AddCommand(tagmanagerServerCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // tagmanagerServerCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // tagmanagerServerCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/tagmanagerweb.go b/cmd/tagmanagerweb.go new file mode 100644 index 0000000..a932ca1 --- /dev/null +++ b/cmd/tagmanagerweb.go @@ -0,0 +1,80 @@ +package cmd + +import ( + "github.com/foomo/sesamy-cli/internal" + "github.com/foomo/sesamy-cli/pkg/tagmanager" + "github.com/spf13/cobra" + "google.golang.org/api/option" +) + +// tagmanagerWebCmd represents the web command +var tagmanagerWebCmd = &cobra.Command{ + Use: "web", + Short: "Provision Google Tag Manager Web Container", + PersistentPreRunE: preRunReadConfig, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + var opt option.ClientOption + if cfg.Google.CredentialsFile != "" { + opt = option.WithCredentialsFile(cfg.Google.CredentialsFile) + } else { + opt = option.WithCredentialsFile(cfg.Google.CredentialsJSON) + } + + eventParameters, err := internal.GetEventParameters(cfg) + if err != nil { + return err + } + + c, err := tagmanager.NewClient( + cfg.Google.GTM.AccountID, + cfg.Google.GTM.Web.ContainerID, + cfg.Google.GTM.Web.WorkspaceID, + cfg.Google.GA4.MeasurementID, + tagmanager.ClientWithClientOptions(opt), + ) + if err != nil { + return err + } + + logger.Info("- Folder:", logger.Args("name", c.FolderName())) + if _, err := c.UpsertFolder(ctx, c.FolderName()); err != nil { + return err + } + + logger.Info("- Variable:", logger.Args("name", "ga4-measurement-id")) + measurementID, err := c.UpsertConstantVariable(ctx, "ga4-measurement-id", c.MeasurementID()) + if err != nil { + return err + } + + for key, value := range eventParameters { + logger.Info("- GA4 Trigger:", logger.Args("name", key, "parameters", value)) + trigger, err := c.UpsertCustomEventTrigger(ctx, key) + if err != nil { + return err + } + logger.Info("- GA4 Tag:", logger.Args("name", key, "parameters", value)) + if _, err := c.UpsertGA4WebTag(ctx, key, value, measurementID, trigger); err != nil { + return err + } + } + + return nil + }, +} + +func init() { + tagmanagerCmd.AddCommand(tagmanagerWebCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // tagmanagerWebCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // tagmanagerWebCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/typescript.go b/cmd/typescript.go new file mode 100644 index 0000000..34b8766 --- /dev/null +++ b/cmd/typescript.go @@ -0,0 +1,37 @@ +package cmd + +import ( + "github.com/gzuidhof/tygo/tygo" + "github.com/spf13/cobra" +) + +// typescriptCmd represents the typescript command +var typescriptCmd = &cobra.Command{ + Use: "typescript", + Short: "Generate typescript events", + PersistentPreRunE: preRunReadConfig, + RunE: func(cmd *cobra.Command, args []string) error { + gen := tygo.New(&tygo.Config{ + Packages: cfg.Events.Packages, + }) + for k, v := range cfg.Events.TypeMappings { + gen.SetTypeMapping(k, v) + } + + return gen.Generate() + }, +} + +func init() { + rootCmd.AddCommand(typescriptCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // typescriptCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // typescriptCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..76f6f3f --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,17 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var version = "latest" + +var versionCmd = &cobra.Command{ + Use: "version", + Short: "show version information", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println(version) + }, +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f00de70 --- /dev/null +++ b/go.mod @@ -0,0 +1,67 @@ +module github.com/foomo/sesamy-cli + +go 1.21 + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc + github.com/fatih/structtag v1.2.0 + github.com/gzuidhof/tygo v0.2.14 + github.com/mitchellh/mapstructure v1.5.0 + github.com/pterm/pterm v0.12.79 + github.com/spf13/cobra v1.8.0 + github.com/spf13/viper v1.18.2 + github.com/stoewer/go-strcase v1.3.0 + github.com/stretchr/testify v1.8.4 + golang.org/x/tools v0.13.0 + google.golang.org/api v0.153.0 +) + +require ( + atomicgo.dev/cursor v0.2.0 // indirect + atomicgo.dev/keyboard v0.2.9 // indirect + atomicgo.dev/schedule v0.1.0 // indirect + cloud.google.com/go/compute v1.23.3 // indirect + cloud.google.com/go/compute/metadata v0.2.3 // indirect + github.com/containerd/console v1.0.3 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/s2a-go v0.1.7 // indirect + github.com/google/uuid v1.4.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.0 // indirect + github.com/gookit/color v1.5.4 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lithammer/fuzzysearch v1.1.8 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + go.opencensus.io v0.24.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/crypto v0.16.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/mod v0.12.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/oauth2 v0.15.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/term v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect + google.golang.org/grpc v1.59.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..47c40af --- /dev/null +++ b/go.sum @@ -0,0 +1,304 @@ +atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg= +atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ= +atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw= +atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= +atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= +atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= +atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= +atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= +cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= +github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= +github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= +github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= +github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= +github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= +github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= +github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= +github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= +github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= +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/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/cpuguy83/go-md2man/v2 v2.0.3/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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/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/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +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.1/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.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +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.5.5/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.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.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.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= +github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/gzuidhof/tygo v0.2.14 h1:QRBD6eby2SMyYzv8KzXA+yopPbEO6w2Qzuuqqp9z+vU= +github.com/gzuidhof/tygo v0.2.14/go.mod h1:s3lpnppkDixQQhMWD78yPtAmugMHENsPWpQYziUIpw0= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= +github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= +github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= +github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= +github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= +github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= +github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= +github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= +github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= +github.com/pterm/pterm v0.12.79 h1:lH3yrYMhdpeqX9y5Ep1u7DejyHy7NSQg9qrBjF9dFT4= +github.com/pterm/pterm v0.12.79/go.mod h1:1v/gzOF1N0FsjbgTHZ1wVycRkKiatFvJSJC4IGaQAAo= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +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/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= +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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +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-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= +golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= +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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.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.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.153.0 h1:N1AwGhielyKFaUqH07/ZSIQR3uNPcV7NVw0vj+j4iR4= +google.golang.org/api v0.153.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY= +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/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +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 v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ= +google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc= +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.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +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.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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/events.go b/internal/events.go new file mode 100644 index 0000000..dd0d562 --- /dev/null +++ b/internal/events.go @@ -0,0 +1,80 @@ +package internal + +import ( + "fmt" + "go/ast" + "go/token" + "strings" + + "github.com/fatih/structtag" + "github.com/foomo/sesamy-cli/pkg/config" + "github.com/pterm/pterm" + "github.com/stoewer/go-strcase" + "golang.org/x/tools/go/packages" +) + +func GetEventParameters(cfg *config.Config) (map[string][]string, error) { + ret := map[string][]string{} + + pkgs, err := packages.Load(&packages.Config{ + Mode: packages.NeedSyntax | packages.NeedFiles, + }, cfg.Events.PackageNames()...) + if err != nil { + return nil, err + } + + for i, pkg := range pkgs { + if len(pkg.Errors) > 0 { + return nil, fmt.Errorf("%+v", pkg.Errors) + } + + if len(pkg.GoFiles) == 0 { + return nil, fmt.Errorf("no input go files for package index %d", i) + } + + conf := cfg.Events.PackageConfig(pkg.ID) + + for i, file := range pkg.Syntax { + if conf.IsFileIgnored(pkg.GoFiles[i]) { + continue + } + + ast.Inspect(file, func(n ast.Node) bool { + // GenDecl can be an import, type, var, or const expression + if x, ok := n.(*ast.GenDecl); ok { + if x.Tok == token.IMPORT { + return false + } + + for _, spec := range x.Specs { + // e.g. "type Foo struct {}" or "type Bar = string" + if elem, ok := spec.(*ast.TypeSpec); ok && elem.Name.IsExported() { + if strct, ok := elem.Type.(*ast.StructType); ok { + for _, field := range strct.Fields.List { + tags, err := structtag.Parse(strings.Trim(field.Tag.Value, "`")) + if err != nil { + pterm.Warning.Println(err.Error(), field.Tag.Value) + return false + } + tag, err := tags.Get("json") + if err != nil { + pterm.Warning.Println(err.Error()) + return false + } + if tag.Value() != "" && tag.Value() != "-" { + name := strcase.SnakeCase(elem.Name.String()) + ret[name] = append(ret[name], strings.Split(tag.Value(), ",")[0]) + } + } + } + } + } + return false + } + return true + }) + } + } + + return ret, nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..d3260ce --- /dev/null +++ b/main.go @@ -0,0 +1,28 @@ +/* +Copyright © 2024 NAME HERE + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package main + +import "github.com/foomo/sesamy-cli/cmd" + +func main() { + cmd.Execute() +} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..0cfdd88 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,56 @@ +package config + +import ( + "github.com/gzuidhof/tygo/tygo" +) + +type Config struct { + Google Google `yaml:"google"` + // https://github.com/gzuidhof/tygo + Events Events `yaml:"events"` +} + +type Google struct { + GA4 GA4 `yaml:"ga4"` + GTM GTM `yaml:"gtm"` + CredentialsFile string `yaml:"credentials_file"` + CredentialsJSON string `yaml:"credentials_json"` +} + +type GA4 struct { + MeasurementID string `yaml:"measurement_id"` +} + +type GTM struct { + AccountID string `yaml:"account_id"` + Web Container `yaml:"web"` + Server Container `yaml:"server"` +} + +type Events struct { + Packages []*tygo.PackageConfig `yaml:"packages"` + TypeMappings map[string]string `yaml:"type_mappings"` +} + +func (e Events) PackageNames() []string { + ret := make([]string, len(e.Packages)) + for i, value := range e.Packages { + ret[i] = value.Path + } + return ret +} + +func (e Events) PackageConfig(path string) *tygo.PackageConfig { + for _, value := range e.Packages { + if value.Path == path { + return value + } + } + return nil +} + +type Container struct { + ContainerID string `yaml:"container_id"` + WorkspaceID string `yaml:"workspace_id"` + MeasurementID string `yaml:"measurement_id"` +} diff --git a/pkg/tagmanager/client.go b/pkg/tagmanager/client.go new file mode 100644 index 0000000..cf904cf --- /dev/null +++ b/pkg/tagmanager/client.go @@ -0,0 +1,800 @@ +package tagmanager + +import ( + "context" + "errors" + + "google.golang.org/api/option" + "google.golang.org/api/tagmanager/v2" +) + +var ( + ErrNotFound = errors.New("not found") +) + +type ( + Client struct { + notes string + accountID string + containerID string + workspaceID string + measurementID string + folderName string + clientOptions []option.ClientOption + // cache + service *tagmanager.Service + clients map[string]*tagmanager.Client + folders map[string]*tagmanager.Folder + variables map[string]*tagmanager.Variable + triggers map[string]*tagmanager.Trigger + tags map[string]*tagmanager.Tag + } + ClientOption func(*Client) +) + +// ------------------------------------------------------------------------------------------------ +// ~ Options +// ------------------------------------------------------------------------------------------------ + +func ClientWithNotes(v string) ClientOption { + return func(o *Client) { + o.notes = v + } +} + +func ClientWithFolderName(v string) ClientOption { + return func(o *Client) { + o.folderName = v + } +} + +func ClientWithClientOptions(v ...option.ClientOption) ClientOption { + return func(o *Client) { + o.clientOptions = append(o.clientOptions, v...) + } +} + +// ------------------------------------------------------------------------------------------------ +// ~ Constructor +// ------------------------------------------------------------------------------------------------ + +func NewClient(accountID, containerID, workspaceID, measurementID string, opts ...ClientOption) (*Client, error) { + inst := &Client{ + accountID: accountID, + containerID: containerID, + workspaceID: workspaceID, + measurementID: measurementID, + clients: map[string]*tagmanager.Client{}, + folders: map[string]*tagmanager.Folder{}, + variables: map[string]*tagmanager.Variable{}, + triggers: map[string]*tagmanager.Trigger{}, + tags: map[string]*tagmanager.Tag{}, + notes: "Managed by Sesamy. DO NOT EDIT.", + folderName: "Sesamy", + clientOptions: []option.ClientOption{ + //// https://developers.google.com/tag-platform/tag-manager/api/v2/authorization#AboutAuthorization + //option.WithScopes( + // "https://www.googleapis.com/auth/tagmanager.readonly", + // "https://www.googleapis.com/auth/tagmanager.edit.containers", + // // https://www.googleapis.com/auth/tagmanager.delete.containers + // //"https://www.googleapis.com/auth/tagmanager.edit.containerversions", + // //"https://www.googleapis.com/auth/tagmanager.publish", + // // https://www.googleapis.com/auth/tagmanager.manage.users + // // https://www.googleapis.com/auth/tagmanager.manage.accounts + //), + }, + } + + for _, opt := range opts { + opt(inst) + } + + return inst, nil +} + +// ------------------------------------------------------------------------------------------------ +// ~ Getter +// ------------------------------------------------------------------------------------------------ + +func (c *Client) AccountID() string { + return c.accountID +} + +func (c *Client) ContainerID() string { + return c.containerID +} + +func (c *Client) WorkspaceID() string { + return c.workspaceID +} + +func (c *Client) Notes() string { + return c.notes +} + +func (c *Client) MeasurementID() string { + return c.measurementID +} + +func (c *Client) FolderName() string { + return c.folderName +} + +func (c *Client) Service(ctx context.Context) (*tagmanager.Service, error) { + if c.service == nil { + v, err := tagmanager.NewService(ctx, c.clientOptions...) + if err != nil { + return nil, err + } + c.service = v + } + return c.service, nil +} + +func (c *Client) AccountPath() string { + return "accounts/" + c.accountID +} + +func (c *Client) ContainerPath() string { + return c.AccountPath() + "/containers/" + c.containerID +} + +func (c *Client) WorkspacePath() string { + return c.ContainerPath() + "/workspaces/" + c.workspaceID +} + +func (c *Client) Client(ctx context.Context, name string) (*tagmanager.Client, error) { + if _, ok := c.clients[name]; !ok { + client, err := c.Service(ctx) + if err != nil { + return nil, err + } + + r, err := client.Accounts.Containers.Workspaces.Clients.List(c.WorkspacePath()).Do() + if err != nil { + return nil, err + } + + var found bool + for _, value := range r.Client { + if value.Name == name { + c.clients[name] = value + found = true + break + } + } + + if !found { + return nil, ErrNotFound + } + } + return c.clients[name], nil +} + +func (c *Client) Folder(ctx context.Context, name string) (*tagmanager.Folder, error) { + if _, ok := c.folders[name]; !ok { + client, err := c.Service(ctx) + if err != nil { + return nil, err + } + + r, err := client.Accounts.Containers.Workspaces.Folders.List(c.WorkspacePath()).Do() + if err != nil { + return nil, err + } + + var found bool + for _, value := range r.Folder { + if value.Name == name { + c.folders[name] = value + found = true + break + } + } + + if !found { + return nil, ErrNotFound + } + } + return c.folders[name], nil +} + +func (c *Client) Variable(ctx context.Context, name string) (*tagmanager.Variable, error) { + if _, ok := c.variables[name]; !ok { + client, err := c.Service(ctx) + if err != nil { + return nil, err + } + + r, err := client.Accounts.Containers.Workspaces.Variables.List(c.WorkspacePath()).Do() + if err != nil { + return nil, err + } + + var found bool + for _, value := range r.Variable { + if value.Name == name { + c.variables[name] = value + found = true + break + } + } + + if !found { + return nil, ErrNotFound + } + } + return c.variables[name], nil +} + +func (c *Client) Trigger(ctx context.Context, name string) (*tagmanager.Trigger, error) { + if _, ok := c.triggers[name]; !ok { + client, err := c.Service(ctx) + if err != nil { + return nil, err + } + + r, err := client.Accounts.Containers.Workspaces.Triggers.List(c.WorkspacePath()).Do() + if err != nil { + return nil, err + } + + var found bool + for _, value := range r.Trigger { + if value.Name == name { + c.triggers[name] = value + found = true + break + } + } + + if !found { + return nil, ErrNotFound + } + } + return c.triggers[name], nil +} + +func (c *Client) Tag(ctx context.Context, name string) (*tagmanager.Tag, error) { + if _, ok := c.tags[name]; !ok { + client, err := c.Service(ctx) + if err != nil { + return nil, err + } + + r, err := client.Accounts.Containers.Workspaces.Tags.List(c.WorkspacePath()).Do() + if err != nil { + return nil, err + } + + var found bool + for _, value := range r.Tag { + if value.Name == name { + c.tags[name] = value + found = true + break + } + } + + if !found { + return nil, ErrNotFound + } + } + return c.tags[name], nil +} + +// ------------------------------------------------------------------------------------------------ +// ~ Public methods +// ------------------------------------------------------------------------------------------------ + +func (c *Client) UpsertGA4Client(ctx context.Context, name string) (*tagmanager.Client, error) { + cache, err := c.Client(ctx, name) + if err != nil && !errors.Is(err, ErrNotFound) { + return nil, err + } + + s, err := c.Service(ctx) + if err != nil { + return nil, err + } + + folder, err := c.Folder(ctx, c.folderName) + if err != nil { + return nil, err + } + + obj := &tagmanager.Client{ + AccountId: c.accountID, + ContainerId: c.containerID, + WorkspaceId: c.workspaceID, + ParentFolderId: folder.FolderId, + Name: name, + Notes: c.notes, + Parameter: []*tagmanager.Parameter{ + { + Type: "boolean", + Key: "activateGtagSupport", + Value: "false", + }, + { + Type: "boolean", + Key: "activateDefaultPaths", + Value: "true", + }, + { + Type: "template", + Key: "cookieManagement", + Value: "js", + }, + }, + Type: "gaaw_client", + } + + if cache == nil { + c.clients[name], err = s.Accounts.Containers.Workspaces.Clients.Create(c.WorkspacePath(), obj).Do() + } else { + c.clients[name], err = s.Accounts.Containers.Workspaces.Clients.Update(c.WorkspacePath()+"/clients/"+cache.ClientId, obj).Do() + } + if err != nil { + return nil, err + } + + return c.Client(ctx, name) +} + +func (c *Client) UpsertGTMClient(ctx context.Context, name, webContainerMeasurementID string) (*tagmanager.Client, error) { + cache, err := c.Client(ctx, name) + if err != nil && !errors.Is(err, ErrNotFound) { + return nil, err + } + + s, err := c.Service(ctx) + if err != nil { + return nil, err + } + + folder, err := c.Folder(ctx, c.folderName) + if err != nil { + return nil, err + } + + obj := &tagmanager.Client{ + AccountId: c.accountID, + ContainerId: c.containerID, + WorkspaceId: c.workspaceID, + ParentFolderId: folder.FolderId, + Name: name, + Notes: c.notes, + Parameter: []*tagmanager.Parameter{ + { + Type: "boolean", + Key: "activateResponseCompression", + Value: "true", + }, + { + Type: "boolean", + Key: "activateGeoResolution", + Value: "false", + }, + { + Type: "boolean", + Key: "activateDependencyServing", + Value: "true", + }, + { + Type: "list", + Key: "allowedContainerIds", + List: []*tagmanager.Parameter{ + { + Type: "map", + Map: []*tagmanager.Parameter{ + { + Type: "template", + Key: "containerId", + Value: webContainerMeasurementID, + }, + }, + }, + }, + }, + }, + Type: "gtm_client", + } + + if cache == nil { + c.clients[name], err = s.Accounts.Containers.Workspaces.Clients.Create(c.WorkspacePath(), obj).Do() + } else { + c.clients[name], err = s.Accounts.Containers.Workspaces.Clients.Update(c.WorkspacePath()+"/clients/"+cache.ClientId, obj).Do() + } + if err != nil { + return nil, err + } + + return c.Client(ctx, name) +} + +func (c *Client) UpsertFolder(ctx context.Context, name string) (*tagmanager.Folder, error) { + cache, err := c.Folder(ctx, name) + if err != nil && !errors.Is(err, ErrNotFound) && !errors.Is(err, ErrNotFound) { + return nil, err + } + + client, err := c.Service(ctx) + if err != nil { + return nil, err + } + + obj := &tagmanager.Folder{ + AccountId: c.accountID, + ContainerId: c.containerID, + WorkspaceId: c.workspaceID, + Name: name, + Notes: c.notes, + } + + if cache == nil { + c.folders[name], err = client.Accounts.Containers.Workspaces.Folders.Create(c.WorkspacePath(), obj).Do() + } else { + c.folders[name], err = client.Accounts.Containers.Workspaces.Folders.Update(c.WorkspacePath()+"/folders/"+cache.FolderId, obj).Do() + } + if err != nil { + return nil, err + } + + return c.Folder(ctx, name) +} + +func (c *Client) UpsertVariable(ctx context.Context, obj *tagmanager.Variable) (*tagmanager.Variable, error) { + cache, err := c.Variable(ctx, obj.Name) + if err != nil && !errors.Is(err, ErrNotFound) { + return nil, err + } + + client, err := c.Service(ctx) + if err != nil { + return nil, err + } + + folder, err := c.Folder(ctx, c.folderName) + if err != nil { + return nil, err + } + obj.ParentFolderId = folder.FolderId + + if cache == nil { + c.variables[obj.Name], err = client.Accounts.Containers.Workspaces.Variables.Create(c.WorkspacePath(), obj).Do() + } else { + c.variables[obj.Name], err = client.Accounts.Containers.Workspaces.Variables.Update(c.WorkspacePath()+"/variables/"+cache.VariableId, obj).Do() + } + if err != nil { + return nil, err + } + + return c.Variable(ctx, obj.Name) +} + +func (c *Client) UpsertConstantVariable(ctx context.Context, name, value string) (*tagmanager.Variable, error) { + obj := &tagmanager.Variable{ + AccountId: c.accountID, + ContainerId: c.containerID, + WorkspaceId: c.workspaceID, + Name: "Constant." + name, + Notes: c.notes, + Parameter: []*tagmanager.Parameter{ + { + Key: "value", + Type: "template", + Value: value, + }, + }, + Type: "c", + } + return c.UpsertVariable(ctx, obj) +} + +func (c *Client) UpsertEventModelVariable(ctx context.Context, name string) (*tagmanager.Variable, error) { + obj := &tagmanager.Variable{ + AccountId: c.accountID, + ContainerId: c.containerID, + WorkspaceId: c.workspaceID, + Name: "EventModel." + name, + Notes: c.notes, + Parameter: []*tagmanager.Parameter{ + { + Key: "dataLayerVersion", + Type: "integer", + Value: "2", + }, + { + Key: "setDefaultValue", + Type: "boolean", + Value: "false", + }, + { + Key: "name", + Type: "template", + Value: "eventModel." + name, + }, + }, + Type: "v", + } + return c.UpsertVariable(ctx, obj) +} + +func (c *Client) UpsertCustomEventTrigger(ctx context.Context, name string) (*tagmanager.Trigger, error) { + name = "Event." + name + cache, err := c.Trigger(ctx, name) + if err != nil && !errors.Is(err, ErrNotFound) { + return nil, err + } + + client, err := c.Service(ctx) + if err != nil { + return nil, err + } + + folder, err := c.Folder(ctx, c.folderName) + if err != nil { + return nil, err + } + + obj := &tagmanager.Trigger{ + AccountId: c.accountID, + ContainerId: c.containerID, + WorkspaceId: c.workspaceID, + ParentFolderId: folder.FolderId, + Type: "customEvent", + Name: name, + Notes: c.notes, + CustomEventFilter: []*tagmanager.Condition{ + { + Parameter: []*tagmanager.Parameter{ + { + Key: "arg0", + Type: "template", + Value: "{{_event}}", + }, + { + Key: "arg1", + Type: "template", + Value: name, + }, + }, + Type: "equals", + }, + }, + } + + if cache == nil { + c.triggers[name], err = client.Accounts.Containers.Workspaces.Triggers.Create(c.WorkspacePath(), obj).Do() + } else { + c.triggers[name], err = client.Accounts.Containers.Workspaces.Triggers.Update(c.WorkspacePath()+"/triggers/"+cache.TriggerId, obj).Do() + } + if err != nil { + return nil, err + } + + return c.Trigger(ctx, name) +} + +func (c *Client) UpsertClientTrigger(ctx context.Context, name, clientName string) (*tagmanager.Trigger, error) { + name = "Client." + name + cache, err := c.Trigger(ctx, name) + if err != nil && !errors.Is(err, ErrNotFound) { + return nil, err + } + + client, err := c.Service(ctx) + if err != nil { + return nil, err + } + + folder, err := c.Folder(ctx, c.folderName) + if err != nil { + return nil, err + } + + obj := &tagmanager.Trigger{ + AccountId: c.accountID, + ContainerId: c.containerID, + WorkspaceId: c.workspaceID, + ParentFolderId: folder.FolderId, + Type: "always", + Name: name, + Notes: c.notes, + Filter: []*tagmanager.Condition{ + { + Parameter: []*tagmanager.Parameter{ + { + Key: "arg0", + Type: "template", + Value: "{{Client Name}}", + }, + { + Key: "arg1", + Type: "template", + Value: clientName, + }, + }, + Type: "equals", + }, + }, + } + + if cache == nil { + c.triggers[name], err = client.Accounts.Containers.Workspaces.Triggers.Create(c.WorkspacePath(), obj).Do() + } else { + c.triggers[name], err = client.Accounts.Containers.Workspaces.Triggers.Update(c.WorkspacePath()+"/triggers/"+cache.TriggerId, obj).Do() + } + if err != nil { + return nil, err + } + + return c.Trigger(ctx, name) +} + +func (c *Client) UpsertGA4WebTag(ctx context.Context, name string, parameters []string, measurementID *tagmanager.Variable, trigger *tagmanager.Trigger) (*tagmanager.Tag, error) { + name = "GA4." + name + + cache, err := c.Tag(ctx, name) + if err != nil && !errors.Is(err, ErrNotFound) { + return nil, err + } + + client, err := c.Service(ctx) + if err != nil { + return nil, err + } + + folder, err := c.Folder(ctx, c.folderName) + if err != nil { + return nil, err + } + + obj := &tagmanager.Tag{ + AccountId: c.accountID, + ContainerId: c.containerID, + WorkspaceId: c.workspaceID, + FiringTriggerId: []string{trigger.TriggerId}, + ParentFolderId: folder.FolderId, + Name: name, + Notes: c.notes, + Parameter: []*tagmanager.Parameter{ + { + Type: "boolean", + Key: "sendEcommerceData", + Value: "false", + }, + { + Type: "template", + Key: "getEcommerceDataFrom", + Value: "dataLayer", + }, + { + Type: "boolean", + Key: "enhancedUserId", + Value: "false", + }, + { + Type: "template", + Key: "eventName", + Value: trigger.Name, + }, + { + Type: "template", + Key: "measurementIdOverride", + Value: "{{" + measurementID.Name + "}}", + }, + }, + Type: "gaawe", + } + + for _, parameterName := range parameters { + if _, err := c.UpsertEventModelVariable(ctx, parameterName); err != nil { + return nil, err + } + obj.Parameter = append(obj.Parameter, &tagmanager.Parameter{ + Type: "list", + Key: "eventSettingsTable", + List: []*tagmanager.Parameter{ + { + Type: "map", + Map: []*tagmanager.Parameter{ + { + Type: "template", + Key: "parameter", + Value: parameterName, + }, + { + Type: "template", + Key: "parameterValue", + Value: "{{EventModel." + parameterName + "}}", + }, + }, + }, + }, + }) + } + + if cache == nil { + c.tags[name], err = client.Accounts.Containers.Workspaces.Tags.Create(c.WorkspacePath(), obj).Do() + } else { + c.tags[name], err = client.Accounts.Containers.Workspaces.Tags.Update(c.WorkspacePath()+"/tags/"+cache.TagId, obj).Do() + } + if err != nil { + return nil, err + } + + return c.Tag(ctx, name) +} + +func (c *Client) UpsertGA4ServerTag(ctx context.Context, name string, measurementID *tagmanager.Variable, trigger *tagmanager.Trigger) (*tagmanager.Tag, error) { + cache, err := c.Tag(ctx, name) + if err != nil && !errors.Is(err, ErrNotFound) { + return nil, err + } + + client, err := c.Service(ctx) + if err != nil { + return nil, err + } + + folder, err := c.Folder(ctx, c.folderName) + if err != nil { + return nil, err + } + + obj := &tagmanager.Tag{ + AccountId: c.accountID, + ContainerId: c.containerID, + WorkspaceId: c.workspaceID, + FiringTriggerId: []string{trigger.TriggerId}, + ParentFolderId: folder.FolderId, + Name: name, + Notes: c.notes, + Parameter: []*tagmanager.Parameter{ + { + Type: "boolean", + Key: "redactVisitorIp", + Value: "false", + }, + { + Type: "template", + Key: "epToIncludeDropdown", + Value: "all", + }, + { + Type: "boolean", + Key: "enableGoogleSignals", + Value: "false", + }, + { + Type: "template", + Key: "upToIncludeDropdown", + Value: "all", + }, + { + Type: "template", + Key: "measurementId", + Value: "{{" + measurementID.Name + "}}", + }, + { + Type: "boolean", + Key: "enableEuid", + Value: "false", + }, + }, + Type: "sgtmgaaw", + } + + if cache == nil { + c.tags[name], err = client.Accounts.Containers.Workspaces.Tags.Create(c.WorkspacePath(), obj).Do() + } else { + c.tags[name], err = client.Accounts.Containers.Workspaces.Tags.Update(c.WorkspacePath()+"/tags/"+cache.TagId, obj).Do() + } + if err != nil { + return nil, err + } + + return c.Tag(ctx, name) +} diff --git a/pkg/tagmanager/client_test.go b/pkg/tagmanager/client_test.go new file mode 100644 index 0000000..e9553a0 --- /dev/null +++ b/pkg/tagmanager/client_test.go @@ -0,0 +1,420 @@ +package tagmanager_test + +import ( + "bytes" + "context" + "encoding/json" + "os" + "reflect" + "strings" + "testing" + + "github.com/foomo/sesamy-cli/pkg/tagmanager" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/api/option" + gtagmanager "google.golang.org/api/tagmanager/v2" +) + +func TestNewClient_Server(t *testing.T) { + t.Skip() + ctx := context.TODO() + + c, err := tagmanager.NewClient( + os.Getenv("TEST_ACCOUNT_ID"), + os.Getenv("TEST_SERVER_CONTAINER_ID"), + os.Getenv("TEST_SERVER_WORKSPACE_ID"), + os.Getenv("TEST_MEASUREMENT_ID"), + tagmanager.ClientWithClientOptions( + option.WithCredentialsFile(os.Getenv("TEST_CREDENTIALS_FILE")), + ), + ) + require.NoError(t, err) + + { // --- Folders --- + t.Run("upsert folder", func(t *testing.T) { + obj, err := c.UpsertFolder(ctx, "Sesamy") + require.NoError(t, err) + dump(t, obj) + }) + } + + { // --- Variables --- + t.Run("upsert GTM client", func(t *testing.T) { + client, err := c.UpsertConstantVariable(ctx, "web-container-id", os.Getenv("TEST_WEB_CONTAINER_GID")) + if assert.NoError(t, err) { + dump(t, client) + } + }) + } + + { // --- Clients --- + t.Run("list clients", func(t *testing.T) { + client, err := c.Service(ctx) + require.NoError(t, err) + cmd := client.Accounts.Containers.Workspaces.Clients.List(c.WorkspacePath()) + if r, err := cmd.Do(); assert.NoError(t, err) { + dump(t, r) + } + }) + + t.Run("upsert GTM client", func(t *testing.T) { + client, err := c.UpsertGTMClient(ctx, "Google Tag Manager Web Container", "Constant.web-container-id") + if assert.NoError(t, err) { + dump(t, client) + } + }) + } + + { // --- Triggers --- + t.Run("list triggers", func(t *testing.T) { + client, err := c.Service(ctx) + require.NoError(t, err) + cmd := client.Accounts.Containers.Workspaces.Triggers.List(c.WorkspacePath()) + if r, err := cmd.Do(); assert.NoError(t, err) { + dump(t, r) + } + }) + } + + { // --- Tags --- + t.Run("list tags", func(t *testing.T) { + client, err := c.Service(ctx) + require.NoError(t, err) + cmd := client.Accounts.Containers.Workspaces.Tags.List(c.WorkspacePath()) + if r, err := cmd.Do(); assert.NoError(t, err) { + dump(t, r) + } + }) + } +} + +func TestNewClient_Web(t *testing.T) { + t.Skip() + ctx := context.TODO() + + c, err := tagmanager.NewClient( + os.Getenv("TEST_ACCOUNT_ID"), + os.Getenv("TEST_WEB_CONTAINER_ID"), + os.Getenv("TEST_WEB_WORKSPACE_ID"), + os.Getenv("TEST_MEASUREMENT_ID"), + tagmanager.ClientWithClientOptions( + option.WithCredentialsFile(os.Getenv("TEST_CREDENTIALS_FILE")), + ), + ) + require.NoError(t, err) + + { // --- Containers --- + t.Run("list containers", func(t *testing.T) { + client, err := c.Service(ctx) + require.NoError(t, err) + cmd := client.Accounts.Containers.List(c.AccountPath()) + if r, err := cmd.Do(); assert.NoError(t, err) { + dump(t, r) + } + }) + } + + { // --- Workspaces --- + t.Run("list workspaces", func(t *testing.T) { + client, err := c.Service(ctx) + require.NoError(t, err) + cmd := client.Accounts.Containers.Workspaces.List(c.ContainerPath()) + if r, err := cmd.Do(); assert.NoError(t, err) { + dump(t, r) + } + }) + } + + folderID := "25" + { // --- Folders --- + name := "Sesamy" + t.Run("list folders", func(t *testing.T) { + client, err := c.Service(ctx) + require.NoError(t, err) + cmd := client.Accounts.Containers.Workspaces.Folders.List(c.WorkspacePath()) + if r, err := cmd.Do(); assert.NoError(t, err) { + dump(t, r) + } + }) + + t.Run("create folder", func(t *testing.T) { + client, err := c.Service(ctx) + require.NoError(t, err) + cmd := client.Accounts.Containers.Workspaces.Folders.Create(c.WorkspacePath(), >agmanager.Folder{ + AccountId: c.AccountID(), + ContainerId: c.ContainerID(), + WorkspaceId: c.WorkspaceID(), + Name: name, + Notes: c.Notes(), + }) + if r, err := cmd.Do(); assert.NoError(t, err) { + dump(t, r) + } + }) + + t.Run("get folder", func(t *testing.T) { + client, err := c.Service(ctx) + require.NoError(t, err) + cmd := client.Accounts.Containers.Workspaces.Folders.List(c.WorkspacePath()) + if r, err := cmd.Do(); assert.NoError(t, err) { + for _, folder := range r.Folder { + if folder.Name == name { + t.Log("ID: " + folder.FolderId) + return + } + } + t.Error("not found") + } + }) + + t.Run("upsert folder", func(t *testing.T) { + obj, err := c.UpsertFolder(ctx, name) + require.NoError(t, err) + t.Log("ID: " + obj.FolderId) + }) + } + + { // --- Variables --- + name := "ga4-measurement-id" + t.Run("list variables", func(t *testing.T) { + client, err := c.Service(ctx) + require.NoError(t, err) + cmd := client.Accounts.Containers.Workspaces.Variables.List(c.WorkspacePath()) + if r, err := cmd.Do(); assert.NoError(t, err) { + dump(t, r) + } + }) + + t.Run("create variable", func(t *testing.T) { + client, err := c.Service(ctx) + require.NoError(t, err) + cmd := client.Accounts.Containers.Workspaces.Variables.Create(c.WorkspacePath(), >agmanager.Variable{ + AccountId: c.AccountID(), + ContainerId: c.ContainerID(), + WorkspaceId: c.WorkspaceID(), + ParentFolderId: folderID, + Name: "Constant." + name, + Notes: c.Notes(), + Parameter: []*gtagmanager.Parameter{ + { + Key: "value", + Type: "template", + Value: c.MeasurementID(), + }, + }, + Type: "c", + }) + if r, err := cmd.Do(); assert.NoError(t, err) { + dump(t, r) + } + }) + + t.Run("get variable", func(t *testing.T) { + client, err := c.Service(ctx) + require.NoError(t, err) + cmd := client.Accounts.Containers.Workspaces.Variables.List(c.WorkspacePath()) + if r, err := cmd.Do(); assert.NoError(t, err) { + for _, variable := range r.Variable { + if variable.Name == "Constant."+name { + t.Log("ID: " + variable.VariableId) + return + } + } + t.Error("not found") + } + }) + + t.Run("upsert variable", func(t *testing.T) { + obj, err := c.UpsertConstantVariable(ctx, name, c.MeasurementID()) + require.NoError(t, err) + t.Log("ID: " + obj.VariableId) + }) + } + + { // --- Triggers --- + name := "login" + t.Run("list triggers", func(t *testing.T) { + client, err := c.Service(ctx) + require.NoError(t, err) + cmd := client.Accounts.Containers.Workspaces.Triggers.List(c.WorkspacePath()) + if r, err := cmd.Do(); assert.NoError(t, err) { + dump(t, r) + } + }) + + t.Run("create trigger", func(t *testing.T) { + client, err := c.Service(ctx) + require.NoError(t, err) + cmd := client.Accounts.Containers.Workspaces.Triggers.Create(c.WorkspacePath(), >agmanager.Trigger{ + AccountId: c.AccountID(), + ContainerId: c.ContainerID(), + WorkspaceId: c.WorkspaceID(), + ParentFolderId: folderID, + Type: "customEvent", + Name: "Event." + name, + Notes: c.Notes(), + CustomEventFilter: []*gtagmanager.Condition{ + { + Parameter: []*gtagmanager.Parameter{ + { + Key: "arg0", + Type: "template", + Value: "{{_event}}", + }, + { + Key: "arg1", + Type: "template", + Value: name, + }, + }, + Type: "equals", + }, + }, + }) + if r, err := cmd.Do(); assert.NoError(t, err) { + dump(t, r) + } + }) + + t.Run("get trigger", func(t *testing.T) { + client, err := c.Service(ctx) + require.NoError(t, err) + cmd := client.Accounts.Containers.Workspaces.Triggers.List(c.WorkspacePath()) + if r, err := cmd.Do(); assert.NoError(t, err) { + for _, trigger := range r.Trigger { + if trigger.Name == "Event."+name { + t.Log("ID: " + trigger.TriggerId) + return + } + } + t.Error("not found") + } + }) + + t.Run("upsert trigger", func(t *testing.T) { + obj, err := c.UpsertCustomEventTrigger(ctx, name) + require.NoError(t, err) + t.Log("ID: " + obj.TriggerId) + }) + } + + { // --- Tags --- + name := "login" + t.Run("list tags", func(t *testing.T) { + client, err := c.Service(ctx) + require.NoError(t, err) + cmd := client.Accounts.Containers.Workspaces.Tags.List(c.WorkspacePath()) + if r, err := cmd.Do(); assert.NoError(t, err) { + dump(t, r) + } + }) + + t.Run("create tag", func(t *testing.T) { + client, err := c.Service(ctx) + require.NoError(t, err) + cmd := client.Accounts.Containers.Workspaces.Tags.Create(c.WorkspacePath(), >agmanager.Tag{ + AccountId: c.AccountID(), + ContainerId: c.ContainerID(), + WorkspaceId: c.WorkspaceID(), + ParentFolderId: folderID, + Name: "GA4." + name, + Notes: c.Notes(), + Parameter: []*gtagmanager.Parameter{ + { + Type: "boolean", + Key: "sendEcommerceData", + Value: "true", + }, + { + Type: "template", + Key: "getEcommerceDataFrom", + Value: "dataLayer", + }, + { + Type: "boolean", + Key: "enhancedUserId", + Value: "false", + }, + { + Type: "template", + Key: "eventName", + Value: "Event." + name, + }, + { + Type: "template", + Key: "measurementIdOverride", + Value: "{{GA Measurement ID}}", + }, + { + Type: "list", + Key: "eventSettingsTable", + List: []*gtagmanager.Parameter{ + { + Type: "map", + Map: []*gtagmanager.Parameter{ + { + Type: "parameter", + Key: "template", + Value: "method", + }, + { + Type: "parameterValue", + Key: "template", + Value: "{{EventModel.method}}", + }, + }, + }, + }, + }, + }, + Type: "gaawe", + }) + if r, err := cmd.Do(); assert.NoError(t, err) { + dump(t, r) + } + }) + + type Login struct { + Method string `json:"method"` + } + + //t.Run("upsert tag", func(t *testing.T) { + // obj, err := c.UpsertGA4WebTag(ctx, "login", eventParameters(Login{})) + // require.NoError(t, err) + // t.Log("ID: " + obj.TagId) + //}) + } +} + +// ------------------------------------------------------------------------------------------------ +// ~ Private methods +// ------------------------------------------------------------------------------------------------ + +func eventParameters(event interface{}) []string { + if event == nil { + return nil + } + var res []string + v := reflect.TypeOf(event) + + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + for i := 0; i < v.NumField(); i++ { + tag := v.Field(i).Tag.Get("json") + if tag != "" && tag != "-" { + res = append(res, strings.Split(tag, ",")[0]) + } + } + return res +} + +func dump(t *testing.T, i interface{ MarshalJSON() ([]byte, error) }) { + t.Helper() + var ret bytes.Buffer + out, err := i.MarshalJSON() + require.NoError(t, err) + require.NoError(t, json.Indent(&ret, out, "", " ")) + t.Log(ret.String()) +}