From 8e66e5511917fd7b99ec6e9ec5b828473b0af66a Mon Sep 17 00:00:00 2001 From: Jay Brown Date: Mon, 27 Jan 2025 17:59:03 +0000 Subject: [PATCH] Merged in feature/serviceconfig (pull request #33) DRAFT PR: Prototype for serviceconfig * in progress * in progress * extract common methods * Merge remote-tracking branch 'origin/feature/serviceconfig' into feature/serviceconfig * fix types * cleanup * add tests * Merged main into feature/serviceconfig Approved-by: Michael McGuinness --- cmd/testServiceInit/main.go | 36 + go.mod | 2 + go.sum | 4 + internal/serviceconfig/common.go | 180 ++++ internal/serviceconfig/readme.md | 17 + internal/serviceconfig/service_config_test.go | 126 +++ .../github.com/caarlos0/env/v11/.editorconfig | 28 + vendor/github.com/caarlos0/env/v11/.gitignore | 5 + .../github.com/caarlos0/env/v11/.golangci.yml | 32 + .../caarlos0/env/v11/.goreleaser.yml | 5 + vendor/github.com/caarlos0/env/v11/.mailmap | 7 + vendor/github.com/caarlos0/env/v11/LICENSE.md | 21 + vendor/github.com/caarlos0/env/v11/Makefile | 37 + vendor/github.com/caarlos0/env/v11/README.md | 156 ++++ vendor/github.com/caarlos0/env/v11/env.go | 845 ++++++++++++++++++ .../github.com/caarlos0/env/v11/env_tomap.go | 16 + .../caarlos0/env/v11/env_tomap_windows.go | 29 + vendor/github.com/caarlos0/env/v11/error.go | 173 ++++ vendor/github.com/joho/godotenv/.gitignore | 1 + vendor/github.com/joho/godotenv/LICENCE | 23 + vendor/github.com/joho/godotenv/README.md | 202 +++++ vendor/github.com/joho/godotenv/godotenv.go | 228 +++++ vendor/github.com/joho/godotenv/parser.go | 271 ++++++ vendor/modules.txt | 6 + 24 files changed, 2450 insertions(+) create mode 100644 cmd/testServiceInit/main.go create mode 100644 internal/serviceconfig/common.go create mode 100644 internal/serviceconfig/readme.md create mode 100644 internal/serviceconfig/service_config_test.go create mode 100644 vendor/github.com/caarlos0/env/v11/.editorconfig create mode 100644 vendor/github.com/caarlos0/env/v11/.gitignore create mode 100644 vendor/github.com/caarlos0/env/v11/.golangci.yml create mode 100644 vendor/github.com/caarlos0/env/v11/.goreleaser.yml create mode 100644 vendor/github.com/caarlos0/env/v11/.mailmap create mode 100644 vendor/github.com/caarlos0/env/v11/LICENSE.md create mode 100644 vendor/github.com/caarlos0/env/v11/Makefile create mode 100644 vendor/github.com/caarlos0/env/v11/README.md create mode 100644 vendor/github.com/caarlos0/env/v11/env.go create mode 100644 vendor/github.com/caarlos0/env/v11/env_tomap.go create mode 100644 vendor/github.com/caarlos0/env/v11/env_tomap_windows.go create mode 100644 vendor/github.com/caarlos0/env/v11/error.go create mode 100644 vendor/github.com/joho/godotenv/.gitignore create mode 100644 vendor/github.com/joho/godotenv/LICENCE create mode 100644 vendor/github.com/joho/godotenv/README.md create mode 100644 vendor/github.com/joho/godotenv/godotenv.go create mode 100644 vendor/github.com/joho/godotenv/parser.go diff --git a/cmd/testServiceInit/main.go b/cmd/testServiceInit/main.go new file mode 100644 index 00000000..5659422d --- /dev/null +++ b/cmd/testServiceInit/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "fmt" + "os" + "queryorchestration/internal/serviceconfig" +) + +// Note: All that a service needs to have its own config is to create a new +// config type (any name you want) and add your customer variables to it that +// then call the serviceconfig.InitializeConfig() function on it. +type AnyServiceNameConfig struct { + // BaseConfig has all of the common base configuration that all services should have + // we can adjust what is in there as needed. + serviceconfig.BaseConfig // Embed the base configuration + + // add any custom values that this service needs + AppEnv string `env:"APP_ENV"` +} + +// main +// Dead simple example of of how to initialize a service custom config - AnyServiceNameConfig object in any project code. +func main() { + + // create your own customer service config + cfg := &AnyServiceNameConfig{} + // and let the shared code initialize it for you correctly. + if err := serviceconfig.InitializeConfig(cfg); err != nil { + fmt.Printf("Error initializing the custom config (see output): %v\n", err) + os.Exit(1) + } + + fmt.Printf("config: %+v\n", cfg) + cfg.Logger.Info("Service initialized ok.") + +} diff --git a/go.mod b/go.mod index c2ae8e9e..cb65a4c2 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,12 @@ require ( github.com/aws/aws-sdk-go-v2 v1.32.5 github.com/aws/aws-sdk-go-v2/config v1.28.5 github.com/aws/aws-sdk-go-v2/service/sqs v1.37.1 + github.com/caarlos0/env/v11 v11.3.1 github.com/getkin/kin-openapi v0.128.0 github.com/go-playground/validator/v10 v10.23.0 github.com/golang-migrate/migrate/v4 v4.18.1 github.com/jackc/pgx/v5 v5.7.1 + github.com/joho/godotenv v1.5.1 github.com/labstack/echo/v4 v4.13.3 github.com/lib/pq v1.10.9 github.com/oapi-codegen/runtime v1.1.1 diff --git a/go.sum b/go.sum index ab3fb738..5ec36469 100644 --- a/go.sum +++ b/go.sum @@ -44,6 +44,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.33.1/go.mod h1:GqWyYCwLXnlUB1lOAXQyN github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= +github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= @@ -135,6 +137,8 @@ github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= diff --git a/internal/serviceconfig/common.go b/internal/serviceconfig/common.go new file mode 100644 index 00000000..4207aab3 --- /dev/null +++ b/internal/serviceconfig/common.go @@ -0,0 +1,180 @@ +package serviceconfig + +import ( + "fmt" + "github.com/caarlos0/env/v11" + "github.com/joho/godotenv" + "log/slog" + "os" + "reflect" + "strings" +) + +// Common Types +// ------------ + +// DbConfig contains database connection configuration parameters +// that are shared across services. +type DbConfig struct { + DBUser string `env:"DB_USER,required,notEmpty"` // Database username + DBSecret string `env:"DB_PASS,required,notEmpty"` // Database password + DBHost string `env:"DB_HOST,required,notEmpty"` // Database host address + DBPort int `env:"DB_PORT,required,notEmpty"` // Database port number + DBName string `env:"DB_NAME,required,notEmpty"` // Database name + DBNoSSL bool `env:"DB_NOSSL" envDefault:"false"` // SSL mode configuration +} + +// LoggerConfig wraps the application logger instance. +type LoggerConfig struct { + Logger *slog.Logger // Structured logger instance +} + +// AwsConfig contains AWS credentials and configuration. +type AwsConfig struct { + AWSClientID string `env:"AWS_CLIENT_ID"` // AWS client identifier + AWSSecretKey string `env:"AWS_SECRET_KEY"` // AWS secret access key +} + +// BaseConfig provides common configuration fields and functionality +// that can be embedded in service-specific configs. +type BaseConfig struct { + LoggerConfig + DbConfig + AwsConfig +} + +// Interfaces +// ---------- + +// ConfigProvider defines the basic requirements for all configuration types. +// Implementations must provide logging capabilities and configuration display. +type ConfigProvider interface { + GetLogger() *slog.Logger + LogConfig(prefixSecret string) +} + +// Configuration Methods +// ------------------- + +// InitializeConfig sets up a configuration instance by loading environment +// variables and initializing the logger. It expects a ConfigProvider implementation. +// Returns an error if initialization fails. +func InitializeConfig(cfg ConfigProvider) error { + // First, initialize the logger by getting the base config through reflection + val := reflect.ValueOf(cfg).Elem() + + // Look for the embedded BaseConfig + var baseConfig *BaseConfig + for i := 0; i < val.NumField(); i++ { + if val.Type().Field(i).Anonymous && val.Field(i).Type() == reflect.TypeOf(BaseConfig{}) { + baseConfig = val.Field(i).Addr().Interface().(*BaseConfig) + break + } + } + + if baseConfig == nil { + return fmt.Errorf("no BaseConfig found in the provided config struct") + } + + // Initialize the logger in the base config + baseConfig.Logger = slog.New(slog.NewTextHandler(os.Stdout, nil)) + slog.SetDefault(baseConfig.Logger) + + // Load .env file + if err := godotenv.Load(); err != nil { + slog.Warn("No .env file found or error loading it", "error", err) + } + + // Parse environment variables + if err := env.Parse(cfg); err != nil { + if envErr, ok := err.(env.AggregateError); ok { + slog.Error("Missing required environment variables:") + for _, e := range envErr.Errors { + slog.Error(e.Error()) + } + } else { + slog.Error("Error parsing environment variables", "error", err) + } + return err + } + + // Log the configuration + cfg.LogConfig("secret") + return nil +} + +// GetLogger implements the ConfigProvider interface by returning +// the configured logger instance. +func (b *BaseConfig) GetLogger() *slog.Logger { + return b.Logger +} + +// Logging Methods +// -------------- + +// LogConfig displays the current configuration values, masking sensitive +// information based on the provided prefixSecret. It implements the +// ConfigProvider interface. +func (b *BaseConfig) LogConfig(prefixSecret string) { + b.logConfigRecursive(reflect.ValueOf(b), "", prefixSecret, make(map[reflect.Value]bool)) +} + +// logConfigRecursive is a helper method that recursively traverses the configuration +// structure and logs each field's value. It handles nested structs, pointers, +// and masks sensitive values containing the specified prefixSecret in their names. +func (b *BaseConfig) logConfigRecursive(val reflect.Value, prefix string, prefixSecret string, visited map[reflect.Value]bool) { + // Handle pointer dereference + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + + // Prevent infinite recursion + if visited[val] { + return + } + visited[val] = true + + typ := val.Type() + + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + fieldType := typ.Field(i) + + // Skip unexported fields + if !fieldType.IsExported() { + continue + } + + fieldName := fieldType.Name + fullPath := prefix + fieldName + + // Handle embedded fields + if fieldType.Anonymous { + b.logConfigRecursive(field, prefix, prefixSecret, visited) + continue + } + + switch field.Kind() { + case reflect.Struct: + b.logConfigRecursive(field, fullPath+".", prefixSecret, visited) + default: + var valueStr string + if field.Kind() == reflect.String { + valueStr = field.String() + } else { + valueStr = fmt.Sprintf("%v", field.Interface()) + } + + // Mask sensitive values + if strings.Contains(strings.ToLower(fieldName), strings.ToLower(prefixSecret)) { + if len(valueStr) > 5 { + valueStr = valueStr[:5] + "..." + } + } + + b.Logger.Info("Config value", + "key", fullPath, + "value", valueStr) + } + } +} diff --git a/internal/serviceconfig/readme.md b/internal/serviceconfig/readme.md new file mode 100644 index 00000000..0b25e5b1 --- /dev/null +++ b/internal/serviceconfig/readme.md @@ -0,0 +1,17 @@ +# Service Configuration +This directory contains the service configuration for the DoczyAI project. +Common code that is shared by all of the services in the project. + +Note that the library currently supports only mapping ENV (and .env) variables to the root of your +configuration struct type. Mapping is by name. See `https://github.com/caarlos0/env` for rules around tagging. + +For example: +``` +type AnyServiceNameConfig struct { + // BaseConfig has all of the common base configuration that all services should have. + serviceconfig.BaseConfig // Embed the base configuration + + // add any custom values that this service needs + SomeCustomStringValue string `env:"SOME_CUSTOM_STRING_VALUE"` +} +``` \ No newline at end of file diff --git a/internal/serviceconfig/service_config_test.go b/internal/serviceconfig/service_config_test.go new file mode 100644 index 00000000..5bdb9e0a --- /dev/null +++ b/internal/serviceconfig/service_config_test.go @@ -0,0 +1,126 @@ +package serviceconfig + +import ( + "github.com/stretchr/testify/assert" + "os" + "strings" + "testing" +) + +type TestingServiceNameConfig struct { + BaseConfig + AppEnv string `env:"APP_ENV"` + BoolTest bool `env:"BOOL_TEST"` + IntTest int `env:"INT_TEST,required,notEmpty"` +} + +func TestInitializeConfig(t *testing.T) { + tests := []struct { + name string + envVars map[string]string + wantErr bool + errMessageContains string + }{ + { + name: "valid configuration", + envVars: map[string]string{ + "APP_ENV": "testing", + "BOOL_TEST": "true", + "INT_TEST": "42", + "DB_USER": "postgres", + "DB_PASS": "pass", + "DB_HOST": "localhost", + "DB_PORT": "5432", + "DB_NAME": "query_orchestration", + "DB_NOSSL": "true", + "SUB_FIELD1:": "value1", + "SUB_FIELD2": "42", + }, + wantErr: false, + }, + { + name: "missing required env var", + envVars: map[string]string{ + "APP_ENV": "testing", + "BOOL_TEST": "true", + "DB_USER": "postgres", + "DB_PASS": "pass", + "DB_HOST": "localhost", + "DB_PORT": "5432", + "DB_NAME": "query_orchestration", + "DB_NOSSL": "true", + // INT_TEST intentionally omitted + }, + wantErr: true, + errMessageContains: "INT_TEST", + }, + { + name: "invalid boolean value", + envVars: map[string]string{ + "APP_ENV": "testing", + "BOOL_TEST": "notabool", + "INT_TEST": "42", + }, + wantErr: true, + errMessageContains: "BoolTest", + }, + { + name: "invalid integer value", + envVars: map[string]string{ + "APP_ENV": "testing", + "BOOL_TEST": "true", + "INT_TEST": "notanint", + }, + wantErr: true, + errMessageContains: "parse error on field \"IntTest\"", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Clear environment before each test + os.Clearenv() + + // Set environment variables for the test + for k, v := range tt.envVars { + os.Setenv(k, v) + } + + cfg := &TestingServiceNameConfig{} + err := InitializeConfig(cfg) + + logger := cfg.GetLogger() + assert.NotNil(t, logger) + logger.Info("Logger initialized") + + if tt.wantErr { + if err == nil { + t.Errorf("InitializeConfig() error = nil, wantErr %v", tt.wantErr) + return + } + // if we expect an error, check if the error message tt.errMessageContains is contained in the error message + if tt.errMessageContains != "" && !strings.Contains(err.Error(), tt.errMessageContains) { + t.Errorf("InitializeConfig() error = %v, want error containing %v", err, tt.errMessageContains) + } + + } else { + if err != nil { + t.Errorf("InitializeConfig() unexpected error = %v", err) + return + } + + // Verify the values were set correctly + if cfg.AppEnv != tt.envVars["APP_ENV"] { + t.Errorf("AppEnv = %v, want %v", cfg.AppEnv, tt.envVars["APP_ENV"]) + } + if cfg.BoolTest != (tt.envVars["BOOL_TEST"] == "true") { + t.Errorf("BoolTest = %v, want %v", cfg.BoolTest, tt.envVars["BOOL_TEST"] == "true") + } + expectedInt := 42 // Known value from test cases + if cfg.IntTest != expectedInt { + t.Errorf("IntTest = %v, want %v", cfg.IntTest, expectedInt) + } + } + }) + } +} diff --git a/vendor/github.com/caarlos0/env/v11/.editorconfig b/vendor/github.com/caarlos0/env/v11/.editorconfig new file mode 100644 index 00000000..0d3bd399 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/.editorconfig @@ -0,0 +1,28 @@ +[*] +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +max_line_length = 120 + +[{go.mod,go.sum,*.go}] +insert_final_newline = true +indent_size = tab +indent_style = tab +tab_width = 4 + +[Makefile] +max_line_length = off +insert_final_newline = true +indent_size = tab +indent_style = tab +tab_width = 4 + +[*.md] +max_line_length = off +trim_trailing_whitespace = false +indent_size = tab +indent_style = space +tab_width = 2 + +[.mailmap] +max_line_length = off diff --git a/vendor/github.com/caarlos0/env/v11/.gitignore b/vendor/github.com/caarlos0/env/v11/.gitignore new file mode 100644 index 00000000..9eea1c98 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/.gitignore @@ -0,0 +1,5 @@ +coverage.txt +bin +card.png +dist +codecov* diff --git a/vendor/github.com/caarlos0/env/v11/.golangci.yml b/vendor/github.com/caarlos0/env/v11/.golangci.yml new file mode 100644 index 00000000..8e51bdcc --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/.golangci.yml @@ -0,0 +1,32 @@ +linters-settings: + gocritic: + enabled-checks: + - emptyStringTest + - evalOrder + - paramTypeCombine + - preferStringWriter + - sprintfQuotedString + - stringConcatSimplify + - yodaStyleExpr + revive: + rules: + - name: line-length-limit + arguments: [120] + +issues: + exclude-rules: + - path: _test\.go + linters: + - revive + text: "line-length-limit:" + +linters: + enable: + - thelper + - gofumpt + - gocritic + - tparallel + - unconvert + - unparam + - wastedassign + - revive diff --git a/vendor/github.com/caarlos0/env/v11/.goreleaser.yml b/vendor/github.com/caarlos0/env/v11/.goreleaser.yml new file mode 100644 index 00000000..a5b7d4d2 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/.goreleaser.yml @@ -0,0 +1,5 @@ +# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json +version: 2 +includes: + - from_url: + url: https://raw.githubusercontent.com/caarlos0/.goreleaserfiles/main/lib.yml diff --git a/vendor/github.com/caarlos0/env/v11/.mailmap b/vendor/github.com/caarlos0/env/v11/.mailmap new file mode 100644 index 00000000..eeeee601 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/.mailmap @@ -0,0 +1,7 @@ +Carlos Alexandro Becker Carlos A Becker +Carlos Alexandro Becker Carlos A Becker +Carlos Alexandro Becker Carlos Alexandro Becker +Carlos Alexandro Becker Carlos Alexandro Becker +Carlos Alexandro Becker Carlos Becker +dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> +actions-user github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> diff --git a/vendor/github.com/caarlos0/env/v11/LICENSE.md b/vendor/github.com/caarlos0/env/v11/LICENSE.md new file mode 100644 index 00000000..28463401 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015-2024 Carlos Alexandro Becker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/caarlos0/env/v11/Makefile b/vendor/github.com/caarlos0/env/v11/Makefile new file mode 100644 index 00000000..da8595fb --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/Makefile @@ -0,0 +1,37 @@ +SOURCE_FILES?=./... +TEST_PATTERN?=. + +export GO111MODULE := on + +setup: + go mod tidy +.PHONY: setup + +build: + go build +.PHONY: build + +test: + go test -v -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt $(SOURCE_FILES) -run $(TEST_PATTERN) -timeout=2m +.PHONY: test + +cover: test + go tool cover -html=coverage.txt +.PHONY: cover + +fmt: + gofumpt -w -l . +.PHONY: fmt + +lint: + golangci-lint run ./... +.PHONY: lint + +ci: build test +.PHONY: ci + +card: + wget -O card.png -c "https://og.caarlos0.dev/**env**: parse envs to structs.png?theme=light&md=1&fontSize=100px&images=https://github.com/caarlos0.png" +.PHONY: card + +.DEFAULT_GOAL := ci diff --git a/vendor/github.com/caarlos0/env/v11/README.md b/vendor/github.com/caarlos0/env/v11/README.md new file mode 100644 index 00000000..de6f8249 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/README.md @@ -0,0 +1,156 @@ +

+ GoReleaser Logo +

A simple, zero-dependencies library to parse environment variables into structs.

+

+ +###### Installation + +```bash +go get github.com/caarlos0/env/v11 +``` + +###### Getting started + +```go +type config struct { + Home string `env:"HOME"` +} + +// parse +var cfg config +err := env.Parse(&cfg) + +// parse with generics +cfg, err := env.ParseAs[config]() +``` + +You can see the full documentation and list of examples at [pkg.go.dev](https://pkg.go.dev/github.com/caarlos0/env/v11). + +--- + +## Used and supported by + +

+ + encore icon + +
+
+ Encore – the platform for building Go-based cloud backends. +
+

+ +## Usage + +### Caveats + +> [!CAUTION] +> +> _Unexported fields_ will be **ignored** by `env`. +> This is by design and will not change. + +### Functions + +- `Parse`: parse the current environment into a type +- `ParseAs`: parse the current environment into a type using generics +- `ParseWithOptions`: parse the current environment into a type with custom options +- `ParseAsWithOptions`: parse the current environment into a type with custom options and using generics +- `Must`: can be used to wrap `Parse.*` calls to panic on error +- `GetFieldParams`: get the `env` parsed options for a type +- `GetFieldParamsWithOptions`: get the `env` parsed options for a type with custom options + +### Supported types + +Out of the box all built-in types are supported, plus a few others that are commonly used. + +Complete list: + +- `bool` +- `float32` +- `float64` +- `int16` +- `int32` +- `int64` +- `int8` +- `int` +- `string` +- `uint16` +- `uint32` +- `uint64` +- `uint8` +- `uint` +- `time.Duration` +- `time.Location` +- `encoding.TextUnmarshaler` +- `url.URL` + +Pointers, slices and slices of pointers, and maps of those types are also supported. + +You may also add custom parsers for your types. + +### Tags + +The following tags are provided: + +- `env`: sets the environment variable name and optionally takes the tag options described below +- `envDefault`: sets the default value for the field +- `envPrefix`: can be used in a field that is a complex type to set a prefix to all environment variables used in it +- `envSeparator`: sets the character to be used to separate items in slices and maps (default: `,`) +- `envKeyValSeparator`: sets the character to be used to separate keys and their values in maps (default: `:`) + +### `env` tag options + +Here are all the options available for the `env` tag: + +- `,expand`: expands environment variables, e.g. `FOO_${BAR}` +- `,file`: instructs that the content of the variable is a path to a file that should be read +- `,init`: initialize nil pointers +- `,notEmpty`: make the field errors if the environment variable is empty +- `,required`: make the field errors if the environment variable is not set +- `,unset`: unset the environment variable after use + +### Parse Options + +There are a few options available in the functions that end with `WithOptions`: + +- `Environment`: keys and values to be used instead of `os.Environ()` +- `TagName`: specifies another tag name to use rather than the default `env` +- `PrefixTagName`: specifies another prefix tag name to use rather than the default `envPrefix` +- `DefaultValueTagName`: specifies another default tag name to use rather than the default `envDefault` +- `RequiredIfNoDef`: set all `env` fields as required if they do not declare `envDefault` +- `OnSet`: allows to hook into the `env` parsing and do something when a value is set +- `Prefix`: prefix to be used in all environment variables +- `UseFieldNameByDefault`: defines whether or not `env` should use the field name by default if the `env` key is missing +- `FuncMap`: custom parse functions for custom types + +### Documentation and examples + +Examples are live in [pkg.go.dev](https://pkg.go.dev/github.com/caarlos0/env/v11), +and also in the [example test file](./example_test.go). + +## Current state + +`env` is considered feature-complete. + +I do not intent to add any new features unless they really make sense, and are +requested by many people. + +Eventual bug fixes will keep being merged. + +## Badges + +[![Release](https://img.shields.io/github/release/caarlos0/env.svg?style=for-the-badge)](https://github.com/goreleaser/goreleaser/releases/latest) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=for-the-badge)](/LICENSE.md) +[![Build status](https://img.shields.io/github/actions/workflow/status/caarlos0/env/build.yml?style=for-the-badge&branch=main)](https://github.com/caarlos0/env/actions?workflow=build) +[![Codecov branch](https://img.shields.io/codecov/c/github/caarlos0/env/main.svg?style=for-the-badge)](https://codecov.io/gh/caarlos0/env) +[![Go docs](https://img.shields.io/badge/godoc-reference-blue.svg?style=for-the-badge)](http://godoc.org/github.com/caarlos0/env/v11) +[![Powered By: GoReleaser](https://img.shields.io/badge/powered%20by-goreleaser-green.svg?style=for-the-badge)](https://github.com/goreleaser) +[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg?style=for-the-badge)](https://conventionalcommits.org) + +## Related projects + +- [envdoc](https://github.com/g4s8/envdoc) - generate documentation for environment variables from `env` tags + +## Stargazers over time + +[![Stargazers over time](https://starchart.cc/caarlos0/env.svg)](https://starchart.cc/caarlos0/env) diff --git a/vendor/github.com/caarlos0/env/v11/env.go b/vendor/github.com/caarlos0/env/v11/env.go new file mode 100644 index 00000000..805ee2de --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/env.go @@ -0,0 +1,845 @@ +// Package env is a simple, zero-dependencies library to parse environment +// variables into structs. +// +// Example: +// +// type config struct { +// Home string `env:"HOME"` +// } +// // parse +// var cfg config +// err := env.Parse(&cfg) +// // or parse with generics +// cfg, err := env.ParseAs[config]() +// +// Check the examples and README for more detailed usage. +package env + +import ( + "encoding" + "fmt" + "net/url" + "os" + "reflect" + "strconv" + "strings" + "time" + "unicode" +) + +// nolint: gochecknoglobals +var ( + defaultBuiltInParsers = map[reflect.Kind]ParserFunc{ + reflect.Bool: func(v string) (interface{}, error) { + return strconv.ParseBool(v) + }, + reflect.String: func(v string) (interface{}, error) { + return v, nil + }, + reflect.Int: func(v string) (interface{}, error) { + i, err := strconv.ParseInt(v, 10, 32) + return int(i), err + }, + reflect.Int16: func(v string) (interface{}, error) { + i, err := strconv.ParseInt(v, 10, 16) + return int16(i), err + }, + reflect.Int32: func(v string) (interface{}, error) { + i, err := strconv.ParseInt(v, 10, 32) + return int32(i), err + }, + reflect.Int64: func(v string) (interface{}, error) { + return strconv.ParseInt(v, 10, 64) + }, + reflect.Int8: func(v string) (interface{}, error) { + i, err := strconv.ParseInt(v, 10, 8) + return int8(i), err + }, + reflect.Uint: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 32) + return uint(i), err + }, + reflect.Uint16: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 16) + return uint16(i), err + }, + reflect.Uint32: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 32) + return uint32(i), err + }, + reflect.Uint64: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 64) + return i, err + }, + reflect.Uint8: func(v string) (interface{}, error) { + i, err := strconv.ParseUint(v, 10, 8) + return uint8(i), err + }, + reflect.Float64: func(v string) (interface{}, error) { + return strconv.ParseFloat(v, 64) + }, + reflect.Float32: func(v string) (interface{}, error) { + f, err := strconv.ParseFloat(v, 32) + return float32(f), err + }, + } +) + +func defaultTypeParsers() map[reflect.Type]ParserFunc { + return map[reflect.Type]ParserFunc{ + reflect.TypeOf(url.URL{}): parseURL, + reflect.TypeOf(time.Nanosecond): parseDuration, + reflect.TypeOf(time.Location{}): parseLocation, + } +} + +func parseURL(v string) (interface{}, error) { + u, err := url.Parse(v) + if err != nil { + return nil, newParseValueError("unable to parse URL", err) + } + return *u, nil +} + +func parseDuration(v string) (interface{}, error) { + d, err := time.ParseDuration(v) + if err != nil { + return nil, newParseValueError("unable to parse duration", err) + } + return d, err +} + +func parseLocation(v string) (interface{}, error) { + loc, err := time.LoadLocation(v) + if err != nil { + return nil, newParseValueError("unable to parse location", err) + } + return *loc, nil +} + +// ParserFunc defines the signature of a function that can be used within +// `Options`' `FuncMap`. +type ParserFunc func(v string) (interface{}, error) + +// OnSetFn is a hook that can be run when a value is set. +type OnSetFn func(tag string, value interface{}, isDefault bool) + +// processFieldFn is a function which takes all information about a field and processes it. +type processFieldFn func( + refField reflect.Value, + refTypeField reflect.StructField, + opts Options, + fieldParams FieldParams, +) error + +// Options for the parser. +type Options struct { + // Environment keys and values that will be accessible for the service. + Environment map[string]string + + // TagName specifies another tag name to use rather than the default 'env'. + TagName string + + // PrefixTagName specifies another prefix tag name to use rather than the default 'envPrefix'. + PrefixTagName string + + // DefaultValueTagName specifies another default tag name to use rather than the default 'envDefault'. + DefaultValueTagName string + + // RequiredIfNoDef automatically sets all fields as required if they do not + // declare 'envDefault'. + RequiredIfNoDef bool + + // OnSet allows to run a function when a value is set. + OnSet OnSetFn + + // Prefix define a prefix for every key. + Prefix string + + // UseFieldNameByDefault defines whether or not `env` should use the field + // name by default if the `env` key is missing. + // Note that the field name will be "converted" to conform with environment + // variable names conventions. + UseFieldNameByDefault bool + + // Custom parse functions for different types. + FuncMap map[reflect.Type]ParserFunc + + // Used internally. maps the env variable key to its resolved string value. + // (for env var expansion) + rawEnvVars map[string]string +} + +func (opts *Options) getRawEnv(s string) string { + val := opts.rawEnvVars[s] + if val == "" { + val = opts.Environment[s] + } + return os.Expand(val, opts.getRawEnv) +} + +func defaultOptions() Options { + return Options{ + TagName: "env", + PrefixTagName: "envPrefix", + DefaultValueTagName: "envDefault", + Environment: toMap(os.Environ()), + FuncMap: defaultTypeParsers(), + rawEnvVars: make(map[string]string), + } +} + +func mergeOptions[T any](target, source *T) { + targetPtr := reflect.ValueOf(target).Elem() + sourcePtr := reflect.ValueOf(source).Elem() + + targetType := targetPtr.Type() + for i := 0; i < targetPtr.NumField(); i++ { + fieldName := targetType.Field(i).Name + targetField := targetPtr.Field(i) + sourceField := sourcePtr.FieldByName(fieldName) + + if targetField.CanSet() && !isZero(sourceField) { + // FuncMaps are being merged, while Environments must be overwritten + if fieldName == "FuncMap" { + if !sourceField.IsZero() { + iter := sourceField.MapRange() + for iter.Next() { + targetField.SetMapIndex(iter.Key(), iter.Value()) + } + } + } else { + targetField.Set(sourceField) + } + } + } +} + +func isZero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Func, reflect.Map, reflect.Slice: + return v.IsNil() + default: + zero := reflect.Zero(v.Type()) + return v.Interface() == zero.Interface() + } +} + +func customOptions(opts Options) Options { + defOpts := defaultOptions() + mergeOptions(&defOpts, &opts) + return defOpts +} + +func optionsWithSliceEnvPrefix(opts Options, index int) Options { + return Options{ + Environment: opts.Environment, + TagName: opts.TagName, + PrefixTagName: opts.PrefixTagName, + DefaultValueTagName: opts.DefaultValueTagName, + RequiredIfNoDef: opts.RequiredIfNoDef, + OnSet: opts.OnSet, + Prefix: fmt.Sprintf("%s%d_", opts.Prefix, index), + UseFieldNameByDefault: opts.UseFieldNameByDefault, + FuncMap: opts.FuncMap, + rawEnvVars: opts.rawEnvVars, + } +} + +func optionsWithEnvPrefix(field reflect.StructField, opts Options) Options { + return Options{ + Environment: opts.Environment, + TagName: opts.TagName, + PrefixTagName: opts.PrefixTagName, + DefaultValueTagName: opts.DefaultValueTagName, + RequiredIfNoDef: opts.RequiredIfNoDef, + OnSet: opts.OnSet, + Prefix: opts.Prefix + field.Tag.Get(opts.PrefixTagName), + UseFieldNameByDefault: opts.UseFieldNameByDefault, + FuncMap: opts.FuncMap, + rawEnvVars: opts.rawEnvVars, + } +} + +// Parse parses a struct containing `env` tags and loads its values from +// environment variables. +func Parse(v interface{}) error { + return parseInternal(v, setField, defaultOptions()) +} + +// ParseWithOptions parses a struct containing `env` tags and loads its values from +// environment variables. +func ParseWithOptions(v interface{}, opts Options) error { + return parseInternal(v, setField, customOptions(opts)) +} + +// ParseAs parses the given struct type containing `env` tags and loads its +// values from environment variables. +func ParseAs[T any]() (T, error) { + var t T + err := Parse(&t) + return t, err +} + +// ParseWithOptions parses the given struct type containing `env` tags and +// loads its values from environment variables. +func ParseAsWithOptions[T any](opts Options) (T, error) { + var t T + err := ParseWithOptions(&t, opts) + return t, err +} + +// Must panic is if err is not nil, and returns t otherwise. +func Must[T any](t T, err error) T { + if err != nil { + panic(err) + } + return t +} + +// GetFieldParams parses a struct containing `env` tags and returns information about +// tags it found. +func GetFieldParams(v interface{}) ([]FieldParams, error) { + return GetFieldParamsWithOptions(v, defaultOptions()) +} + +// GetFieldParamsWithOptions parses a struct containing `env` tags and returns information about +// tags it found. +func GetFieldParamsWithOptions(v interface{}, opts Options) ([]FieldParams, error) { + var result []FieldParams + err := parseInternal( + v, + func(_ reflect.Value, _ reflect.StructField, _ Options, fieldParams FieldParams) error { + if fieldParams.OwnKey != "" { + result = append(result, fieldParams) + } + return nil + }, + customOptions(opts), + ) + if err != nil { + return nil, err + } + + return result, nil +} + +func parseInternal(v interface{}, processField processFieldFn, opts Options) error { + ptrRef := reflect.ValueOf(v) + if ptrRef.Kind() != reflect.Ptr { + return newAggregateError(NotStructPtrError{}) + } + ref := ptrRef.Elem() + if ref.Kind() != reflect.Struct { + return newAggregateError(NotStructPtrError{}) + } + + return doParse(ref, processField, opts) +} + +func doParse(ref reflect.Value, processField processFieldFn, opts Options) error { + refType := ref.Type() + + var agrErr AggregateError + + for i := 0; i < refType.NumField(); i++ { + refField := ref.Field(i) + refTypeField := refType.Field(i) + + if err := doParseField(refField, refTypeField, processField, opts); err != nil { + if val, ok := err.(AggregateError); ok { + agrErr.Errors = append(agrErr.Errors, val.Errors...) + } else { + agrErr.Errors = append(agrErr.Errors, err) + } + } + } + + if len(agrErr.Errors) == 0 { + return nil + } + + return agrErr +} + +func doParseField( + refField reflect.Value, + refTypeField reflect.StructField, + processField processFieldFn, + opts Options, +) error { + if !refField.CanSet() { + return nil + } + if refField.Kind() == reflect.Ptr && refField.Elem().Kind() == reflect.Struct && !refField.IsNil() { + return parseInternal(refField.Interface(), processField, optionsWithEnvPrefix(refTypeField, opts)) + } + if refField.Kind() == reflect.Struct && refField.CanAddr() && refField.Type().Name() == "" { + return parseInternal(refField.Addr().Interface(), processField, optionsWithEnvPrefix(refTypeField, opts)) + } + + params, err := parseFieldParams(refTypeField, opts) + if err != nil { + return err + } + + if params.Ignored { + return nil + } + + if err := processField(refField, refTypeField, opts, params); err != nil { + return err + } + + if params.Init && isInvalidPtr(refField) { + refField.Set(reflect.New(refField.Type().Elem())) + refField = refField.Elem() + } + + if refField.Kind() == reflect.Struct { + return doParse(refField, processField, optionsWithEnvPrefix(refTypeField, opts)) + } + + if isSliceOfStructs(refTypeField) { + return doParseSlice(refField, processField, optionsWithEnvPrefix(refTypeField, opts)) + } + + return nil +} + +func isSliceOfStructs(refTypeField reflect.StructField) bool { + field := refTypeField.Type + + // *[]struct + if field.Kind() == reflect.Ptr { + field = field.Elem() + if field.Kind() == reflect.Slice && field.Elem().Kind() == reflect.Struct { + return true + } + } + + // []struct{} + if field.Kind() == reflect.Slice && field.Elem().Kind() == reflect.Struct { + return true + } + + return false +} + +func doParseSlice(ref reflect.Value, processField processFieldFn, opts Options) error { + if opts.Prefix != "" && !strings.HasSuffix(opts.Prefix, string(underscore)) { + opts.Prefix += string(underscore) + } + + var environments []string + for environment := range opts.Environment { + if strings.HasPrefix(environment, opts.Prefix) { + environments = append(environments, environment) + } + } + + if len(environments) > 0 { + counter := 0 + for finished := false; !finished; { + finished = true + prefix := fmt.Sprintf("%s%d%c", opts.Prefix, counter, underscore) + for _, variable := range environments { + if strings.HasPrefix(variable, prefix) { + counter++ + finished = false + break + } + } + } + + sliceType := ref.Type() + var initialized int + if reflect.Ptr == ref.Kind() { + sliceType = sliceType.Elem() + // Due to the rest of code the pre-initialized slice has no chance for this situation + initialized = 0 + } else { + initialized = ref.Len() + } + + var capacity int + if capacity = initialized; counter > initialized { + capacity = counter + } + result := reflect.MakeSlice(sliceType, capacity, capacity) + for i := 0; i < capacity; i++ { + item := result.Index(i) + if i < initialized { + item.Set(ref.Index(i)) + } + if err := doParse(item, processField, optionsWithSliceEnvPrefix(opts, i)); err != nil { + return err + } + } + + if result.Len() > 0 { + if reflect.Ptr == ref.Kind() { + resultPtr := reflect.New(sliceType) + resultPtr.Elem().Set(result) + result = resultPtr + } + ref.Set(result) + } + } + + return nil +} + +func setField(refField reflect.Value, refTypeField reflect.StructField, opts Options, fieldParams FieldParams) error { + value, err := get(fieldParams, opts) + if err != nil { + return err + } + + if value != "" { + return set(refField, refTypeField, value, opts.FuncMap) + } + + return nil +} + +const underscore rune = '_' + +func toEnvName(input string) string { + var output []rune + for i, c := range input { + if c == underscore { + continue + } + if len(output) > 0 && unicode.IsUpper(c) { + if len(input) > i+1 { + peek := rune(input[i+1]) + if unicode.IsLower(peek) || unicode.IsLower(rune(input[i-1])) { + output = append(output, underscore) + } + } + } + output = append(output, unicode.ToUpper(c)) + } + return string(output) +} + +// FieldParams contains information about parsed field tags. +type FieldParams struct { + OwnKey string + Key string + DefaultValue string + HasDefaultValue bool + Required bool + LoadFile bool + Unset bool + NotEmpty bool + Expand bool + Init bool + Ignored bool +} + +func parseFieldParams(field reflect.StructField, opts Options) (FieldParams, error) { + ownKey, tags := parseKeyForOption(field.Tag.Get(opts.TagName)) + if ownKey == "" && opts.UseFieldNameByDefault { + ownKey = toEnvName(field.Name) + } + + defaultValue, hasDefaultValue := field.Tag.Lookup(opts.DefaultValueTagName) + + result := FieldParams{ + OwnKey: ownKey, + Key: opts.Prefix + ownKey, + Required: opts.RequiredIfNoDef, + DefaultValue: defaultValue, + HasDefaultValue: hasDefaultValue, + Ignored: ownKey == "-", + } + + for _, tag := range tags { + switch tag { + case "": + continue + case "file": + result.LoadFile = true + case "required": + result.Required = true + case "unset": + result.Unset = true + case "notEmpty": + result.NotEmpty = true + case "expand": + result.Expand = true + case "init": + result.Init = true + case "-": + result.Ignored = true + default: + return FieldParams{}, newNoSupportedTagOptionError(tag) + } + } + + return result, nil +} + +func get(fieldParams FieldParams, opts Options) (val string, err error) { + var exists, isDefault bool + + val, exists, isDefault = getOr( + fieldParams.Key, + fieldParams.DefaultValue, + fieldParams.HasDefaultValue, + opts.Environment, + ) + + if fieldParams.Expand { + val = os.Expand(val, opts.getRawEnv) + } + + opts.rawEnvVars[fieldParams.OwnKey] = val + + if fieldParams.Unset { + defer os.Unsetenv(fieldParams.Key) + } + + if fieldParams.Required && !exists && fieldParams.OwnKey != "" { + return "", newVarIsNotSetError(fieldParams.Key) + } + + if fieldParams.NotEmpty && val == "" { + return "", newEmptyVarError(fieldParams.Key) + } + + if fieldParams.LoadFile && val != "" { + filename := val + val, err = getFromFile(filename) + if err != nil { + return "", newLoadFileContentError(filename, fieldParams.Key, err) + } + } + + if opts.OnSet != nil { + if fieldParams.OwnKey != "" { + opts.OnSet(fieldParams.Key, val, isDefault) + } + } + return val, err +} + +// split the env tag's key into the expected key and desired option, if any. +func parseKeyForOption(key string) (string, []string) { + opts := strings.Split(key, ",") + return opts[0], opts[1:] +} + +func getFromFile(filename string) (value string, err error) { + b, err := os.ReadFile(filename) + return string(b), err +} + +func getOr(key, defaultValue string, defExists bool, envs map[string]string) (val string, exists, isDefault bool) { + value, exists := envs[key] + switch { + case (!exists || key == "") && defExists: + return defaultValue, true, true + case exists && value == "" && defExists: + return defaultValue, true, true + case !exists: + return "", false, false + } + + return value, true, false +} + +func set(field reflect.Value, sf reflect.StructField, value string, funcMap map[reflect.Type]ParserFunc) error { + if tm := asTextUnmarshaler(field); tm != nil { + if err := tm.UnmarshalText([]byte(value)); err != nil { + return newParseError(sf, err) + } + return nil + } + + typee := sf.Type + fieldee := field + if typee.Kind() == reflect.Ptr { + typee = typee.Elem() + fieldee = field.Elem() + } + + parserFunc, ok := funcMap[typee] + if ok { + val, err := parserFunc(value) + if err != nil { + return newParseError(sf, err) + } + + fieldee.Set(reflect.ValueOf(val)) + return nil + } + + parserFunc, ok = defaultBuiltInParsers[typee.Kind()] + if ok { + val, err := parserFunc(value) + if err != nil { + return newParseError(sf, err) + } + + fieldee.Set(reflect.ValueOf(val).Convert(typee)) + return nil + } + + switch field.Kind() { + case reflect.Slice: + return handleSlice(field, value, sf, funcMap) + case reflect.Map: + return handleMap(field, value, sf, funcMap) + } + + return newNoParserError(sf) +} + +func handleSlice(field reflect.Value, value string, sf reflect.StructField, funcMap map[reflect.Type]ParserFunc) error { + separator := sf.Tag.Get("envSeparator") + if separator == "" { + separator = "," + } + parts := strings.Split(value, separator) + + typee := sf.Type.Elem() + if typee.Kind() == reflect.Ptr { + typee = typee.Elem() + } + + if _, ok := reflect.New(typee).Interface().(encoding.TextUnmarshaler); ok { + return parseTextUnmarshalers(field, parts, sf) + } + + parserFunc, ok := funcMap[typee] + if !ok { + parserFunc, ok = defaultBuiltInParsers[typee.Kind()] + if !ok { + return newNoParserError(sf) + } + } + + result := reflect.MakeSlice(sf.Type, 0, len(parts)) + for _, part := range parts { + r, err := parserFunc(part) + if err != nil { + return newParseError(sf, err) + } + v := reflect.ValueOf(r).Convert(typee) + if sf.Type.Elem().Kind() == reflect.Ptr { + v = reflect.New(typee) + v.Elem().Set(reflect.ValueOf(r).Convert(typee)) + } + result = reflect.Append(result, v) + } + field.Set(result) + return nil +} + +func handleMap(field reflect.Value, value string, sf reflect.StructField, funcMap map[reflect.Type]ParserFunc) error { + keyType := sf.Type.Key() + keyParserFunc, ok := funcMap[keyType] + if !ok { + keyParserFunc, ok = defaultBuiltInParsers[keyType.Kind()] + if !ok { + return newNoParserError(sf) + } + } + + elemType := sf.Type.Elem() + elemParserFunc, ok := funcMap[elemType] + if !ok { + elemParserFunc, ok = defaultBuiltInParsers[elemType.Kind()] + if !ok { + return newNoParserError(sf) + } + } + + separator := sf.Tag.Get("envSeparator") + if separator == "" { + separator = "," + } + + keyValSeparator := sf.Tag.Get("envKeyValSeparator") + if keyValSeparator == "" { + keyValSeparator = ":" + } + + result := reflect.MakeMap(sf.Type) + for _, part := range strings.Split(value, separator) { + pairs := strings.SplitN(part, keyValSeparator, 2) + if len(pairs) != 2 { + return newParseError(sf, fmt.Errorf(`%q should be in "key%svalue" format`, part, keyValSeparator)) + } + + key, err := keyParserFunc(pairs[0]) + if err != nil { + return newParseError(sf, err) + } + + elem, err := elemParserFunc(pairs[1]) + if err != nil { + return newParseError(sf, err) + } + + result.SetMapIndex(reflect.ValueOf(key).Convert(keyType), reflect.ValueOf(elem).Convert(elemType)) + } + + field.Set(result) + return nil +} + +func asTextUnmarshaler(field reflect.Value) encoding.TextUnmarshaler { + if field.Kind() == reflect.Ptr { + if field.IsNil() { + field.Set(reflect.New(field.Type().Elem())) + } + } else if field.CanAddr() { + field = field.Addr() + } + + tm, ok := field.Interface().(encoding.TextUnmarshaler) + if !ok { + return nil + } + return tm +} + +func parseTextUnmarshalers(field reflect.Value, data []string, sf reflect.StructField) error { + s := len(data) + elemType := field.Type().Elem() + slice := reflect.MakeSlice(reflect.SliceOf(elemType), s, s) + for i, v := range data { + sv := slice.Index(i) + kind := sv.Kind() + if kind == reflect.Ptr { + sv = reflect.New(elemType.Elem()) + } else { + sv = sv.Addr() + } + tm := sv.Interface().(encoding.TextUnmarshaler) + if err := tm.UnmarshalText([]byte(v)); err != nil { + return newParseError(sf, err) + } + if kind == reflect.Ptr { + slice.Index(i).Set(sv) + } + } + + field.Set(slice) + + return nil +} + +// ToMap Converts list of env vars as provided by os.Environ() to map you +// can use as Options.Environment field +func ToMap(env []string) map[string]string { + return toMap(env) +} + +func isInvalidPtr(v reflect.Value) bool { + return reflect.Ptr == v.Kind() && v.Elem().Kind() == reflect.Invalid +} diff --git a/vendor/github.com/caarlos0/env/v11/env_tomap.go b/vendor/github.com/caarlos0/env/v11/env_tomap.go new file mode 100644 index 00000000..aece2ae9 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/env_tomap.go @@ -0,0 +1,16 @@ +//go:build !windows + +package env + +import "strings" + +func toMap(env []string) map[string]string { + r := map[string]string{} + for _, e := range env { + p := strings.SplitN(e, "=", 2) + if len(p) == 2 { + r[p[0]] = p[1] + } + } + return r +} diff --git a/vendor/github.com/caarlos0/env/v11/env_tomap_windows.go b/vendor/github.com/caarlos0/env/v11/env_tomap_windows.go new file mode 100644 index 00000000..04ce66f5 --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/env_tomap_windows.go @@ -0,0 +1,29 @@ +//go:build windows + +package env + +import "strings" + +func toMap(env []string) map[string]string { + r := map[string]string{} + for _, e := range env { + p := strings.SplitN(e, "=", 2) + + // On Windows, environment variables can start with '='. If so, Split at next character. + // See env_windows.go in the Go source: https://github.com/golang/go/blob/master/src/syscall/env_windows.go#L58 + prefixEqualSign := false + if len(e) > 0 && e[0] == '=' { + e = e[1:] + prefixEqualSign = true + } + p = strings.SplitN(e, "=", 2) + if prefixEqualSign { + p[0] = "=" + p[0] + } + + if len(p) == 2 { + r[p[0]] = p[1] + } + } + return r +} diff --git a/vendor/github.com/caarlos0/env/v11/error.go b/vendor/github.com/caarlos0/env/v11/error.go new file mode 100644 index 00000000..d615912c --- /dev/null +++ b/vendor/github.com/caarlos0/env/v11/error.go @@ -0,0 +1,173 @@ +package env + +import ( + "fmt" + "reflect" + "strings" +) + +// An aggregated error wrapper to combine gathered errors. +// This allows either to display all errors or convert them individually +// List of the available errors +// ParseError +// NotStructPtrError +// NoParserError +// NoSupportedTagOptionError +// VarIsNotSetError +// EmptyVarError +// LoadFileContentError +// ParseValueError +type AggregateError struct { + Errors []error +} + +func newAggregateError(initErr error) error { + return AggregateError{ + []error{ + initErr, + }, + } +} + +func (e AggregateError) Error() string { + var sb strings.Builder + + sb.WriteString("env:") + + for _, err := range e.Errors { + sb.WriteString(fmt.Sprintf(" %v;", err.Error())) + } + + return strings.TrimRight(sb.String(), ";") +} + +// Unwrap implements std errors.Join go1.20 compatibility +func (e AggregateError) Unwrap() []error { + return e.Errors +} + +// Is conforms with errors.Is. +func (e AggregateError) Is(err error) bool { + for _, ie := range e.Errors { + if reflect.TypeOf(ie) == reflect.TypeOf(err) { + return true + } + } + return false +} + +// The error occurs when it's impossible to convert the value for given type. +type ParseError struct { + Name string + Type reflect.Type + Err error +} + +func newParseError(sf reflect.StructField, err error) error { + return ParseError{sf.Name, sf.Type, err} +} + +func (e ParseError) Error() string { + return fmt.Sprintf("parse error on field %q of type %q: %v", e.Name, e.Type, e.Err) +} + +// The error occurs when pass something that is not a pointer to a struct to Parse +type NotStructPtrError struct{} + +func (e NotStructPtrError) Error() string { + return "expected a pointer to a Struct" +} + +// This error occurs when there is no parser provided for given type. +type NoParserError struct { + Name string + Type reflect.Type +} + +func newNoParserError(sf reflect.StructField) error { + return NoParserError{sf.Name, sf.Type} +} + +func (e NoParserError) Error() string { + return fmt.Sprintf("no parser found for field %q of type %q", e.Name, e.Type) +} + +// This error occurs when the given tag is not supported. +// Built-in supported tags: "", "file", "required", "unset", "notEmpty", +// "expand", "envDefault", and "envSeparator". +type NoSupportedTagOptionError struct { + Tag string +} + +func newNoSupportedTagOptionError(tag string) error { + return NoSupportedTagOptionError{tag} +} + +func (e NoSupportedTagOptionError) Error() string { + return fmt.Sprintf("tag option %q not supported", e.Tag) +} + +// This error occurs when the required variable is not set. +// +// Deprecated: use VarIsNotSetError. +type EnvVarIsNotSetError = VarIsNotSetError + +// This error occurs when the required variable is not set. +type VarIsNotSetError struct { + Key string +} + +func newVarIsNotSetError(key string) error { + return VarIsNotSetError{key} +} + +func (e VarIsNotSetError) Error() string { + return fmt.Sprintf(`required environment variable %q is not set`, e.Key) +} + +// This error occurs when the variable which must be not empty is existing but has an empty value +// +// Deprecated: use EmptyVarError. +type EmptyEnvVarError = EmptyVarError + +// This error occurs when the variable which must be not empty is existing but has an empty value +type EmptyVarError struct { + Key string +} + +func newEmptyVarError(key string) error { + return EmptyVarError{key} +} + +func (e EmptyVarError) Error() string { + return fmt.Sprintf("environment variable %q should not be empty", e.Key) +} + +// This error occurs when it's impossible to load the value from the file. +type LoadFileContentError struct { + Filename string + Key string + Err error +} + +func newLoadFileContentError(filename, key string, err error) error { + return LoadFileContentError{filename, key, err} +} + +func (e LoadFileContentError) Error() string { + return fmt.Sprintf("could not load content of file %q from variable %s: %v", e.Filename, e.Key, e.Err) +} + +// This error occurs when it's impossible to convert value using given parser. +type ParseValueError struct { + Msg string + Err error +} + +func newParseValueError(message string, err error) error { + return ParseValueError{message, err} +} + +func (e ParseValueError) Error() string { + return fmt.Sprintf("%s: %v", e.Msg, e.Err) +} diff --git a/vendor/github.com/joho/godotenv/.gitignore b/vendor/github.com/joho/godotenv/.gitignore new file mode 100644 index 00000000..e43b0f98 --- /dev/null +++ b/vendor/github.com/joho/godotenv/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/vendor/github.com/joho/godotenv/LICENCE b/vendor/github.com/joho/godotenv/LICENCE new file mode 100644 index 00000000..e7ddd51b --- /dev/null +++ b/vendor/github.com/joho/godotenv/LICENCE @@ -0,0 +1,23 @@ +Copyright (c) 2013 John Barton + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/vendor/github.com/joho/godotenv/README.md b/vendor/github.com/joho/godotenv/README.md new file mode 100644 index 00000000..bfbe66a0 --- /dev/null +++ b/vendor/github.com/joho/godotenv/README.md @@ -0,0 +1,202 @@ +# GoDotEnv ![CI](https://github.com/joho/godotenv/workflows/CI/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/joho/godotenv)](https://goreportcard.com/report/github.com/joho/godotenv) + +A Go (golang) port of the Ruby [dotenv](https://github.com/bkeepers/dotenv) project (which loads env vars from a .env file). + +From the original Library: + +> Storing configuration in the environment is one of the tenets of a twelve-factor app. Anything that is likely to change between deployment environments–such as resource handles for databases or credentials for external services–should be extracted from the code into environment variables. +> +> But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. Dotenv load variables from a .env file into ENV when the environment is bootstrapped. + +It can be used as a library (for loading in env for your own daemons etc.) or as a bin command. + +There is test coverage and CI for both linuxish and Windows environments, but I make no guarantees about the bin version working on Windows. + +## Installation + +As a library + +```shell +go get github.com/joho/godotenv +``` + +or if you want to use it as a bin command + +go >= 1.17 +```shell +go install github.com/joho/godotenv/cmd/godotenv@latest +``` + +go < 1.17 +```shell +go get github.com/joho/godotenv/cmd/godotenv +``` + +## Usage + +Add your application configuration to your `.env` file in the root of your project: + +```shell +S3_BUCKET=YOURS3BUCKET +SECRET_KEY=YOURSECRETKEYGOESHERE +``` + +Then in your Go app you can do something like + +```go +package main + +import ( + "log" + "os" + + "github.com/joho/godotenv" +) + +func main() { + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + + s3Bucket := os.Getenv("S3_BUCKET") + secretKey := os.Getenv("SECRET_KEY") + + // now do something with s3 or whatever +} +``` + +If you're even lazier than that, you can just take advantage of the autoload package which will read in `.env` on import + +```go +import _ "github.com/joho/godotenv/autoload" +``` + +While `.env` in the project root is the default, you don't have to be constrained, both examples below are 100% legit + +```go +godotenv.Load("somerandomfile") +godotenv.Load("filenumberone.env", "filenumbertwo.env") +``` + +If you want to be really fancy with your env file you can do comments and exports (below is a valid env file) + +```shell +# I am a comment and that is OK +SOME_VAR=someval +FOO=BAR # comments at line end are OK too +export BAR=BAZ +``` + +Or finally you can do YAML(ish) style + +```yaml +FOO: bar +BAR: baz +``` + +as a final aside, if you don't want godotenv munging your env you can just get a map back instead + +```go +var myEnv map[string]string +myEnv, err := godotenv.Read() + +s3Bucket := myEnv["S3_BUCKET"] +``` + +... or from an `io.Reader` instead of a local file + +```go +reader := getRemoteFile() +myEnv, err := godotenv.Parse(reader) +``` + +... or from a `string` if you so desire + +```go +content := getRemoteFileContent() +myEnv, err := godotenv.Unmarshal(content) +``` + +### Precedence & Conventions + +Existing envs take precedence of envs that are loaded later. + +The [convention](https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use) +for managing multiple environments (i.e. development, test, production) +is to create an env named `{YOURAPP}_ENV` and load envs in this order: + +```go +env := os.Getenv("FOO_ENV") +if "" == env { + env = "development" +} + +godotenv.Load(".env." + env + ".local") +if "test" != env { + godotenv.Load(".env.local") +} +godotenv.Load(".env." + env) +godotenv.Load() // The Original .env +``` + +If you need to, you can also use `godotenv.Overload()` to defy this convention +and overwrite existing envs instead of only supplanting them. Use with caution. + +### Command Mode + +Assuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH` + +``` +godotenv -f /some/path/to/.env some_command with some args +``` + +If you don't specify `-f` it will fall back on the default of loading `.env` in `PWD` + +By default, it won't override existing environment variables; you can do that with the `-o` flag. + +### Writing Env Files + +Godotenv can also write a map representing the environment to a correctly-formatted and escaped file + +```go +env, err := godotenv.Unmarshal("KEY=value") +err := godotenv.Write(env, "./.env") +``` + +... or to a string + +```go +env, err := godotenv.Unmarshal("KEY=value") +content, err := godotenv.Marshal(env) +``` + +## Contributing + +Contributions are welcome, but with some caveats. + +This library has been declared feature complete (see [#182](https://github.com/joho/godotenv/issues/182) for background) and will not be accepting issues or pull requests adding new functionality or breaking the library API. + +Contributions would be gladly accepted that: + +* bring this library's parsing into closer compatibility with the mainline dotenv implementations, in particular [Ruby's dotenv](https://github.com/bkeepers/dotenv) and [Node.js' dotenv](https://github.com/motdotla/dotenv) +* keep the library up to date with the go ecosystem (ie CI bumps, documentation changes, changes in the core libraries) +* bug fixes for use cases that pertain to the library's purpose of easing development of codebases deployed into twelve factor environments + +*code changes without tests and references to peer dotenv implementations will not be accepted* + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Added some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request + +## Releases + +Releases should follow [Semver](http://semver.org/) though the first couple of releases are `v1` and `v1.1`. + +Use [annotated tags for all releases](https://github.com/joho/godotenv/issues/30). Example `git tag -a v1.2.1` + +## Who? + +The original library [dotenv](https://github.com/bkeepers/dotenv) was written by [Brandon Keepers](http://opensoul.org/), and this port was done by [John Barton](https://johnbarton.co/) based off the tests/fixtures in the original library. diff --git a/vendor/github.com/joho/godotenv/godotenv.go b/vendor/github.com/joho/godotenv/godotenv.go new file mode 100644 index 00000000..61b0ebba --- /dev/null +++ b/vendor/github.com/joho/godotenv/godotenv.go @@ -0,0 +1,228 @@ +// Package godotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv) +// +// Examples/readme can be found on the GitHub page at https://github.com/joho/godotenv +// +// The TL;DR is that you make a .env file that looks something like +// +// SOME_ENV_VAR=somevalue +// +// and then in your go code you can call +// +// godotenv.Load() +// +// and all the env vars declared in .env will be available through os.Getenv("SOME_ENV_VAR") +package godotenv + +import ( + "bytes" + "fmt" + "io" + "os" + "os/exec" + "sort" + "strconv" + "strings" +) + +const doubleQuoteSpecialChars = "\\\n\r\"!$`" + +// Parse reads an env file from io.Reader, returning a map of keys and values. +func Parse(r io.Reader) (map[string]string, error) { + var buf bytes.Buffer + _, err := io.Copy(&buf, r) + if err != nil { + return nil, err + } + + return UnmarshalBytes(buf.Bytes()) +} + +// Load will read your env file(s) and load them into ENV for this process. +// +// Call this function as close as possible to the start of your program (ideally in main). +// +// If you call Load without any args it will default to loading .env in the current path. +// +// You can otherwise tell it which files to load (there can be more than one) like: +// +// godotenv.Load("fileone", "filetwo") +// +// It's important to note that it WILL NOT OVERRIDE an env variable that already exists - consider the .env file to set dev vars or sensible defaults. +func Load(filenames ...string) (err error) { + filenames = filenamesOrDefault(filenames) + + for _, filename := range filenames { + err = loadFile(filename, false) + if err != nil { + return // return early on a spazout + } + } + return +} + +// Overload will read your env file(s) and load them into ENV for this process. +// +// Call this function as close as possible to the start of your program (ideally in main). +// +// If you call Overload without any args it will default to loading .env in the current path. +// +// You can otherwise tell it which files to load (there can be more than one) like: +// +// godotenv.Overload("fileone", "filetwo") +// +// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefully set all vars. +func Overload(filenames ...string) (err error) { + filenames = filenamesOrDefault(filenames) + + for _, filename := range filenames { + err = loadFile(filename, true) + if err != nil { + return // return early on a spazout + } + } + return +} + +// Read all env (with same file loading semantics as Load) but return values as +// a map rather than automatically writing values into env +func Read(filenames ...string) (envMap map[string]string, err error) { + filenames = filenamesOrDefault(filenames) + envMap = make(map[string]string) + + for _, filename := range filenames { + individualEnvMap, individualErr := readFile(filename) + + if individualErr != nil { + err = individualErr + return // return early on a spazout + } + + for key, value := range individualEnvMap { + envMap[key] = value + } + } + + return +} + +// Unmarshal reads an env file from a string, returning a map of keys and values. +func Unmarshal(str string) (envMap map[string]string, err error) { + return UnmarshalBytes([]byte(str)) +} + +// UnmarshalBytes parses env file from byte slice of chars, returning a map of keys and values. +func UnmarshalBytes(src []byte) (map[string]string, error) { + out := make(map[string]string) + err := parseBytes(src, out) + + return out, err +} + +// Exec loads env vars from the specified filenames (empty map falls back to default) +// then executes the cmd specified. +// +// Simply hooks up os.Stdin/err/out to the command and calls Run(). +// +// If you want more fine grained control over your command it's recommended +// that you use `Load()`, `Overload()` or `Read()` and the `os/exec` package yourself. +func Exec(filenames []string, cmd string, cmdArgs []string, overload bool) error { + op := Load + if overload { + op = Overload + } + if err := op(filenames...); err != nil { + return err + } + + command := exec.Command(cmd, cmdArgs...) + command.Stdin = os.Stdin + command.Stdout = os.Stdout + command.Stderr = os.Stderr + return command.Run() +} + +// Write serializes the given environment and writes it to a file. +func Write(envMap map[string]string, filename string) error { + content, err := Marshal(envMap) + if err != nil { + return err + } + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + _, err = file.WriteString(content + "\n") + if err != nil { + return err + } + return file.Sync() +} + +// Marshal outputs the given environment as a dotenv-formatted environment file. +// Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped. +func Marshal(envMap map[string]string) (string, error) { + lines := make([]string, 0, len(envMap)) + for k, v := range envMap { + if d, err := strconv.Atoi(v); err == nil { + lines = append(lines, fmt.Sprintf(`%s=%d`, k, d)) + } else { + lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v))) + } + } + sort.Strings(lines) + return strings.Join(lines, "\n"), nil +} + +func filenamesOrDefault(filenames []string) []string { + if len(filenames) == 0 { + return []string{".env"} + } + return filenames +} + +func loadFile(filename string, overload bool) error { + envMap, err := readFile(filename) + if err != nil { + return err + } + + currentEnv := map[string]bool{} + rawEnv := os.Environ() + for _, rawEnvLine := range rawEnv { + key := strings.Split(rawEnvLine, "=")[0] + currentEnv[key] = true + } + + for key, value := range envMap { + if !currentEnv[key] || overload { + _ = os.Setenv(key, value) + } + } + + return nil +} + +func readFile(filename string) (envMap map[string]string, err error) { + file, err := os.Open(filename) + if err != nil { + return + } + defer file.Close() + + return Parse(file) +} + +func doubleQuoteEscape(line string) string { + for _, c := range doubleQuoteSpecialChars { + toReplace := "\\" + string(c) + if c == '\n' { + toReplace = `\n` + } + if c == '\r' { + toReplace = `\r` + } + line = strings.Replace(line, string(c), toReplace, -1) + } + return line +} diff --git a/vendor/github.com/joho/godotenv/parser.go b/vendor/github.com/joho/godotenv/parser.go new file mode 100644 index 00000000..cc709af8 --- /dev/null +++ b/vendor/github.com/joho/godotenv/parser.go @@ -0,0 +1,271 @@ +package godotenv + +import ( + "bytes" + "errors" + "fmt" + "regexp" + "strings" + "unicode" +) + +const ( + charComment = '#' + prefixSingleQuote = '\'' + prefixDoubleQuote = '"' + + exportPrefix = "export" +) + +func parseBytes(src []byte, out map[string]string) error { + src = bytes.Replace(src, []byte("\r\n"), []byte("\n"), -1) + cutset := src + for { + cutset = getStatementStart(cutset) + if cutset == nil { + // reached end of file + break + } + + key, left, err := locateKeyName(cutset) + if err != nil { + return err + } + + value, left, err := extractVarValue(left, out) + if err != nil { + return err + } + + out[key] = value + cutset = left + } + + return nil +} + +// getStatementPosition returns position of statement begin. +// +// It skips any comment line or non-whitespace character. +func getStatementStart(src []byte) []byte { + pos := indexOfNonSpaceChar(src) + if pos == -1 { + return nil + } + + src = src[pos:] + if src[0] != charComment { + return src + } + + // skip comment section + pos = bytes.IndexFunc(src, isCharFunc('\n')) + if pos == -1 { + return nil + } + + return getStatementStart(src[pos:]) +} + +// locateKeyName locates and parses key name and returns rest of slice +func locateKeyName(src []byte) (key string, cutset []byte, err error) { + // trim "export" and space at beginning + src = bytes.TrimLeftFunc(src, isSpace) + if bytes.HasPrefix(src, []byte(exportPrefix)) { + trimmed := bytes.TrimPrefix(src, []byte(exportPrefix)) + if bytes.IndexFunc(trimmed, isSpace) == 0 { + src = bytes.TrimLeftFunc(trimmed, isSpace) + } + } + + // locate key name end and validate it in single loop + offset := 0 +loop: + for i, char := range src { + rchar := rune(char) + if isSpace(rchar) { + continue + } + + switch char { + case '=', ':': + // library also supports yaml-style value declaration + key = string(src[0:i]) + offset = i + 1 + break loop + case '_': + default: + // variable name should match [A-Za-z0-9_.] + if unicode.IsLetter(rchar) || unicode.IsNumber(rchar) || rchar == '.' { + continue + } + + return "", nil, fmt.Errorf( + `unexpected character %q in variable name near %q`, + string(char), string(src)) + } + } + + if len(src) == 0 { + return "", nil, errors.New("zero length string") + } + + // trim whitespace + key = strings.TrimRightFunc(key, unicode.IsSpace) + cutset = bytes.TrimLeftFunc(src[offset:], isSpace) + return key, cutset, nil +} + +// extractVarValue extracts variable value and returns rest of slice +func extractVarValue(src []byte, vars map[string]string) (value string, rest []byte, err error) { + quote, hasPrefix := hasQuotePrefix(src) + if !hasPrefix { + // unquoted value - read until end of line + endOfLine := bytes.IndexFunc(src, isLineEnd) + + // Hit EOF without a trailing newline + if endOfLine == -1 { + endOfLine = len(src) + + if endOfLine == 0 { + return "", nil, nil + } + } + + // Convert line to rune away to do accurate countback of runes + line := []rune(string(src[0:endOfLine])) + + // Assume end of line is end of var + endOfVar := len(line) + if endOfVar == 0 { + return "", src[endOfLine:], nil + } + + // Work backwards to check if the line ends in whitespace then + // a comment (ie asdasd # some comment) + for i := endOfVar - 1; i >= 0; i-- { + if line[i] == charComment && i > 0 { + if isSpace(line[i-1]) { + endOfVar = i + break + } + } + } + + trimmed := strings.TrimFunc(string(line[0:endOfVar]), isSpace) + + return expandVariables(trimmed, vars), src[endOfLine:], nil + } + + // lookup quoted string terminator + for i := 1; i < len(src); i++ { + if char := src[i]; char != quote { + continue + } + + // skip escaped quote symbol (\" or \', depends on quote) + if prevChar := src[i-1]; prevChar == '\\' { + continue + } + + // trim quotes + trimFunc := isCharFunc(rune(quote)) + value = string(bytes.TrimLeftFunc(bytes.TrimRightFunc(src[0:i], trimFunc), trimFunc)) + if quote == prefixDoubleQuote { + // unescape newlines for double quote (this is compat feature) + // and expand environment variables + value = expandVariables(expandEscapes(value), vars) + } + + return value, src[i+1:], nil + } + + // return formatted error if quoted string is not terminated + valEndIndex := bytes.IndexFunc(src, isCharFunc('\n')) + if valEndIndex == -1 { + valEndIndex = len(src) + } + + return "", nil, fmt.Errorf("unterminated quoted value %s", src[:valEndIndex]) +} + +func expandEscapes(str string) string { + out := escapeRegex.ReplaceAllStringFunc(str, func(match string) string { + c := strings.TrimPrefix(match, `\`) + switch c { + case "n": + return "\n" + case "r": + return "\r" + default: + return match + } + }) + return unescapeCharsRegex.ReplaceAllString(out, "$1") +} + +func indexOfNonSpaceChar(src []byte) int { + return bytes.IndexFunc(src, func(r rune) bool { + return !unicode.IsSpace(r) + }) +} + +// hasQuotePrefix reports whether charset starts with single or double quote and returns quote character +func hasQuotePrefix(src []byte) (prefix byte, isQuored bool) { + if len(src) == 0 { + return 0, false + } + + switch prefix := src[0]; prefix { + case prefixDoubleQuote, prefixSingleQuote: + return prefix, true + default: + return 0, false + } +} + +func isCharFunc(char rune) func(rune) bool { + return func(v rune) bool { + return v == char + } +} + +// isSpace reports whether the rune is a space character but not line break character +// +// this differs from unicode.IsSpace, which also applies line break as space +func isSpace(r rune) bool { + switch r { + case '\t', '\v', '\f', '\r', ' ', 0x85, 0xA0: + return true + } + return false +} + +func isLineEnd(r rune) bool { + if r == '\n' || r == '\r' { + return true + } + return false +} + +var ( + escapeRegex = regexp.MustCompile(`\\.`) + expandVarRegex = regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`) + unescapeCharsRegex = regexp.MustCompile(`\\([^$])`) +) + +func expandVariables(v string, m map[string]string) string { + return expandVarRegex.ReplaceAllStringFunc(v, func(s string) string { + submatch := expandVarRegex.FindStringSubmatch(s) + + if submatch == nil { + return s + } + if submatch[1] == "\\" || submatch[2] == "(" { + return submatch[0][1:] + } else if submatch[4] != "" { + return m[submatch[4]] + } + return s + }) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index e3ece200..bab741b2 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -127,6 +127,9 @@ github.com/aws/smithy-go/time github.com/aws/smithy-go/tracing github.com/aws/smithy-go/transport/http github.com/aws/smithy-go/transport/http/internal/io +# github.com/caarlos0/env/v11 v11.3.1 +## explicit; go 1.18 +github.com/caarlos0/env/v11 # github.com/cenkalti/backoff/v4 v4.3.0 ## explicit; go 1.18 github.com/cenkalti/backoff/v4 @@ -284,6 +287,9 @@ github.com/jackc/pgx/v5/pgxpool ## explicit; go 1.19 github.com/jackc/puddle/v2 github.com/jackc/puddle/v2/internal/genstack +# github.com/joho/godotenv v1.5.1 +## explicit; go 1.12 +github.com/joho/godotenv # github.com/josharian/intern v1.0.0 ## explicit; go 1.5 github.com/josharian/intern