diff --git a/.github/.goreleaser.yaml b/.github/.goreleaser.yaml index d2336af..0f051c6 100644 --- a/.github/.goreleaser.yaml +++ b/.github/.goreleaser.yaml @@ -25,6 +25,8 @@ builds: goarch: - amd64 - arm64 + hooks: + post: "go run main.go update latest" changelog: use: git groups: @@ -45,7 +47,8 @@ dockers: goos: linux goarch: amd64 image_templates: - - "ghcr.io/{{ .Env.GITHUB_ORG }}/{{ .ProjectName }}:{{ .Version }}-amd64" + - "ghcr.io/{{ .Env.GITHUB_ORG }}/{{ .ProjectName }}:latest" + - "ghcr.io/{{ .Env.GITHUB_ORG }}/{{ .ProjectName }}:{{ .Version }}" build_flag_templates: - "--pull" - "--platform=linux/amd64" @@ -54,40 +57,4 @@ dockers: - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" extra_files: - - tzdata - - use: buildx - goos: linux - goarch: arm64 - image_templates: - - "ghcr.io/{{ .Env.GITHUB_ORG }}/{{ .ProjectName }}:{{ .Version }}-arm64v8" - build_flag_templates: - - "--pull" - - "--platform=linux/arm64" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - extra_files: - - tzdata -docker_manifests: - - name_template: ghcr.io/{{ .Env.GITHUB_ORG }}/{{ .ProjectName }}:{{ .Version }} - image_templates: - - ghcr.io/{{ .Env.GITHUB_ORG }}/{{ .ProjectName }}:{{ .Version }}-amd64 - - ghcr.io/{{ .Env.GITHUB_ORG }}/{{ .ProjectName }}:{{ .Version }}-arm64v8 -# nfpms: -# - id: packages -# maintainer: -# # Formats to be generated. -# formats: -# - deb -# - rpm -# # contents: -# # # for updstart -# # - src: path/to/local/bar.conf -# # dst: /etc/bar.conf -# # type: "config|noreplace" -# # # for logging -# # - dst: /some/dir -# # type: dir -# # file_info: -# # mode: 0700 + - tzdata \ No newline at end of file diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..e5e4b08 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,9 @@ +version: 2 +updates: + # Enable version updates for github-actions + - package-ecosystem: "github-actions" + # Look for a `Dockerfile` in the `root` directory + directory: "/" + # Check for updates once a month + schedule: + interval: "monthly" \ No newline at end of file diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 53de02d..fcc5983 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -15,7 +15,7 @@ jobs: fetch-depth: 2 - uses: actions/setup-go@v2 with: - go-version: "1.18" + go-version: "1.22" - name: Run coverage run: make test-ci - name: Upload coverage to Codecov @@ -26,10 +26,10 @@ jobs: steps: - uses: actions/setup-go@v3 with: - go-version: 1.18 + go-version: 1.21 - uses: actions/checkout@v3 - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version - version: latest \ No newline at end of file + version: latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5856398..0adc6d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,10 +7,6 @@ on: - "LICENSE" - "docs" - "Makefile" - branches: - - "main" - tags: - - "v*" workflow_dispatch: permissions: diff --git a/.gitignore b/.gitignore index 158494b..40c0532 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ +geo2tz dist -release \ No newline at end of file +release + +coverage.* +*.zip \ No newline at end of file diff --git a/Makefile b/Makefile index a06ffca..03c98b1 100644 --- a/Makefile +++ b/Makefile @@ -27,9 +27,14 @@ endif test: test-all test-all: - @go test -v $(GOPACKAGES) -coverprofile .testCoverage.txt + @go test -v $(GOPACKAGES) -race -covermode=atomic -coverprofile coverage.txt + +test-coverage: + go test -mod=readonly -coverprofile=coverage.out -covermode=atomic -timeout 30s $(GOPACKAGES) && \ + go tool cover -html=coverage.out test-ci: + go run main.go update current go test -coverprofile=coverage.txt -covermode=atomic -race -mod=readonly $(GOPACKAGES) bench: bench-all @@ -43,7 +48,7 @@ go.sum: go.mod lint: @echo "--> Running linter" - @golangci-lint run --config .github/.golangci.yaml + golangci-lint run --config .github/.golangci.yaml @go mod verify debug-start: @@ -63,6 +68,6 @@ k8s-rollback: update-tzdata: @echo "--> Updating timzaone data" @echo build binary - goreleaser build --single-target --config .github/.goreleaser.yaml --snapshot --clean -o dist/geo2tz - ./scripts/update-tzdata.sh + goreleaser build --single-target --config .github/.goreleaser.yaml --snapshot --clean -o geo2tz + ./geo2tz update latest @echo done diff --git a/README.md b/README.md index 22f5877..54d7dbd 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ # Geo2Tz -[![Build Status](https://travis-ci.com/noandrea/geo2tz.svg?branch=master)](https://travis-ci.com/noandrea/geo2tz) [![GoDoc](https://godoc.org/github.com/noandrea/geo2tz?status.svg)](https://godoc.org/github.com/noandrea/geo2tz) [![Go Report Card](https://goreportcard.com/badge/github.com/noandrea/geo2tz)](https://goreportcard.com/report/github.com/noandrea/geo2tz) +[![QA](https://github.com/noandrea/geo2tz/actions/workflows/quality.yml/badge.svg)](https://github.com/noandrea/geo2tz/actions/workflows/quality.yml) [![GoDoc](https://godoc.org/github.com/noandrea/geo2tz?status.svg)](https://godoc.org/github.com/noandrea/geo2tz) [![Go Report Card](https://goreportcard.com/badge/github.com/noandrea/geo2tz)](https://goreportcard.com/report/github.com/noandrea/geo2tz) -A self-host-able service to get the timezone given geo-coordinates (lat/long) - -It does it by exposing the library from [github.com/evanoberholster/timezoneLookup](https://github.com/evanoberholster/timezoneLookup) +A self-host-able service to get the timezone given geo-coordinates (lat/lng) Timezone data comes from [github.com/evansiroky/timezone-boundary-builder](https://github.com/evansiroky/timezone-boundary-builder) (release [2023b](https://github.com/evansiroky/timezone-boundary-builder/releases/tag/2023b)) @@ -217,8 +215,26 @@ spec: ## Development notes -To update the timezone database, set the version of the database in the `scripts/update-tzdata.sh` script and run: +To update the timezone database you have a few options: + +1. update to the latest version + +```console +geo2tz update latest +``` + +2. update to a specific version ```console -make update-tzdata +geo2tz update 2023b ``` + + +the `update` command will download the timezone geojson zip and generate a version file in the `tzdata` directory, the version file is used to track the current version of the database. + + +## Tests and known issues + +Automated tests are executed for coordinates with a precision between 1.11m (4 decimals) and 11.1m (5 decimals), the test file is `db/testdata/coordinates.json`. + +Some of the location are marked with `err` since they produce incorrect results, this is due to the fact that the timezone boundaries often overlaps. If you have additional test cases that you think are relevant, please open an issue or a PR. diff --git a/cmd/build.go b/cmd/build.go deleted file mode 100644 index 0c7cd6f..0000000 --- a/cmd/build.go +++ /dev/null @@ -1,91 +0,0 @@ -/* -Copyright © 2021 NAME HERE - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package cmd - -import ( - "fmt" - "os" - - "github.com/evanoberholster/timezoneLookup" - "github.com/spf13/cobra" -) - -// buildCmd represents the build command -var buildCmd = &cobra.Command{ - Use: "build", - Short: "Build the location database", - Long: `The commands replicates the functionality of the evanoberholster/timezoneLookup timezone command`, - Run: build, -} - -var ( - // geo data url - GeoDataURL = "https://github.com/evansiroky/timezone-boundary-builder/releases/download/2022b/timezones-with-oceans.geojson.zip" - // cli parameters. - snappy bool - jsonFilename string - dbFilename string -) - -func init() { - rootCmd.AddCommand(buildCmd) - buildCmd.Flags().StringVar(&dbFilename, "db", "timezone", "Destination database filename") - buildCmd.Flags().BoolVar(&snappy, "snappy", true, "Use Snappy compression (true/false)") - buildCmd.Flags().StringVar(&jsonFilename, "json", "combined-with-oceans.json", "GEOJSON Filename") -} - -func build(*cobra.Command, []string) { - if dbFilename == "" || jsonFilename == "" { - fmt.Printf(`Options: - -snappy=true Use Snappy compression - -json=filename GEOJSON filename - -db=filename Database destination -`) - return - } - - tz := timezoneLookup.MemoryStorage(snappy, dbFilename) - - if !fileExists(jsonFilename) { - fmt.Printf("json file %v does not exists, will try to download from the source", jsonFilename) - return - } - - if jsonFilename != "" { - err := tz.CreateTimezones(jsonFilename) - if err != nil { - fmt.Println(err) - return - } - } else { - fmt.Println(`"--json" No GeoJSON source file specified`) - return - } - - tz.Close() - -} - -func fileExists(filePath string) bool { - f, err := os.Stat(filePath) - if err != nil { - return false - } - if f.IsDir() { - return false - } - return true -} diff --git a/cmd/root.go b/cmd/root.go index b586cb7..d5b64d2 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,15 +5,21 @@ import ( "log" "strings" + "github.com/noandrea/geo2tz/v2/web" "github.com/spf13/cobra" "github.com/spf13/viper" - - "github.com/noandrea/geo2tz/v2/server" ) var cfgFile string var debug bool -var settings server.ConfigSchema +var settings web.ConfigSchema + +type RuntimeVersion struct { + Version string + Commit string + Date string + BuiltBy string +} // rootCmd represents the base command when called without any subcommands. var rootCmd = &cobra.Command{ @@ -25,8 +31,8 @@ var rootCmd = &cobra.Command{ // 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(v string) error { - rootCmd.Version = v +func Execute(v RuntimeVersion) error { + rootCmd.Version = v.Version if err := initConfig(); err != nil { return err @@ -50,7 +56,7 @@ func initConfig() error { viper.AddConfigPath("/etc/geo2tz") viper.SetConfigName("config") } - server.Defaults() + web.Defaults() viper.SetEnvPrefix("GEO2TZ") viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) viper.AutomaticEnv() // read in environment variables that match diff --git a/cmd/start.go b/cmd/start.go index 75d13d6..39eefd7 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -8,7 +8,7 @@ import ( "github.com/spf13/cobra" - "github.com/noandrea/geo2tz/v2/server" + "github.com/noandrea/geo2tz/v2/web" ) // startCmd represents the start command. @@ -33,10 +33,15 @@ func start(*cobra.Command, []string) { \_____|\___|\___/____|\__/___| version %s `, rootCmd.Version) // Start server + server, err := web.NewServer(settings) + if err != nil { + log.Println("Error creating the server ", err) + os.Exit(1) + } go func() { - if err := server.Start(settings); err != nil { + if err = server.Start(); err != nil { log.Println("Error starting the server ", err) - return + os.Exit(1) } }() @@ -46,7 +51,7 @@ func start(*cobra.Command, []string) { quit := make(chan os.Signal, signalChannelLength) signal.Notify(quit, os.Interrupt) <-quit - if err := server.Teardown(); err != nil { + if err = server.Teardown(); err != nil { log.Println("error stopping server: ", err) } fmt.Print("Goodbye") diff --git a/cmd/update.go b/cmd/update.go new file mode 100644 index 0000000..94698b2 --- /dev/null +++ b/cmd/update.go @@ -0,0 +1,132 @@ +/* +Copyright © 2021 NAME HERE + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cmd + +import ( + "fmt" + "io" + "net/http" + "os" + "strings" + "time" + + "github.com/noandrea/geo2tz/v2/helpers" + "github.com/noandrea/geo2tz/v2/web" + "github.com/spf13/cobra" +) + +const ( + // geo data url + LatestReleaseURL = "https://github.com/evansiroky/timezone-boundary-builder/releases/latest" +) + +// updateCmd represents the build command +var updateCmd = &cobra.Command{ + Use: "update VERSION", + Short: "Download the timezone data from the latest release or a specific version", + Example: `To update using the tzdata/version.json file: +geo2tz update current + +To update to the latest version: +geo2tz update latest + +To update to a specific version: +geo2tz update 2023d +`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + versionName := args[0] + return update(versionName, web.Settings.Tz.DatabaseName) + }, +} + +func init() { + rootCmd.AddCommand(updateCmd) + updateCmd.Flags().StringVar(&web.Settings.Tz.DatabaseName, "db", web.TZDBFile, "Destination database filename") + updateCmd.Flags().StringVar(&web.Settings.Tz.VersionFile, "version-file", web.TZVersionFile, "Version file") +} + +func update(versionName, targetFile string) (err error) { + var release = web.NewTzRelease(versionName) + // do we need the latest version? + if versionName == "latest" { + release, err = getLatest() + if err != nil { + return + } + } + // shall we read from the version file? + if versionName == "current" { + err = helpers.LoadJSON(web.Settings.Tz.VersionFile, &release) + if err != nil { + return + } + println("Current version is", release.Version) + } + if err := fetchAndCacheFile(targetFile, release.GeoDataURL); err != nil { + return err + } + err = helpers.SaveJSON(release, web.Settings.Tz.VersionFile) + return +} + +func fetchAndCacheFile(filename string, url string) (err error) { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + f, err := os.Create(filename) + if err != nil { + return err + } + defer f.Close() + + n, err := io.Copy(f, resp.Body) + if err != nil { + return err + } + if n != resp.ContentLength { + fmt.Println(n, resp.ContentLength) + } + return +} + +func getLatest() (web.TzRelease, error) { + // create http client + client := &http.Client{ + Timeout: 1 * time.Second, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + // don't follow redirects + return http.ErrUseLastResponse + }, + } + r, err := client.Head(LatestReleaseURL) + if err != nil { + err = fmt.Errorf("failed to get release url: %w", err) + return web.TzRelease{}, err + } + defer r.Body.Close() + // get the tag name + releaseURL, err := r.Location() + if err != nil { + err = fmt.Errorf("failed to get release url: %w", err) + return web.TzRelease{}, err + } + v := web.NewTzRelease(releaseURL.Path[strings.LastIndex(releaseURL.Path, "/")+1:]) + return v, nil +} diff --git a/db/db.go b/db/db.go new file mode 100644 index 0000000..8ecbb78 --- /dev/null +++ b/db/db.go @@ -0,0 +1,14 @@ +package db + +import "errors" + +type TzDBIndex interface { + Lookup(lat, lon float64) (string, error) + Size() int +} + +var ( + // ErrNotFound is returned when a timezone is not found + ErrNotFound = errors.New("timezone not found") + ErrInternal = errors.New("shape without tzID") +) diff --git a/db/rtree.go b/db/rtree.go new file mode 100644 index 0000000..5be4e70 --- /dev/null +++ b/db/rtree.go @@ -0,0 +1,228 @@ +package db + +import ( + "encoding/json" + "errors" + "io" + "strings" + + "github.com/klauspost/compress/zip" + "github.com/tidwall/rtree" +) + +type Geo2TzRTreeIndex struct { + land rtree.RTreeG[string] + sea rtree.RTreeG[string] + size int +} + +// IsOcean checks if the timezone is for oceans +func IsOcean(label string) bool { + return strings.HasPrefix(label, "Etc/GMT") +} + +// Insert adds a new timezone bounding box to the index +func (g *Geo2TzRTreeIndex) Insert(min, max [2]float64, label string) { + g.size++ + if IsOcean(label) { + g.sea.Insert(min, max, label) + return + } + g.land.Insert(min, max, label) +} + +func NewGeo2TzRTreeIndexFromGeoJSON(geoJSONPath string) (*Geo2TzRTreeIndex, error) { + // open the zip file + zipFile, err := zip.OpenReader(geoJSONPath) + if err != nil { + return nil, err + } + defer zipFile.Close() + + // create a new shape index + gri := &Geo2TzRTreeIndex{} + + // this function will add the timezone polygons to the shape index + iter := func(tz timezoneGeo) error { + for _, p := range tz.Polygons { + minLat, minLng, maxLat, maxLng := p.Vertices[0].lat, p.Vertices[0].lng, p.Vertices[0].lat, p.Vertices[0].lng + for _, v := range p.Vertices { + if v.lng < minLng { + minLng = v.lng + } + if v.lng > maxLng { + maxLng = v.lng + } + if v.lat < minLat { + minLat = v.lat + } + if v.lat > maxLat { + maxLat = v.lat + } + } + gri.Insert([2]float64{minLat, minLng}, [2]float64{maxLat, maxLng}, tz.Name) + } + return nil + } + // iterate over the zip file + for _, v := range zipFile.File { + if strings.EqualFold(".json", v.Name[len(v.Name)-5:]) { + if err := decodeJSON(v, iter); err != nil { + return nil, err + } + } + } + // build the shape index + return gri, nil +} + +// Lookup returns the timezone ID for a given latitude and longitude +// if the timezone is not found, it returns an error +// It first searches in the land index, if not found, it searches in the sea index +func (g *Geo2TzRTreeIndex) Lookup(lat, lng float64) (tzID string, err error) { + + g.land.Search( + [2]float64{lat, lng}, + [2]float64{lat, lng}, + func(min, max [2]float64, label string) bool { + tzID = label + return true + }, + ) + + if tzID == "" { + g.sea.Search( + [2]float64{lat, lng}, + [2]float64{lat, lng}, + func(min, max [2]float64, label string) bool { + tzID = label + return true + }, + ) + } + + if tzID == "" { + err = ErrNotFound + } + return +} + +func (g *Geo2TzRTreeIndex) Size() int { + return g.size +} + +/* +GeoJSON processing +*/ + +// Polygon represents a polygon +// with a list of vertices [lat, lng] +type polygon struct { + Vertices []vertex +} + +type vertex struct { + lat, lng float64 +} + +type GeoJSONFeature struct { + Type string `json:"type"` + Properties struct { + TzID string `json:"tzid"` + } `json:"properties"` + Geometry struct { + Item string `json:"type"` + Coordinates []interface{} `json:"coordinates"` + } `json:"geometry"` +} + +func (p *polygon) AddVertex(lat, lng float64) { + p.Vertices = append(p.Vertices, vertex{lat, lng}) +} + +type timezoneGeo struct { + Name string + Polygons []polygon +} + +func decodeJSON(f *zip.File, iter func(tz timezoneGeo) error) (err error) { + var rc io.ReadCloser + if rc, err = f.Open(); err != nil { + return err + } + defer rc.Close() + + dec := json.NewDecoder(rc) + + var token json.Token + for dec.More() { + if token, err = dec.Token(); err != nil { + break + } + if t, ok := token.(string); ok && t == "features" { + if token, err = dec.Token(); err == nil && token.(json.Delim) == '[' { + return decodeFeatures(dec, iter) // decode features + } + } + } + return errors.New("error no features found") +} + +func decodeFeatures(dec *json.Decoder, fn func(tz timezoneGeo) error) error { + var f GeoJSONFeature + var err error + + for dec.More() { + if err = dec.Decode(&f); err != nil { + return err + } + var pp []polygon + switch f.Geometry.Item { + case "Polygon": + pp = decodePolygons(f.Geometry.Coordinates) + case "MultiPolygon": + pp = decodeMultiPolygons(f.Geometry.Coordinates) + } + if err = fn(timezoneGeo{Name: f.Properties.TzID, Polygons: pp}); err != nil { + return err + } + } + + return nil +} + +// decodePolygons +// GeoJSON Spec https://geojson.org/geojson-spec.html +// Coordinates: [Longitude, Latitude] +func decodePolygons(polygons []interface{}) []polygon { + var pp []polygon + for _, points := range polygons { + p := polygon{} + for _, i := range points.([]interface{}) { + if latlng, ok := i.([]interface{}); ok { + p.AddVertex(latlng[1].(float64), latlng[0].(float64)) + } + } + pp = append(pp, p) + } + return pp +} + +// decodeMultiPolygons +// GeoJSON Spec https://geojson.org/geojson-spec.html +// Coordinates: [Longitude, Latitude] +func decodeMultiPolygons(polygons []interface{}) []polygon { + var pp []polygon + for _, v := range polygons { + p := polygon{} + for _, points := range v.([]interface{}) { // 2 + for _, i := range points.([]interface{}) { + if latlng, ok := i.([]interface{}); ok { + p.AddVertex(latlng[1].(float64), latlng[0].(float64)) + } + } + } + pp = append(pp, p) + } + return pp +} diff --git a/db/rtree_test.go b/db/rtree_test.go new file mode 100644 index 0000000..1765fd0 --- /dev/null +++ b/db/rtree_test.go @@ -0,0 +1,75 @@ +package db + +import ( + "testing" + + "github.com/noandrea/geo2tz/v2/helpers" + "github.com/stretchr/testify/assert" +) + +// TestGeo2TzTreeIndex_LookupZone tests the LookupZone function +// since the timezone is not always the same as the expected one, we need to check the reference timezone +func TestGeo2TzTreeIndex_LookupZone(t *testing.T) { + var tests []struct { + Tz string `json:"tz"` + Lat float64 `json:"lat"` + Lon float64 `json:"lon"` + HasError bool `json:"err,omitempty"` + } + + // load the database + gsi, err := NewGeo2TzRTreeIndexFromGeoJSON("../tzdata/timezones.zip") + assert.NoError(t, err) + assert.NotEmpty(t, gsi.Size()) + + // load the timezone references + var tzZones map[string]struct { + Zone string `json:"zone"` + UtcOffset float32 `json:"utc_offset_h"` + Dst struct { + Start string `json:"start"` + End string `json:"end"` + Zone string `json:"zone"` + UtcOffset float32 `json:"utc_offset_h"` + } `json:"dst,omitempty"` + } + err = helpers.LoadJSON("testdata/zones.json", &tzZones) + assert.NoError(t, err) + assert.NotEmpty(t, tzZones) + + // load the coordinates + err = helpers.LoadJSON("testdata/coordinates.json", &tests) + assert.NoError(t, err) + assert.NotEmpty(t, tests) + + for _, tt := range tests { + t.Run(tt.Tz, func(t *testing.T) { + got, err := gsi.Lookup(tt.Lat, tt.Lon) + assert.NoError(t, err) + + if tt.HasError { + t.Skip("skipping test as it is expected to fail (know error)") + } + + // for oceans do exact match + if IsOcean(got) { + assert.Equal(t, tt.Tz, got, "expected %s to be %s for https://www.google.com/maps/@%v,%v,12z", tt.Tz, got, tt.Lat, tt.Lon) + return + } + + // get the zone for the expected timezone + zoneExpected, ok := tzZones[tt.Tz] + assert.True(t, ok, "timezone %s not found in zones.json", tt.Tz) + + // get the reference timezone for the expected timezone + zoneGot, ok := tzZones[got] + assert.True(t, ok, "timezone %s not found in zones.json", got) + + if !ok { + assert.Equal(t, zoneExpected.Zone, got, "expected %s (%s) to be %s (%s) for https://www.google.com/maps/@%v,%v,12z", tt.Tz, zoneExpected.Zone, got, zoneGot.Zone, tt.Lat, tt.Lon) + } else { + assert.Equal(t, zoneExpected.Zone, zoneGot.Zone, "expected %s (%s) to be %s (%s) for https://www.google.com/maps/@%v,%v,12z", tt.Tz, zoneExpected.Zone, got, zoneGot.Zone, tt.Lat, tt.Lon) + } + }) + } +} diff --git a/db/testdata/coordinates.json b/db/testdata/coordinates.json new file mode 100755 index 0000000..ce9cc25 --- /dev/null +++ b/db/testdata/coordinates.json @@ -0,0 +1,120 @@ +[ + { + "lat": 90, + "lon": 0, + "tz": "Etc/GMT", + "note": "https://github.com/noandrea/geo2tz/issues/22" + }, + { + "lat": 43.42582, + "lon": 11.831443, + "tz": "Europe/Rome", + "note": "https://github.com/noandrea/geo2tz/issues/22" + }, + { "lat": 32.7767, "lon": -96.797, "tz": "America/Chicago" }, + { "lat": 34.0522, "lon": -118.2437, "tz": "America/Los_Angeles" }, + { "lat": 40.7128, "lon": -74.006, "tz": "America/New_York" }, + { "lat": 51.5074, "lon": -0.1278, "tz": "Europe/London" }, + { "lat": 35.6895, "lon": 139.6917, "tz": "Asia/Tokyo" }, + { "lat": 48.8566, "lon": 2.3522, "tz": "Europe/Paris" }, + { "lat": -33.8688, "lon": 151.2093, "tz": "Australia/Sydney" }, + { "lat": 19.4326, "lon": -99.1332, "tz": "America/Mexico_City" }, + { "lat": 39.9042, "lon": 116.4074, "tz": "Asia/Shanghai" }, + { "lat": 28.6139, "lon": 77.209, "tz": "Asia/Kolkata", "err": true }, + { "lat": -23.5505, "lon": -46.6333, "tz": "America/Sao_Paulo" }, + { "lat": -34.6037, "lon": -58.3816, "tz": "America/Argentina/Buenos_Aires" }, + { "lat": -26.2041, "lon": 28.0473, "tz": "Africa/Johannesburg", "err": true }, + { "lat": 41.9028, "lon": 12.4964, "tz": "Europe/Rome" }, + { "lat": 37.7749, "lon": -122.4194, "tz": "America/Los_Angeles" }, + { "lat": 52.52, "lon": 13.405, "tz": "Europe/Berlin" }, + { "lat": 31.2304, "lon": 121.4737, "tz": "Asia/Shanghai" }, + { "lat": 22.3964, "lon": 114.1095, "tz": "Asia/Hong_Kong", "err": true }, + { "lat": -1.2921, "lon": 36.8219, "tz": "Africa/Nairobi" }, + { "lat": 33.8688, "lon": 151.2093, "tz": "Australia/Sydney", "err": true }, + { "lat": 50.1109, "lon": 8.6821, "tz": "Europe/Berlin" }, + { "lat": 40.4168, "lon": -3.7038, "tz": "Europe/Madrid" }, + { "lat": 45.4642, "lon": 9.19, "tz": "Europe/Rome" }, + { "lat": 43.6532, "lon": -79.3832, "tz": "America/Toronto" }, + { "lat": 37.9838, "lon": 23.7275, "tz": "Europe/Athens" }, + { "lat": 1.3521, "lon": 103.8198, "tz": "Asia/Singapore", "err": true }, + { "lat": 19.076, "lon": 72.8777, "tz": "Asia/Kolkata" }, + { "lat": -33.9249, "lon": 18.4241, "tz": "Africa/Johannesburg" }, + { "lat": 40.7306, "lon": -73.9352, "tz": "America/New_York" }, + { "lat": 35.6762, "lon": 139.6503, "tz": "Asia/Tokyo" }, + { "lat": 34.0522, "lon": -118.244, "tz": "America/Los_Angeles" }, + { "lat": 55.6761, "lon": 12.5683, "tz": "Europe/Copenhagen" }, + { "lat": 25.276987, "lon": 55.296249, "tz": "Asia/Dubai", "err": true }, + { "lat": 52.3676, "lon": 4.9041, "tz": "Europe/Amsterdam" }, + { "lat": 41.0082, "lon": 28.9784, "tz": "Europe/Istanbul" }, + { "lat": 59.3293, "lon": 18.0686, "tz": "Europe/Stockholm" }, + { "lat": 40.748817, "lon": -73.985428, "tz": "America/New_York" }, + { "lat": 49.2827, "lon": -123.1207, "tz": "America/Vancouver" }, + { "lat": -37.8136, "lon": 144.9631, "tz": "Australia/Melbourne" }, + { "lat": 45.4215, "lon": -75.6972, "tz": "America/Toronto" }, + { "lat": 55.9533, "lon": -3.1883, "tz": "Europe/London" }, + { "lat": 41.3851, "lon": 2.1734, "tz": "Europe/Madrid" }, + { "lat": -22.9068, "lon": -43.1729, "tz": "America/Sao_Paulo" }, + { "lat": -34.9285, "lon": 138.6007, "tz": "Australia/Adelaide" }, + { "lat": 37.5665, "lon": 126.978, "tz": "Asia/Seoul" }, + { "lat": 13.7563, "lon": 100.5018, "tz": "Asia/Bangkok", "err": true }, + { "lat": 22.5726, "lon": 88.3639, "tz": "Asia/Kolkata", "err": true }, + { "lat": 37.7749, "lon": -122.4194, "tz": "America/Los_Angeles" }, + { "lat": 48.2082, "lon": 16.3738, "tz": "Europe/Vienna" }, + { "lat": 52.2297, "lon": 21.0122, "tz": "Europe/Warsaw" }, + { "lat": 50.4501, "lon": 30.5234, "tz": "Europe/Kyiv" }, + { "lat": 49.8397, "lon": 24.0297, "tz": "Europe/Kyiv", "err": true }, + { "lat": 48.8566, "lon": 2.3522, "tz": "Europe/Paris" }, + { "lat": 34.6937, "lon": 135.5023, "tz": "Asia/Tokyo" }, + { "lat": 48.1351, "lon": 11.582, "tz": "Europe/Berlin" }, + { "lat": 40.4168, "lon": -3.7038, "tz": "Europe/Madrid" }, + { "lat": 1.3521, "lon": 103.8198, "tz": "Asia/Singapore", "err": true }, + { "lat": 50.0755, "lon": 14.4378, "tz": "Europe/Prague" }, + { "lat": 52.52, "lon": 13.405, "tz": "Europe/Berlin" }, + { "lat": 31.2304, "lon": 121.4737, "tz": "Asia/Shanghai" }, + { "lat": 45.4642, "lon": 9.19, "tz": "Europe/Rome" }, + { "lat": 51.1657, "lon": 10.4515, "tz": "Europe/Berlin" }, + { "lat": -15.7942, "lon": -47.8822, "tz": "America/Sao_Paulo" }, + { "lat": 40.7306, "lon": -73.9352, "tz": "America/New_York" }, + { "lat": -3.745, "lon": -38.523, "tz": "America/Fortaleza" }, + { "lat": 30.0444, "lon": 31.2357, "tz": "Africa/Cairo" }, + { "lat": -17.8249, "lon": 31.053, "tz": "Africa/Harare" }, + { "lat": 14.5995, "lon": 120.9842, "tz": "Asia/Manila" }, + { "lat": 31.7683, "lon": 35.2137, "tz": "Asia/Jerusalem", "err": true }, + { "lat": -22.9068, "lon": -43.1729, "tz": "America/Sao_Paulo" }, + { "lat": 12.9716, "lon": 77.5946, "tz": "Asia/Kolkata" }, + { "lat": -1.2921, "lon": 36.8219, "tz": "Africa/Nairobi" }, + { "lat": 41.9028, "lon": 12.4964, "tz": "Europe/Rome" }, + { "lat": 60.1695, "lon": 24.9354, "tz": "Europe/Helsinki" }, + { "lat": 45.4215, "lon": -75.6972, "tz": "America/Toronto" }, + { "lat": -25.2744, "lon": 133.7751, "tz": "Australia/Adelaide" }, + { "lat": -33.8688, "lon": 151.2093, "tz": "Australia/Sydney" }, + { "lat": 50.8503, "lon": 4.3517, "tz": "Europe/Brussels" }, + { "lat": 38.7223, "lon": -9.1393, "tz": "Europe/Lisbon" }, + { "lat": 1.29027, "lon": 103.851959, "tz": "Asia/Singapore", "err": true }, + { "lat": 35.6895, "lon": 139.6917, "tz": "Asia/Tokyo" }, + { "lat": 37.7749, "lon": -122.4194, "tz": "America/Los_Angeles" }, + { "lat": 48.8566, "lon": 2.3522, "tz": "Europe/Paris" }, + { "lat": 51.5074, "lon": -0.1278, "tz": "Europe/London" }, + { "lat": 40.7128, "lon": -74.006, "tz": "America/New_York" }, + { "lat": 34.0522, "lon": -118.2437, "tz": "America/Los_Angeles" }, + { "lat": 55.6761, "lon": 12.5683, "tz": "Europe/Copenhagen" }, + { "lat": 19.4326, "lon": -99.1332, "tz": "America/Mexico_City" }, + { "lat": 39.9042, "lon": 116.4074, "tz": "Asia/Shanghai" }, + { "lat": 28.6139, "lon": 77.209, "tz": "Asia/Kolkata", "err": true }, + { "lat": -23.5505, "lon": -46.6333, "tz": "America/Sao_Paulo" }, + { "lat": -34.6037, "lon": -58.3816, "tz": "America/Argentina/Buenos_Aires" }, + { "lat": -26.2041, "lon": 28.0473, "tz": "Africa/Johannesburg", "err": true }, + { "lat": 41.9028, "lon": 12.4964, "tz": "Europe/Rome" }, + { "lat": 37.7749, "lon": -122.4194, "tz": "America/Los_Angeles" }, + { "lat": 52.52, "lon": 13.405, "tz": "Europe/Berlin" }, + { "lat": 55.9533, "lon": -3.1883, "tz": "Europe/London" }, + { "lat": 37.5665, "lon": 126.978, "tz": "Asia/Seoul" }, + { "lat": -34.6037, "lon": -58.3816, "tz": "America/Argentina/Buenos_Aires" }, + { "lat": -23.5505, "lon": -46.6333, "tz": "America/Sao_Paulo" }, + { "lat": 22.3964, "lon": 114.1095, "tz": "Asia/Hong_Kong", "err": true }, + { "lat": 52.52, "lon": 13.405, "tz": "Europe/Berlin" }, + { "lat": 39.9042, "lon": 116.4074, "tz": "Asia/Shanghai" }, + { "lat": 48.8566, "lon": 2.3522, "tz": "Europe/Paris" }, + { "lat": 31.2304, "lon": 121.4737, "tz": "Asia/Shanghai" }, + { "lat": 52.3676, "lon": 4.9041, "tz": "Europe/Amsterdam" } +] diff --git a/db/testdata/zones.json b/db/testdata/zones.json new file mode 100644 index 0000000..e1cf2d9 --- /dev/null +++ b/db/testdata/zones.json @@ -0,0 +1,992 @@ +{ + "Africa/Abidjan": { + "zone": "GMT", + "utc_offset_h": 0 + }, + "Africa/Accra": { + "zone": "GMT", + "utc_offset_h": 0 + }, + "Africa/Addis_Ababa": { + "zone": "EAT", + "utc_offset_h": 3 + }, + "Africa/Algiers": { + "zone": "CET", + "utc_offset_h": 1 + }, + "Africa/Cairo": { + "zone": "EET", + "utc_offset_h": 2, + "dst": { + "zone": "EEST", + "start": "last Friday in April", + "end": "last Thursday in September", + "utc_offset_h": 3 + } + }, + "Africa/Cape_Town": { + "zone": "SAST", + "utc_offset_h": 2 + }, + "Africa/Casablanca": { + "zone": "WET", + "utc_offset_h": 0, + "dst": { + "zone": "WEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 1 + } + }, + "Africa/Ceuta": { + "zone": "CET", + "utc_offset_h": 1, + "dst": { + "zone": "CEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 2 + } + }, + "Africa/Johannesburg": { + "zone": "SAST", + "utc_offset_h": 2 + }, + "Africa/Tripoli": { + "zone": "EET", + "utc_offset_h": 2, + "dst": { + "zone": "EEST", + "start": "last Friday in March", + "end": "last Thursday in October", + "utc_offset_h": 3 + } + }, + "America/Adak": { + "zone": "HST", + "utc_offset_h": -10, + "dst": { + "zone": "HDT", + "start": "second Sunday in March", + "end": "first Sunday in November", + "utc_offset_h": -9 + } + }, + "Africa/Harare": { + "zone": "CAT", + "utc_offset_h": 2 + }, + "America/Vancouver": { + "zone": "PST", + "utc_offset_h": -8, + "dst": { + "zone": "PDT", + "start": "second Sunday in March", + "end": "first Sunday in November", + "utc_offset_h": -7 + } + }, + "Africa/Gaborone": { + "zone": "CAT", + "utc_offset_h": 2 + }, + "Africa/Lusaka": { + "zone": "CAT", + "utc_offset_h": 2 + }, + "Asia/Tehran": { + "zone": "IRST", + "utc_offset_h": 3.5, + "dst": { + "zone": "IRDT", + "start": "21st March", + "end": "21st September", + "utc_offset_h": 4.5 + } + }, + "Australia/Melbourne": { + "zone": "AEST", + "utc_offset_h": 10, + "dst": { + "zone": "AEDT", + "start": "first Sunday in October", + "end": "first Sunday in April", + "utc_offset_h": 11 + } + }, + "Africa/Nairobi": { + "zone": "EAT", + "utc_offset_h": 3 + }, + "America/Toronto": { + "zone": "EST", + "utc_offset_h": -5, + "dst": { + "zone": "EDT", + "start": "second Sunday in March", + "end": "first Sunday in November", + "utc_offset_h": -4 + } + }, + "America/Anchorage": { + "zone": "AKST", + "utc_offset_h": -9, + "dst": { + "zone": "AKDT", + "start": "second Sunday in March", + "end": "first Sunday in November", + "utc_offset_h": -8 + } + }, + "America/Argentina/Buenos_Aires": { + "zone": "ART", + "utc_offset_h": -3 + }, + "America/Argentina/Catamarca": { + "zone": "ART", + "utc_offset_h": -3 + }, + "America/Argentina/ComodRivadavia": { + "zone": "ART", + "utc_offset_h": -3 + }, + "America/Argentina/Cordoba": { + "zone": "ART", + "utc_offset_h": -3 + }, + "America/Argentina/Jujuy": { + "zone": "ART", + "utc_offset_h": -3 + }, + "America/Argentina/La_Rioja": { + "zone": "ART", + "utc_offset_h": -3 + }, + "America/Argentina/Mendoza": { + "zone": "ART", + "utc_offset_h": -3 + }, + "America/Argentina/Rio_Gallegos": { + "zone": "ART", + "utc_offset_h": -3 + }, + "America/Argentina/Salta": { + "zone": "ART", + "utc_offset_h": -3 + }, + "America/Argentina/San_Juan": { + "zone": "ART", + "utc_offset_h": -3 + }, + "America/Argentina/San_Luis": { + "zone": "WART", + "utc_offset_h": -3 + }, + "America/Argentina/Tucuman": { + "zone": "ART", + "utc_offset_h": -3 + }, + "America/Argentina/Ushuaia": { + "zone": "ART", + "utc_offset_h": -3 + }, + "America/Asuncion": { + "zone": "PYT", + "utc_offset_h": -4, + "dst": { + "zone": "PYST", + "start": "first Sunday in October", + "end": "fourth Sunday in March", + "utc_offset_h": -3 + } + }, + "America/Atikokan": { + "zone": "EST", + "utc_offset_h": -5 + }, + "America/Bahia_Banderas": { + "zone": "CST", + "utc_offset_h": -6, + "dst": { + "zone": "CDT", + "start": "first Sunday in April", + "end": "last Sunday in October", + "utc_offset_h": -5 + } + }, + "America/Barbados": { + "zone": "AST", + "utc_offset_h": -4 + }, + "America/Belize": { + "zone": "CST", + "utc_offset_h": -6 + }, + "America/Boa_Vista": { + "zone": "AMT", + "utc_offset_h": -4 + }, + "America/Bogota": { + "zone": "COT", + "utc_offset_h": -5 + }, + "America/Caracas": { + "zone": "VET", + "utc_offset_h": -4.5 + }, + "America/Chicago": { + "zone": "CST", + "utc_offset_h": -6, + "dst": { + "zone": "CDT", + "start": "second Sunday in March", + "end": "first Sunday in November", + "utc_offset_h": -5 + } + }, + "America/Denver": { + "zone": "MST", + "utc_offset_h": -7, + "dst": { + "zone": "MDT", + "start": "second Sunday in March", + "end": "first Sunday in November", + "utc_offset_h": -6 + } + }, + "America/Detroit": { + "zone": "EST", + "utc_offset_h": -5, + "dst": { + "zone": "EDT", + "start": "second Sunday in March", + "end": "first Sunday in November", + "utc_offset_h": -4 + } + }, + "America/Edmonton": { + "zone": "MST", + "utc_offset_h": -7, + "dst": { + "zone": "MDT", + "start": "second Sunday in March", + "end": "first Sunday in November", + "utc_offset_h": -6 + } + }, + "America/El_Salvador": { + "zone": "CST", + "utc_offset_h": -6 + }, + "America/Fortaleza": { + "zone": "BRT", + "utc_offset_h": -3 + }, + "America/Glace_Bay": { + "zone": "AST", + "utc_offset_h": -4, + "dst": { + "zone": "ADT", + "start": "second Sunday in March", + "end": "first Sunday in November", + "utc_offset_h": -3 + } + }, + "America/Godthab": { + "zone": "WGT", + "utc_offset_h": -3, + "dst": { + "zone": "WGST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": -2 + } + }, + "America/Grand_Turk": { + "zone": "EST", + "utc_offset_h": -5, + "dst": { + "zone": "EDT", + "start": "second Sunday in March", + "end": "first Sunday in November", + "utc_offset_h": -4 + } + }, + "America/Guatemala": { + "zone": "CST", + "utc_offset_h": -6 + }, + "America/Halifax": { + "zone": "AST", + "utc_offset_h": -4, + "dst": { + "zone": "ADT", + "start": "second Sunday in March", + "end": "first Sunday in November", + "utc_offset_h": -3 + } + }, + "America/Havana": { + "zone": "CST", + "utc_offset_h": -5, + "dst": { + "zone": "CDT", + "start": "second Sunday in March", + "end": "first Sunday in November", + "utc_offset_h": -4 + } + }, + "America/Indiana/Indianapolis": { + "zone": "EST", + "utc_offset_h": -5, + "dst": { + "zone": "EDT", + "start": "second Sunday in March", + "end": "first Sunday in November", + "utc_offset_h": -4 + } + }, + "America/Indiana/Knox": { + "zone": "CST", + "utc_offset_h": -6, + "dst": { + "zone": "CDT", + "start": "second Sunday in March", + "end": "first Sunday in November", + "utc_offset_h": -5 + } + }, + "America/Jamaica": { + "zone": "EST", + "utc_offset_h": -5 + }, + "America/Los_Angeles": { + "zone": "PST", + "utc_offset_h": -8, + "dst": { + "zone": "PDT", + "start": "second Sunday in March", + "end": "first Sunday in November", + "utc_offset_h": -7 + } + }, + "America/Managua": { + "zone": "CST", + "utc_offset_h": -6 + }, + "America/Mexico_City": { + "zone": "CST", + "utc_offset_h": -6, + "dst": { + "zone": "CDT", + "start": "first Sunday in April", + "end": "last Sunday in October", + "utc_offset_h": -5 + } + }, + "America/Montevideo": { + "zone": "UYT", + "utc_offset_h": -3 + }, + "America/New_York": { + "zone": "EST", + "utc_offset_h": -5, + "dst": { + "zone": "EDT", + "start": "second Sunday in March", + "end": "first Sunday in November", + "utc_offset_h": -4 + } + }, + "America/Panama": { + "zone": "EST", + "utc_offset_h": -5 + }, + "America/Phoenix": { + "zone": "MST", + "utc_offset_h": -7 + }, + "America/Santiago": { + "zone": "CLT", + "utc_offset_h": -4, + "dst": { + "zone": "CLST", + "start": "first Sunday in September", + "end": "first Sunday in April", + "utc_offset_h": -3 + } + }, + "America/Sao_Paulo": { + "zone": "BRT", + "utc_offset_h": -3, + "dst": { + "zone": "BRST", + "start": "third Sunday in October", + "end": "third Sunday in February", + "utc_offset_h": -2 + } + }, + "Asia/Baghdad": { + "zone": "AST", + "utc_offset_h": 3 + }, + "Asia/Baku": { + "zone": "AZT", + "utc_offset_h": 4, + "dst": { + "zone": "AZST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 5 + } + }, + "Asia/Bangkok": { + "zone": "ICT", + "utc_offset_h": 7 + }, + "Asia/Beirut": { + "zone": "EET", + "utc_offset_h": 2, + "dst": { + "zone": "EEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 3 + } + }, + "Asia/Dhaka": { + "zone": "BST", + "utc_offset_h": 6 + }, + "Asia/Dubai": { + "zone": "GST", + "utc_offset_h": 4 + }, + "Asia/Hong_Kong": { + "zone": "HKT", + "utc_offset_h": 8 + }, + "Asia/Ho_Chi_Minh": { + "zone": "ICT", + "utc_offset_h": 7 + }, + "Asia/Jakarta": { + "zone": "WIB", + "utc_offset_h": 7 + }, + "Asia/Jerusalem": { + "zone": "IST", + "utc_offset_h": 2, + "dst": { + "zone": "IDT", + "start": "last Friday in March", + "end": "last Sunday in October", + "utc_offset_h": 3 + } + }, + "Asia/Kolkata": { + "zone": "IST", + "utc_offset_h": 5.5 + }, + "Asia/Kuala_Lumpur": { + "zone": "MYT", + "utc_offset_h": 8 + }, + "Asia/Kuwait": { + "zone": "AST", + "utc_offset_h": 3 + }, + "Asia/Manila": { + "zone": "PHT", + "utc_offset_h": 8 + }, + "Asia/Riyadh": { + "zone": "AST", + "utc_offset_h": 3 + }, + "Asia/Seoul": { + "zone": "KST", + "utc_offset_h": 9 + }, + "Asia/Shanghai": { + "zone": "CST", + "utc_offset_h": 8 + }, + "Asia/Singapore": { + "zone": "SGT", + "utc_offset_h": 8 + }, + "Asia/Tokyo": { + "zone": "JST", + "utc_offset_h": 9 + }, + "Asia/Yangon": { + "zone": "MMT", + "utc_offset_h": 6.5 + }, + "Atlantic/Azores": { + "zone": "AZOT", + "utc_offset_h": -1, + "dst": { + "zone": "AZOST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 0 + } + }, + "Atlantic/Bermuda": { + "zone": "AST", + "utc_offset_h": -4, + "dst": { + "zone": "ADT", + "start": "second Sunday in March", + "end": "first Sunday in November", + "utc_offset_h": -3 + } + }, + "Atlantic/Canary": { + "zone": "WET", + "utc_offset_h": 0, + "dst": { + "zone": "WEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 1 + } + }, + "Atlantic/Cape_Verde": { + "zone": "CVT", + "utc_offset_h": -1 + }, + "Atlantic/Reykjavik": { + "zone": "GMT", + "utc_offset_h": 0 + }, + "Australia/Adelaide": { + "zone": "ACST", + "utc_offset_h": 9.5, + "dst": { + "zone": "ACDT", + "start": "first Sunday in October", + "end": "first Sunday in April", + "utc_offset_h": 10.5 + } + }, + "Australia/Brisbane": { + "zone": "AEST", + "utc_offset_h": 10 + }, + "Australia/Darwin": { + "zone": "ACST", + "utc_offset_h": 9.5 + }, + "Australia/Hobart": { + "zone": "AEST", + "utc_offset_h": 10, + "dst": { + "zone": "AEDT", + "start": "first Sunday in October", + "end": "first Sunday in April", + "utc_offset_h": 11 + } + }, + "Australia/Perth": { + "zone": "AWST", + "utc_offset_h": 8 + }, + "Australia/Sydney": { + "zone": "AEST", + "utc_offset_h": 10, + "dst": { + "zone": "AEDT", + "start": "first Sunday in October", + "end": "first Sunday in April", + "utc_offset_h": 11 + } + }, + "Europe/Amsterdam": { + "zone": "CET", + "utc_offset_h": 1, + "dst": { + "zone": "CEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 2 + } + }, + "Europe/Athens": { + "zone": "EET", + "utc_offset_h": 2, + "dst": { + "zone": "EEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 3 + } + }, + "Europe/Belgrade": { + "zone": "CET", + "utc_offset_h": 1, + "dst": { + "zone": "CEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 2 + } + }, + "Europe/Berlin": { + "zone": "CET", + "utc_offset_h": 1, + "dst": { + "zone": "CEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 2 + } + }, + "Europe/Brussels": { + "zone": "CET", + "utc_offset_h": 1, + "dst": { + "zone": "CEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 2 + } + }, + "Europe/Bucharest": { + "zone": "EET", + "utc_offset_h": 2, + "dst": { + "zone": "EEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 3 + } + }, + "Europe/Budapest": { + "zone": "CET", + "utc_offset_h": 1, + "dst": { + "zone": "CEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 2 + } + }, + "Europe/Chisinau": { + "zone": "EET", + "utc_offset_h": 2, + "dst": { + "zone": "EEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 3 + } + }, + "Europe/Copenhagen": { + "zone": "CET", + "utc_offset_h": 1, + "dst": { + "zone": "CEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 2 + } + }, + "Europe/Dublin": { + "zone": "GMT", + "utc_offset_h": 0, + "dst": { + "zone": "IST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 1 + } + }, + "Europe/Helsinki": { + "zone": "EET", + "utc_offset_h": 2, + "dst": { + "zone": "EEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 3 + } + }, + "Europe/Istanbul": { + "zone": "TRT", + "utc_offset_h": 3 + }, + "Europe/Kaliningrad": { + "zone": "EET", + "utc_offset_h": 2 + }, + "Europe/Kyiv": { + "zone": "EET", + "utc_offset_h": 2, + "dst": { + "zone": "EEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 3 + } + }, + "Europe/Lisbon": { + "zone": "WET", + "utc_offset_h": 0, + "dst": { + "zone": "WEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 1 + } + }, + "Europe/London": { + "zone": "GMT", + "utc_offset_h": 0, + "dst": { + "zone": "BST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 1 + } + }, + "Europe/Luxembourg": { + "zone": "CET", + "utc_offset_h": 1, + "dst": { + "zone": "CEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 2 + } + }, + "Europe/Madrid": { + "zone": "CET", + "utc_offset_h": 1, + "dst": { + "zone": "CEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 2 + } + }, + "Europe/Minsk": { + "zone": "MSK", + "utc_offset_h": 3 + }, + "Europe/Monaco": { + "zone": "CET", + "utc_offset_h": 1, + "dst": { + "zone": "CEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 2 + } + }, + "Europe/Moscow": { + "zone": "MSK", + "utc_offset_h": 3 + }, + "Europe/Oslo": { + "zone": "CET", + "utc_offset_h": 1, + "dst": { + "zone": "CEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 2 + } + }, + "Europe/Paris": { + "zone": "CET", + "utc_offset_h": 1, + "dst": { + "zone": "CEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 2 + } + }, + "Europe/Prague": { + "zone": "CET", + "utc_offset_h": 1, + "dst": { + "zone": "CEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 2 + } + }, + "Europe/Riga": { + "zone": "EET", + "utc_offset_h": 2, + "dst": { + "zone": "EEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 3 + } + }, + "Europe/Rome": { + "zone": "CET", + "utc_offset_h": 1, + "dst": { + "zone": "CEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 2 + } + }, + "Europe/Samara": { + "zone": "SAMT", + "utc_offset_h": 4 + }, + "Europe/Simferopol": { + "zone": "MSK", + "utc_offset_h": 3 + }, + "Europe/Sofia": { + "zone": "EET", + "utc_offset_h": 2, + "dst": { + "zone": "EEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 3 + } + }, + "Europe/Stockholm": { + "zone": "CET", + "utc_offset_h": 1, + "dst": { + "zone": "CEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 2 + } + }, + "Europe/Tallinn": { + "zone": "EET", + "utc_offset_h": 2, + "dst": { + "zone": "EEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 3 + } + }, + "Europe/Tirane": { + "zone": "CET", + "utc_offset_h": 1, + "dst": { + "zone": "CEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 2 + } + }, + "Europe/Vienna": { + "zone": "CET", + "utc_offset_h": 1, + "dst": { + "zone": "CEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 2 + } + }, + "Europe/Vilnius": { + "zone": "EET", + "utc_offset_h": 2, + "dst": { + "zone": "EEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 3 + } + }, + "Europe/Warsaw": { + "zone": "CET", + "utc_offset_h": 1, + "dst": { + "zone": "CEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 2 + } + }, + "Europe/Zurich": { + "zone": "CET", + "utc_offset_h": 1, + "dst": { + "zone": "CEST", + "start": "last Sunday in March", + "end": "last Sunday in October", + "utc_offset_h": 2 + } + }, + "Pacific/Auckland": { + "zone": "NZST", + "utc_offset_h": 12, + "dst": { + "zone": "NZDT", + "start": "last Sunday in September", + "end": "first Sunday in April", + "utc_offset_h": 13 + } + }, + "Pacific/Chatham": { + "zone": "CHAST", + "utc_offset_h": 12.75, + "dst": { + "zone": "CHADT", + "start": "last Sunday in September", + "end": "first Sunday in April", + "utc_offset_h": 13.75 + } + }, + "Pacific/Easter": { + "zone": "EAST", + "utc_offset_h": -6, + "dst": { + "zone": "EASST", + "start": "first Sunday in September", + "end": "first Sunday in April", + "utc_offset_h": -5 + } + }, + "Pacific/Fiji": { + "zone": "FJT", + "utc_offset_h": 12, + "dst": { + "zone": "FJST", + "start": "second Sunday in November", + "end": "second Sunday in January", + "utc_offset_h": 13 + } + }, + "Pacific/Guam": { + "zone": "ChST", + "utc_offset_h": 10 + }, + "Pacific/Honolulu": { + "zone": "HST", + "utc_offset_h": -10 + }, + "Pacific/Marquesas": { + "zone": "MART", + "utc_offset_h": -9.5 + }, + "Pacific/Pago_Pago": { + "zone": "SST", + "utc_offset_h": -11 + }, + "Pacific/Tahiti": { + "zone": "TAHT", + "utc_offset_h": -10 + } +} diff --git a/go.mod b/go.mod index b393ae5..59937d7 100644 --- a/go.mod +++ b/go.mod @@ -1,43 +1,46 @@ module github.com/noandrea/geo2tz/v2 -go 1.20 +go 1.21 require ( - github.com/evanoberholster/timezoneLookup v1.0.0 - github.com/labstack/echo/v4 v4.10.2 - github.com/spf13/cobra v1.7.0 - github.com/spf13/viper v1.16.0 - golang.org/x/crypto v0.10.0 + github.com/klauspost/compress v1.17.9 + github.com/labstack/echo/v4 v4.12.0 + github.com/spf13/cobra v1.8.1 + github.com/spf13/viper v1.19.0 + github.com/stretchr/testify v1.9.0 + github.com/tidwall/rtree v1.10.0 + golang.org/x/crypto v0.24.0 ) require ( - github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/goccy/go-json v0.10.2 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/klauspost/compress v1.16.6 // indirect - github.com/labstack/gommon v0.4.0 // indirect + github.com/labstack/gommon v0.4.2 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/pelletier/go-toml/v2 v2.0.8 // indirect - github.com/spf13/afero v1.9.5 // indirect - github.com/spf13/cast v1.5.1 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/sagikazarmark/locafero v0.6.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.4.2 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/tidwall/geoindex v1.7.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect - github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - go.etcd.io/bbolt v1.3.7 // indirect - golang.org/x/net v0.11.0 // indirect - golang.org/x/sys v0.9.0 // indirect - golang.org/x/text v0.10.0 // indirect - golang.org/x/time v0.3.0 // indirect - google.golang.org/protobuf v1.30.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/time v0.5.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 index c3bf7a0..82dc6de 100644 --- a/go.sum +++ b/go.sum @@ -1,535 +1,107 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -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/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanoberholster/timezoneLookup v1.0.0 h1:VZp2ugt55qVAGq6EypdfXRXNcydkg4JqGRHsL/NEzhE= -github.com/evanoberholster/timezoneLookup v1.0.0/go.mod h1:A5eGBsgCkXitWnHNPB4Ltr0O+6y3nvl6Mn1IG8w/eDU= -github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +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/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-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -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.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -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.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -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/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -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.4.1/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.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/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.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +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/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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk= -github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -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/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M= -github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k= -github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= -github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= +github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= -github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +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/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/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= -github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= -github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= -github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= +github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= +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/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.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= -github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= 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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= -github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tidwall/cities v0.1.0 h1:CVNkmMf7NEC9Bvokf5GoSsArHCKRMTgLuubRTHnH0mE= +github.com/tidwall/cities v0.1.0/go.mod h1:lV/HDp2gCcRcHJWqgt6Di54GiDrTZwh1aG2ZUPNbqa4= +github.com/tidwall/geoindex v1.7.0 h1:jtk41sfgwIt8MEDyC3xyKSj75iXXf6rjReJGDNPtR5o= +github.com/tidwall/geoindex v1.7.0/go.mod h1:rvVVNEFfkJVWGUdEfU8QaoOg/9zFX0h9ofWzA60mz1I= +github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8= +github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8= +github.com/tidwall/rtree v1.10.0 h1:+EcI8fboEaW1L3/9oW/6AMoQ8HiEIHyR7bQOGnmz4Mg= +github.com/tidwall/rtree v1.10.0/go.mod h1:iDJQ9NBRtbfKkzZu02za+mIlaP+bjYPnunbSNidpbCQ= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= -github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= -github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= -github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= -go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= -golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -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-20190108225652-1e06a53dbb7e/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-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/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-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= -golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -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-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/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-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= -golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -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-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -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.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -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-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -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.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -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.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -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.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +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.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-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/helpers/helpers.go b/helpers/helpers.go new file mode 100644 index 0000000..93e466f --- /dev/null +++ b/helpers/helpers.go @@ -0,0 +1,56 @@ +package helpers + +import ( + "encoding/json" + "fmt" + "os" +) + +func SaveJSON(data any, toFile string) (err error) { + // write the version file as json, overwriting the old one + jsonString, err := json.MarshalIndent(data, "", " ") + if err != nil { + return + } + if err = os.WriteFile(toFile, jsonString, os.ModePerm); err != nil { + return + } + return +} + +func LoadJSON(fromFile string, data any) (err error) { + // read the version file + jsonFile, err := os.Open(fromFile) + if err != nil { + return + } + defer jsonFile.Close() + jsonParser := json.NewDecoder(jsonFile) + if err = jsonParser.Decode(data); err != nil { + return + } + return +} + +func DeleteQuietly(filePath ...string) (err error) { + for _, filePath := range filePath { + if xErr := FileExists(filePath); xErr == nil { + fmt.Println("removing file", filePath) + if err = os.Remove(filePath); err != nil { + return + } + } + } + return +} + +func FileExists(filePath string) error { + f, err := os.Stat(filePath) + if err != nil { + return fmt.Errorf("file does not exist: %s", filePath) + } + if f.IsDir() { + return fmt.Errorf("file is a directory: %s", filePath) + } + return nil +} diff --git a/main.go b/main.go index fd67e77..c3ac3f7 100644 --- a/main.go +++ b/main.go @@ -29,12 +29,24 @@ import ( ) // Version hold the version of the program. -var Version = "0.0.0" +var ( + version = "dev" + commit = "unknown" + date = "unknown" + builtBy = "unknown" +) func main() { - if err := cmd.Execute(Version); err != nil { + + version := cmd.RuntimeVersion{ + Version: version, + Commit: commit, + Date: date, + BuiltBy: builtBy, + } + + if err := cmd.Execute(version); err != nil { fmt.Println(err) os.Exit(1) } - } diff --git a/scripts/update-tzdata.sh b/scripts/update-tzdata.sh deleted file mode 100755 index d9a21ac..0000000 --- a/scripts/update-tzdata.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -VERSION=2024a -WORKDIR=dist - -pushd $WORKDIR -echo download timezones -curl -LO https://github.com/evansiroky/timezone-boundary-builder/releases/download/$VERSION/timezones-with-oceans.geojson.zip -unzip timezones-with-oceans.geojson.zip -./geo2tz build --json "combined-with-oceans.json" --db=timezone -echo "https://github.com/evansiroky/timezone-boundary-builder" > SOURCE -echo "tzdata v$VERSION" >> SOURCE - -popd -mv dist/timezone.snap.json tzdata/timezone.snap.json -mv dist/SOURCE tzdata/SOURCE diff --git a/server/server.go b/server/server.go deleted file mode 100644 index 4bc3003..0000000 --- a/server/server.go +++ /dev/null @@ -1,143 +0,0 @@ -package server - -import ( - "context" - "crypto/subtle" - "fmt" - "net/http" - "strconv" - "strings" - "time" - - "github.com/evanoberholster/timezoneLookup" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" - "golang.org/x/crypto/blake2b" -) - -// constant valuses for lat / lon -const ( - Latitude = "lat" - Longitude = "lon" - compareEquals = 1 - teardownTimeout = 10 * time.Second -) - -var ( - tz timezoneLookup.TimezoneInterface - e *echo.Echo -) - -// hash calculate the hash of a string -func hash(data ...interface{}) []byte { - hash := blake2b.Sum256([]byte(fmt.Sprint(data...))) - return hash[:] -} - -// isEq check if the hash of the second value is equals to the first value -func isEq(expectedTokenHash []byte, actualToken string) bool { - return subtle.ConstantTimeCompare(expectedTokenHash, hash(actualToken)) == compareEquals -} - -// Start starts the web server -func Start(config ConfigSchema) (err error) { - // echo start - e = echo.New() - // open the database - tz, err = timezoneLookup.LoadTimezones( - timezoneLookup.Config{ - DatabaseType: "memory", // memory or boltdb - DatabaseName: config.Tz.DatabaseName, // Name without suffix - Snappy: config.Tz.Snappy, - }) - if err != nil { - return - } - - // check token authorization - hashedToken := hash(config.Web.AuthTokenValue) - authEnabled := false - if len(config.Web.AuthTokenValue) > 0 { - e.Logger.Info("Authorization enabled") - authEnabled = true - } else { - e.Logger.Info("Authorization disabled") - } - - e.HideBanner = true - e.Use(middleware.CORS()) - e.Use(middleware.Logger()) - e.Use(middleware.Recover()) - // logger - e.GET("/tz/:lat/:lon", func(c echo.Context) (err error) { - // token verification - if authEnabled { - requestToken := c.QueryParam(config.Web.AuthTokenParamName) - if !isEq(hashedToken, requestToken) { - e.Logger.Errorf("request unhautorized, invalid token: %v", requestToken) - return c.JSON(http.StatusUnauthorized, map[string]interface{}{"message": "unauthorized"}) - } - } - // parse latitude - lat, err := parseCoordinate(c.Param(Latitude), Latitude) - if err != nil { - e.Logger.Errorf("error parsing latitude: %v", err) - return c.JSON(http.StatusBadRequest, map[string]interface{}{"message": fmt.Sprint(err)}) - } - // parse longitude - lon, err := parseCoordinate(c.Param(Longitude), Longitude) - if err != nil { - e.Logger.Errorf("error parsing longitude: %v", err) - return c.JSON(http.StatusBadRequest, map[string]interface{}{"message": fmt.Sprint(err)}) - } - // build coordinates object - coords := timezoneLookup.Coord{ - Lat: lat, - Lon: lon, - } - // query the coordinates - res, err := tz.Query(coords) - if err != nil { - e.Logger.Errorf("error querying the timezoned db: %v", err) - return c.JSON(http.StatusInternalServerError, map[string]interface{}{"message": fmt.Sprint(err)}) - } - return c.JSON(http.StatusOK, map[string]interface{}{"tz": res, "coords": coords}) - }) - err = e.Start(config.Web.ListenAddress) - return -} - -// parseCoordinate parse a string into a coordinate -func parseCoordinate(val, side string) (float32, error) { - if strings.TrimSpace(val) == "" { - return 0, fmt.Errorf("empty coordinates value") - } - c, err := strconv.ParseFloat(val, 32) - if err != nil { - return 0, fmt.Errorf("invalid type for %s, a number is required (eg. 45.3123)", side) - } - switch side { - case Latitude: - if c < -90 || c > 90 { - return 0, fmt.Errorf("%s value %s out of range (-90/+90)", side, val) - } - case Longitude: - if c < -180 || c > 180 { - return 0, fmt.Errorf("%s value %s out of range (-180/+180)", side, val) - } - } - return float32(c), nil -} - -// Teardown gracefully release resources -func Teardown() (err error) { - if tz != nil { - tz.Close() - } - ctx, cancel := context.WithTimeout(context.Background(), teardownTimeout) - defer cancel() - if e != nil { - err = e.Shutdown(ctx) - } - return -} diff --git a/server/server_test.go b/server/server_test.go deleted file mode 100644 index 40c39fd..0000000 --- a/server/server_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package server - -import ( - "fmt" - "reflect" - "testing" -) - -func Test_parseCoordinate(t *testing.T) { - type c struct { - val string - side string - } - tests := []struct { - ll c - want float32 - wantErr bool - }{ - {c{"22", Latitude}, 22, false}, - {c{"78.312", Longitude}, 78.312, false}, - {c{"0x429c9fbe", Longitude}, 0, true}, // 78.312 in hex - {c{"", Longitude}, 0, true}, - {c{" ", Longitude}, 0, true}, - {c{"2e4", Longitude}, 0, true}, - {c{"not a number", Longitude}, 0, true}, - {c{"-90.1", Latitude}, 0, true}, - {c{"90.001", Latitude}, 0, true}, - {c{"-180.1", Longitude}, 0, true}, - {c{"180.001", Longitude}, 0, true}, - } - for _, tt := range tests { - t.Run(fmt.Sprint(tt.ll), func(t *testing.T) { - got, err := parseCoordinate(tt.ll.val, tt.ll.side) - if (err != nil) != tt.wantErr { - t.Errorf("parseCoordinate() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("parseCoordinate() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_hash(t *testing.T) { - tests := []struct { - name string - args []interface{} - want []byte - }{ - { - "one element", - []interface{}{ - "test1", - }, - []byte{229, 104, 55, 204, 215, 163, 141, 103, 149, 211, 10, 194, 171, 99, 236, 204, 140, 43, 87, 18, 137, 166, 45, 196, 6, 187, 98, 118, 126, 136, 176, 108}, - }, - { - "two elements", - []interface{}{ - "test1", - "test2", - }, - []byte{84, 182, 224, 44, 5, 184, 19, 24, 41, 163, 6, 53, 242, 3, 167, 200, 192, 113, 61, 137, 208, 241, 141, 225, 134, 61, 78, 124, 88, 254, 117, 159}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := hash(tt.args...); !reflect.DeepEqual(got, tt.want) { - t.Errorf("hash() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_isEq(t *testing.T) { - type args struct { - expectedTokenHash []byte - actualToken string - } - tests := []struct { - name string - args args - want bool - }{ - { - "PASS: token matches", - args{ - []byte{229, 104, 55, 204, 215, 163, 141, 103, 149, 211, 10, 194, 171, 99, 236, 204, 140, 43, 87, 18, 137, 166, 45, 196, 6, 187, 98, 118, 126, 136, 176, 108}, - "test1", - }, - true, - }, - { - "FAIL: token mismatch", - args{ - []byte{84, 182, 224, 44, 5, 184, 19, 24, 41, 163, 6, 53, 242, 3, 167, 200, 192, 113, 61, 137, 208, 241, 141, 225, 134, 61, 78, 124, 88, 254, 117, 159}, - "test1", - }, - false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := isEq(tt.args.expectedTokenHash, tt.args.actualToken); got != tt.want { - t.Errorf("isEq() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/tzdata/SOURCE b/tzdata/SOURCE deleted file mode 100644 index b930ac7..0000000 --- a/tzdata/SOURCE +++ /dev/null @@ -1,2 +0,0 @@ -https://github.com/evansiroky/timezone-boundary-builder -tzdata v2024a diff --git a/tzdata/timezone.snap.json b/tzdata/timezone.snap.json deleted file mode 100644 index 21cff05..0000000 Binary files a/tzdata/timezone.snap.json and /dev/null differ diff --git a/tzdata/version.json b/tzdata/version.json new file mode 100755 index 0000000..67b25b4 --- /dev/null +++ b/tzdata/version.json @@ -0,0 +1,5 @@ +{ + "version": "2024a", + "url": "https://github.com/evansiroky/timezone-boundary-builder/releases/tag/2024a", + "geo_data_url": "https://github.com/evansiroky/timezone-boundary-builder/releases/download/2024a/timezones-with-oceans.geojson.zip" +} \ No newline at end of file diff --git a/server/config.go b/web/config.go similarity index 52% rename from server/config.go rename to web/config.go index b8848c1..cf39ac8 100644 --- a/server/config.go +++ b/web/config.go @@ -1,16 +1,37 @@ -package server +package web import ( + "fmt" + "github.com/spf13/viper" ) +const ( + TZDBFile = "tzdata/timezones.zip" + TZVersionFile = "tzdata/version.json" + + GeoDataURLTemplate = "https://github.com/evansiroky/timezone-boundary-builder/releases/download/%s/timezones-with-oceans.geojson.zip" + GeoDataReleaseURLTemplate = "https://github.com/evansiroky/timezone-boundary-builder/releases/tag/%s" +) + +type TzRelease struct { + Version string `json:"version"` + URL string `json:"url"` + GeoDataURL string `json:"geo_data_url"` +} + +func NewTzRelease(version string) TzRelease { + return TzRelease{ + Version: version, + URL: fmt.Sprintf(GeoDataReleaseURLTemplate, version), + GeoDataURL: fmt.Sprintf(GeoDataURLTemplate, version), + } +} + // TzSchema configuration type TzSchema struct { - DatabaseName string `mapstructure:"database_name"` - Snappy bool `mapstructure:"snappy"` - DownloadTzData bool `mapstructure:"download_tz_data"` - DownloadTzDataURL string `mapstructure:"download_tz_data_url"` - DownloadTzFilename string `mapstructure:"download_tz_filename"` + DatabaseName string `mapstructure:"database_name"` + VersionFile string `mapstructure:"version_file"` } // WebSchema configuration @@ -30,16 +51,12 @@ type ConfigSchema struct { // Defaults configure defaults func Defaults() { // tz defaults - viper.SetDefault("tz.database_name", "tzdata/timezone") - viper.SetDefault("tz.snappy", true) - viper.SetDefault("tz.download_tz_data", true) - viper.SetDefault("tz.download_tz_data_url", "https://api.github.com/repos/evansiroky/timezone-boundary-builder/releases/latest") - viper.SetDefault("tz.download_tz_filename", "timezones.geojson.zip") + viper.SetDefault("tz.database_name", TZDBFile) + viper.SetDefault("tz.version_file", TZVersionFile) // web viper.SetDefault("web.listen_address", ":2004") viper.SetDefault("web.auth_token_value", "") // GEO2TZ_WEB_AUTH_TOKEN_VALUE="ciao" viper.SetDefault("web.auth_token_param_name", "t") - } // Validate a configuration diff --git a/web/errors.go b/web/errors.go new file mode 100644 index 0000000..e411697 --- /dev/null +++ b/web/errors.go @@ -0,0 +1,10 @@ +package web + +import "errors" + +var ( + ErrorVersionFileNotFound = errors.New("release version file not found") + ErrorVersionFileInvalid = errors.New("release version file invalid") + ErrorDatabaseFileNotFound = errors.New("database file not found") + ErrorDatabaseFileInvalid = errors.New("database file invalid") +) diff --git a/web/server.go b/web/server.go new file mode 100644 index 0000000..ff2ad3a --- /dev/null +++ b/web/server.go @@ -0,0 +1,172 @@ +package web + +import ( + "context" + "crypto/subtle" + "errors" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/noandrea/geo2tz/v2/db" + "github.com/noandrea/geo2tz/v2/helpers" + + "golang.org/x/crypto/blake2b" +) + +// constant valuses for lat / lon +const ( + Latitude = "lat" + Longitude = "lon" + compareEquals = 1 + teardownTimeout = 10 * time.Second +) + +// hash calculate the hash of a string +func hash(data ...interface{}) []byte { + hash := blake2b.Sum256([]byte(fmt.Sprint(data...))) + return hash[:] +} + +// isEq check if the hash of the second value is equals to the first value +func isEq(expectedTokenHash []byte, actualToken string) bool { + return subtle.ConstantTimeCompare(expectedTokenHash, hash(actualToken)) == compareEquals +} + +type Server struct { + config ConfigSchema + tzDB db.TzDBIndex + tzRelease TzRelease + echo *echo.Echo + authEnabled bool + authHashedToken []byte +} + +func (server *Server) Start() error { + return server.echo.Start(server.config.Web.ListenAddress) +} + +func (server *Server) Teardown() (err error) { + ctx, cancel := context.WithTimeout(context.Background(), teardownTimeout) + defer cancel() + if server.echo != nil { + err = server.echo.Shutdown(ctx) + } + return +} + +func NewServer(config ConfigSchema) (*Server, error) { + var server Server + server.config = config + server.echo = echo.New() + + // load the database + tzDB, err := db.NewGeo2TzRTreeIndexFromGeoJSON(config.Tz.DatabaseName) + if err != nil { + return nil, errors.Join(ErrorDatabaseFileNotFound, err) + } + server.tzDB = tzDB + + // check token authorization + server.authHashedToken = hash(config.Web.AuthTokenValue) + if len(config.Web.AuthTokenValue) > 0 { + server.echo.Logger.Info("Authorization enabled") + server.authEnabled = true + } else { + server.echo.Logger.Info("Authorization disabled") + } + + server.echo.HideBanner = true + server.echo.Use(middleware.CORS()) + server.echo.Use(middleware.Logger()) + server.echo.Use(middleware.Recover()) + + // load the release info + if err = helpers.LoadJSON(config.Tz.VersionFile, &server.tzRelease); err != nil { + err = errors.Join(ErrorVersionFileNotFound, err, fmt.Errorf("error loading the timezone release info: %w", err)) + return nil, err + } + + // register routes + server.echo.GET("/tz/:lat/:lon", server.handleTzRequest) + server.echo.GET("/tz/version", server.handleTzVersion) + + return &server, nil +} + +func (server *Server) handleTzRequest(c echo.Context) error { + // token verification + if server.authEnabled { + requestToken := c.QueryParam(server.config.Web.AuthTokenParamName) + if !isEq(server.authHashedToken, requestToken) { + server.echo.Logger.Errorf("request unauthorized, invalid token: %v", requestToken) + return c.JSON(http.StatusUnauthorized, map[string]interface{}{"message": "unauthorized"}) + } + } + // parse latitude + lat, err := parseCoordinate(c.Param(Latitude), Latitude) + if err != nil { + server.echo.Logger.Errorf("error parsing latitude: %v", err) + return c.JSON(http.StatusBadRequest, newErrResponse(err)) + } + // parse longitude + lon, err := parseCoordinate(c.Param(Longitude), Longitude) + if err != nil { + server.echo.Logger.Errorf("error parsing longitude: %v", err) + return c.JSON(http.StatusBadRequest, newErrResponse(err)) + } + + // query the coordinates + res, err := server.tzDB.Lookup(lat, lon) + switch err { + case nil: + tzr := newTzResponse(res, lat, lon) + return c.JSON(http.StatusOK, tzr) + case db.ErrNotFound: + notFoundErr := fmt.Errorf("timezone not found for coordinates %f,%f", lat, lon) + server.echo.Logger.Errorf("error querying the timezone db: %v", notFoundErr) + return c.JSON(http.StatusNotFound, newErrResponse(notFoundErr)) + default: + server.echo.Logger.Errorf("error querying the timezone db: %v", err) + return c.JSON(http.StatusInternalServerError, newErrResponse(err)) + } +} + +func newTzResponse(tzName string, lat, lon float64) map[string]any { + return map[string]any{"tz": tzName, "coords": map[string]float64{Latitude: lat, Longitude: lon}} +} + +func newErrResponse(err error) map[string]any { + return map[string]any{"message": err.Error()} +} + +func (server *Server) handleTzVersion(c echo.Context) error { + return c.JSON(http.StatusOK, server.tzRelease) +} + +// parseCoordinate parse a string into a coordinate +func parseCoordinate(val, side string) (float64, error) { + if strings.TrimSpace(val) == "" { + return 0, fmt.Errorf("empty coordinates value") + } + + c, err := strconv.ParseFloat(val, 64) + if err != nil { + return 0, fmt.Errorf("invalid type for %s, a number is required (eg. 45.3123)", side) + } + switch side { + case Latitude: + if c < -90 || c > 90 { + return 0, fmt.Errorf("%s value %s out of range (-90/+90)", side, val) + } + case Longitude: + if c < -180 || c > 180 { + return 0, fmt.Errorf("%s value %s out of range (-180/+180)", side, val) + } + } + return c, nil +} diff --git a/web/server_test.go b/web/server_test.go new file mode 100644 index 0000000..1376093 --- /dev/null +++ b/web/server_test.go @@ -0,0 +1,246 @@ +package web + +import ( + "fmt" + "net/http" + "net/http/httptest" + "reflect" + "strings" + "testing" + + "encoding/json" + + "github.com/stretchr/testify/assert" +) + +func Test_parseCoordinate(t *testing.T) { + type c struct { + val string + side string + } + tests := []struct { + ll c + want float64 + wantErr bool + }{ + {c{"22", Latitude}, 22, false}, + {c{"78.312", Longitude}, 78.312, false}, + {c{"0x429c9fbe", Longitude}, 0, true}, // 78.312 in hex + {c{"", Longitude}, 0, true}, + {c{" ", Longitude}, 0, true}, + {c{"2e4", Longitude}, 0, true}, + {c{"not a number", Longitude}, 0, true}, + {c{"-90.1", Latitude}, 0, true}, + {c{"90.001", Latitude}, 0, true}, + {c{"-180.1", Longitude}, 0, true}, + {c{"180.001", Longitude}, 0, true}, + {c{"43.42582", Latitude}, 43.42582, false}, + {c{"11.831443", Longitude}, 11.831443, false}, + } + for _, tt := range tests { + t.Run(fmt.Sprint(tt.ll), func(t *testing.T) { + got, err := parseCoordinate(tt.ll.val, tt.ll.side) + if (err != nil) != tt.wantErr { + t.Errorf("parseCoordinate() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("parseCoordinate() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_hash(t *testing.T) { + tests := []struct { + name string + args []interface{} + want []byte + }{ + { + "one element", + []interface{}{ + "test1", + }, + []byte{229, 104, 55, 204, 215, 163, 141, 103, 149, 211, 10, 194, 171, 99, 236, 204, 140, 43, 87, 18, 137, 166, 45, 196, 6, 187, 98, 118, 126, 136, 176, 108}, + }, + { + "two elements", + []interface{}{ + "test1", + "test2", + }, + []byte{84, 182, 224, 44, 5, 184, 19, 24, 41, 163, 6, 53, 242, 3, 167, 200, 192, 113, 61, 137, 208, 241, 141, 225, 134, 61, 78, 124, 88, 254, 117, 159}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := hash(tt.args...); !reflect.DeepEqual(got, tt.want) { + t.Errorf("hash() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_isEq(t *testing.T) { + type args struct { + expectedTokenHash []byte + actualToken string + } + tests := []struct { + name string + args args + want bool + }{ + { + "PASS: token matches", + args{ + []byte{229, 104, 55, 204, 215, 163, 141, 103, 149, 211, 10, 194, 171, 99, 236, 204, 140, 43, 87, 18, 137, 166, 45, 196, 6, 187, 98, 118, 126, 136, 176, 108}, + "test1", + }, + true, + }, + { + "FAIL: token mismatch", + args{ + []byte{84, 182, 224, 44, 5, 184, 19, 24, 41, 163, 6, 53, 242, 3, 167, 200, 192, 113, 61, 137, 208, 241, 141, 225, 134, 61, 78, 124, 88, 254, 117, 159}, + "test1", + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isEq(tt.args.expectedTokenHash, tt.args.actualToken); got != tt.want { + t.Errorf("isEq() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewServer(t *testing.T) { + settings := ConfigSchema{ + Tz: TzSchema{ + VersionFile: "file_not_found.json", + DatabaseName: "../tzdata/timezones.zip", + }, + } + _, err := NewServer(settings) + assert.ErrorIs(t, err, ErrorVersionFileNotFound) + + settings = ConfigSchema{ + Tz: TzSchema{ + VersionFile: "../tzdata/version.json", + DatabaseName: "timezone_not_found.db", + }, + } + _, err = NewServer(settings) + assert.ErrorIs(t, err, ErrorDatabaseFileNotFound) +} + +func Test_TzVersion(t *testing.T) { + settings := ConfigSchema{ + Tz: TzSchema{ + VersionFile: "../tzdata/version.json", + DatabaseName: "../tzdata/timezones.zip", + }, + } + server, err := NewServer(settings) + assert.NoError(t, err) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + c := server.echo.NewContext(req, rec) + c.SetPath("/tz/version") + + // Assertions + if assert.NoError(t, server.handleTzVersion(c)) { + assert.Equal(t, http.StatusOK, rec.Code) + var version TzRelease + reply := rec.Body.String() + err = json.Unmarshal([]byte(reply), &version) + assert.NoError(t, err) + assert.NotEmpty(t, version.Version) + assert.NotEmpty(t, version.URL) + assert.NotEmpty(t, version.GeoDataURL) + } +} + +func Test_TzRequest(t *testing.T) { + settings := ConfigSchema{ + Tz: TzSchema{ + VersionFile: "../tzdata/version.json", + DatabaseName: "../tzdata/timezones.zip", + }, + } + server, err := NewServer(settings) + assert.NoError(t, err) + + tests := []struct { + name string + lat string + lon string + wantCode int + wantReply string + }{ + { + "PASS: valid coordinates", + "51.477811", + "0", + http.StatusOK, + `{"coords":{"lat":51.477811,"lon":0},"tz":"Europe/London"}`, + }, + { + "PASS: valid coordinates", + "43.42582", + "11.831443", + http.StatusOK, + `{"coords":{"lat":43.42582,"lon":11.831443},"tz":"Europe/Rome"}`, + }, + { + "PASS: valid coordinates", + "41.9028", + "12.4964", + http.StatusOK, + `{"coords":{"lat":41.9028,"lon":12.4964},"tz":"Europe/Rome"}`, + }, + { + "FAIL: invalid latitude", + "100", + "11.831443", + http.StatusBadRequest, + `{"message":"lat value 100 out of range (-90/+90)"}`, + }, + { + "FAIL: invalid longitude", + "43.42582", + "200", + http.StatusBadRequest, + `{"message":"lon value 200 out of range (-180/+180)"}`, + }, + { + "FAIL: invalid latitude and longitude", + "100", + "200", + http.StatusBadRequest, + `{"message":"lat value 100 out of range (-90/+90)"}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + c := server.echo.NewContext(req, rec) + c.SetPath("/tz/:lat/:lon") + c.SetParamNames("lat", "lon") + c.SetParamValues(tt.lat, tt.lon) + + // Assertions + if assert.NoError(t, server.handleTzRequest(c)) { + assert.Equal(t, tt.wantCode, rec.Code) + assert.Equal(t, tt.wantReply, strings.TrimSpace(rec.Body.String())) + } + }) + } +}