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 @@
+
+
+
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 – 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
+
+[](https://github.com/goreleaser/goreleaser/releases/latest)
+[](/LICENSE.md)
+[](https://github.com/caarlos0/env/actions?workflow=build)
+[](https://codecov.io/gh/caarlos0/env)
+[](http://godoc.org/github.com/caarlos0/env/v11)
+[](https://github.com/goreleaser)
+[](https://conventionalcommits.org)
+
+## Related projects
+
+- [envdoc](https://github.com/g4s8/envdoc) - generate documentation for environment variables from `env` tags
+
+## Stargazers over time
+
+[](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  [](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