From 2d3d2bbab572c829bbb57ca77e387396bc0abdf5 Mon Sep 17 00:00:00 2001 From: Ray Johnson Date: Fri, 26 Sep 2025 21:06:14 +0000 Subject: [PATCH 01/12] small fixes to code comments and error messages --- util.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/util.go b/util.go index 9ce6ad5..fe5b2b1 100644 --- a/util.go +++ b/util.go @@ -154,7 +154,7 @@ func GetOrFloat32(key string, defaultValue float32) float32 { return defaultValue } -// MustGetUFloat32 - get an environment variable or panic if does not exist +// MustGetFloat32 - get an environment variable or panic if does not exist func MustGetFloat32(key string) float32 { strValue, ok := os.LookupEnv(key) if ok { @@ -186,7 +186,7 @@ func GetOrFloat64(key string, defaultValue float64) float64 { return defaultValue } -// MustGetUFloat64 - get an environment variable or panic if does not exist +// MustGetFloat64 - get an environment variable or panic if does not exist func MustGetFloat64(key string) float64 { strValue, ok := os.LookupEnv(key) if ok { @@ -281,7 +281,7 @@ func GetOrDuration(key string, defaultValue string) time.Duration { } defaultDuration, err := time.ParseDuration(defaultValue) if err != nil { - panic(fmt.Sprintf("default duration \"%s\" could not be converted to time.Duration", key)) + panic(fmt.Sprintf("default duration \"%s\" could not be converted to time.Duration", defaultValue)) } return defaultDuration } @@ -317,7 +317,7 @@ func GetOrUrl(key string, defaultValue string) *url.URL { } defaultUrl, err := url.ParseRequestURI(defaultValue) if err != nil { - panic(fmt.Sprintf("default duration \"%s\" could not be converted to url.URL", key)) + panic(fmt.Sprintf("default url \"%s\" could not be converted to url.URL", defaultValue)) } return defaultUrl } From fd045bc97db2bfb649e7bd40641df3fde80d3f97 Mon Sep 17 00:00:00 2001 From: Ray Johnson Date: Fri, 26 Sep 2025 21:14:29 +0000 Subject: [PATCH 02/12] small performance improvements suggested by claude --- env.go | 2 +- file.go | 9 +----- file_test.go | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 9 deletions(-) diff --git a/env.go b/env.go index d725cec..bf10f35 100644 --- a/env.go +++ b/env.go @@ -351,7 +351,7 @@ func parseInt64s(data []string) ([]int64, error) { } func parseUint64s(data []string) ([]uint64, error) { - var uintSlice []uint64 + uintSlice := make([]uint64, 0, len(data)) for _, v := range data { uintValue, err := strconv.ParseUint(v, 10, 64) diff --git a/file.go b/file.go index f8ddd82..a384036 100644 --- a/file.go +++ b/file.go @@ -197,15 +197,8 @@ func loadFile(filename string, overload bool) error { 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 { + if _, exists := os.LookupEnv(key); !exists || overload { os.Setenv(key, value) } } diff --git a/file_test.go b/file_test.go index fb201df..1699e07 100644 --- a/file_test.go +++ b/file_test.go @@ -476,3 +476,91 @@ func TestRoundtrip(t *testing.T) { } } + +// Benchmark tests for performance improvements +func BenchmarkLoadFile(b *testing.B) { + // Create a temporary .env file for benchmarking + tempFile := "benchmark_test.env" + envContent := `TEST_VAR1=value1 +TEST_VAR2=value2 +TEST_VAR3=value3 +TEST_VAR4=value4 +TEST_VAR5=value5 +TEST_VAR6=value6 +TEST_VAR7=value7 +TEST_VAR8=value8 +TEST_VAR9=value9 +TEST_VAR10=value10` + + err := os.WriteFile(tempFile, []byte(envContent), 0644) + if err != nil { + b.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tempFile) + + // Set some existing environment variables to test the lookup optimization + os.Setenv("EXISTING_VAR1", "existing1") + os.Setenv("EXISTING_VAR2", "existing2") + os.Setenv("EXISTING_VAR3", "existing3") + defer func() { + os.Unsetenv("EXISTING_VAR1") + os.Unsetenv("EXISTING_VAR2") + os.Unsetenv("EXISTING_VAR3") + }() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + err := loadFile(tempFile, false) + if err != nil { + b.Fatalf("loadFile failed: %v", err) + } + } +} + +func BenchmarkLoadFileWithOverload(b *testing.B) { + // Create a temporary .env file for benchmarking + tempFile := "benchmark_overload_test.env" + envContent := `TEST_VAR1=new_value1 +TEST_VAR2=new_value2 +TEST_VAR3=new_value3 +EXISTING_VAR1=overridden1 +EXISTING_VAR2=overridden2` + + err := os.WriteFile(tempFile, []byte(envContent), 0644) + if err != nil { + b.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tempFile) + + // Set some existing environment variables + os.Setenv("EXISTING_VAR1", "original1") + os.Setenv("EXISTING_VAR2", "original2") + defer func() { + os.Unsetenv("EXISTING_VAR1") + os.Unsetenv("EXISTING_VAR2") + }() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + err := loadFile(tempFile, true) + if err != nil { + b.Fatalf("loadFile with overload failed: %v", err) + } + } +} + +func BenchmarkParseUint64s(b *testing.B) { + // Create test data with many uint64 values + testData := make([]string, 100) + for i := 0; i < 100; i++ { + testData[i] = fmt.Sprintf("%d", uint64(i*12345)) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := parseUint64s(testData) + if err != nil { + b.Fatalf("parseUint64s failed: %v", err) + } + } +} From f95c1b49b92768d801f9f0ddc7cd6ac17221aab9 Mon Sep 17 00:00:00 2001 From: Ray Johnson Date: Fri, 26 Sep 2025 22:16:15 +0000 Subject: [PATCH 03/12] various code design issues resolved as suggested by claude --- env.go | 66 ++++++++++++++++++++++++++++++++++++++-------------- env_test.go | 64 ++++++++++++++++++++++++++++++++++++++++++++++++-- util.go | 50 +++++++++++++++++++++++++-------------- util_test.go | 30 ++++++++++++++++++++++++ 4 files changed, 173 insertions(+), 37 deletions(-) diff --git a/env.go b/env.go index bf10f35..af04ec2 100644 --- a/env.go +++ b/env.go @@ -12,6 +12,15 @@ import ( "time" ) +// Constants for parsing operations (shared with util.go) +const ( + DecimalBase = 10 + Int32Bits = 32 + Int64Bits = 64 + Float32Bits = 32 + Float64Bits = 64 +) + var ( // ErrNotAStructPtr is returned if you pass something that is not a pointer to a // Struct to Parse @@ -20,6 +29,29 @@ var ( ErrUnsupportedType = errors.New("type is not supported") // ErrUnsupportedSliceType if the slice element type is not supported by env ErrUnsupportedSliceType = errors.New("unsupported slice type") +) + +// ParseErrors represents multiple errors that occurred during parsing +type ParseErrors []error + +// Error implements the error interface for ParseErrors +func (pe ParseErrors) Error() string { + if len(pe) == 0 { + return "" + } + if len(pe) == 1 { + return pe[0].Error() + } + + var sb strings.Builder + sb.WriteString(fmt.Sprintf("multiple parsing errors (%d):", len(pe))) + for i, err := range pe { + sb.WriteString(fmt.Sprintf("\n %d. %s", i+1, err.Error())) + } + return sb.String() +} + +var ( // OnEnvVarSet is an optional convenience callback, such as for logging purposes. // If not nil, it's called after successfully setting the given field from the given value. OnEnvVarSet func(reflect.StructField, string) @@ -75,7 +107,7 @@ func ParseWithPrefixFuncs(v interface{}, prefix string, funcMap CustomParsers) e func doParse(ref reflect.Value, prefix string, funcMap CustomParsers) error { refType := ref.Type() - var errorList []string + var parseErrors ParseErrors for i := 0; i < refType.NumField(); i++ { refField := ref.Field(i) @@ -89,29 +121,29 @@ func doParse(ref reflect.Value, prefix string, funcMap CustomParsers) error { refTypeField := refType.Field(i) value, err := get(refTypeField, prefix) if err != nil { - errorList = append(errorList, err.Error()) + parseErrors = append(parseErrors, err) continue } if value == "" { if reflect.Struct == refField.Kind() { if err := doParse(refField, prefix, funcMap); err != nil { - errorList = append(errorList, err.Error()) + parseErrors = append(parseErrors, err) } } continue } if err := set(refField, refTypeField, value, funcMap); err != nil { - errorList = append(errorList, err.Error()) + parseErrors = append(parseErrors, err) continue } if OnEnvVarSet != nil { OnEnvVarSet(refTypeField, value) } } - if len(errorList) == 0 { + if len(parseErrors) == 0 { return nil } - return errors.New(strings.Join(errorList, ". ")) + return parseErrors } func get(field reflect.StructField, prefix string) (string, error) { @@ -185,25 +217,25 @@ func set(field reflect.Value, refType reflect.StructField, value string, funcMap } field.SetBool(bvalue) case reflect.Int: - intValue, err := strconv.ParseInt(value, 10, 32) + intValue, err := strconv.ParseInt(value, DecimalBase, Int32Bits) if err != nil { return err } field.SetInt(intValue) case reflect.Uint: - uintValue, err := strconv.ParseUint(value, 10, 32) + uintValue, err := strconv.ParseUint(value, DecimalBase, Int32Bits) if err != nil { return err } field.SetUint(uintValue) case reflect.Float32: - v, err := strconv.ParseFloat(value, 32) + v, err := strconv.ParseFloat(value, Float32Bits) if err != nil { return err } field.SetFloat(v) case reflect.Float64: - v, err := strconv.ParseFloat(value, 64) + v, err := strconv.ParseFloat(value, Float64Bits) if err != nil { return err } @@ -216,14 +248,14 @@ func set(field reflect.Value, refType reflect.StructField, value string, funcMap } field.Set(reflect.ValueOf(dValue)) } else { - intValue, err := strconv.ParseInt(value, 10, 64) + intValue, err := strconv.ParseInt(value, DecimalBase, Int64Bits) if err != nil { return err } field.SetInt(intValue) } case reflect.Uint64: - uintValue, err := strconv.ParseUint(value, 10, 64) + uintValue, err := strconv.ParseUint(value, DecimalBase, Int64Bits) if err != nil { return err } @@ -328,7 +360,7 @@ func parseInts(data []string) ([]int, error) { intSlice := make([]int, 0, len(data)) for _, v := range data { - intValue, err := strconv.ParseInt(v, 10, 32) + intValue, err := strconv.ParseInt(v, DecimalBase, Int32Bits) if err != nil { return nil, err } @@ -341,7 +373,7 @@ func parseInt64s(data []string) ([]int64, error) { intSlice := make([]int64, 0, len(data)) for _, v := range data { - intValue, err := strconv.ParseInt(v, 10, 64) + intValue, err := strconv.ParseInt(v, DecimalBase, Int64Bits) if err != nil { return nil, err } @@ -354,7 +386,7 @@ func parseUint64s(data []string) ([]uint64, error) { uintSlice := make([]uint64, 0, len(data)) for _, v := range data { - uintValue, err := strconv.ParseUint(v, 10, 64) + uintValue, err := strconv.ParseUint(v, DecimalBase, Int64Bits) if err != nil { return nil, err } @@ -367,7 +399,7 @@ func parseFloat32s(data []string) ([]float32, error) { float32Slice := make([]float32, 0, len(data)) for _, v := range data { - data, err := strconv.ParseFloat(v, 32) + data, err := strconv.ParseFloat(v, Float32Bits) if err != nil { return nil, err } @@ -380,7 +412,7 @@ func parseFloat64s(data []string) ([]float64, error) { float64Slice := make([]float64, 0, len(data)) for _, v := range data { - data, err := strconv.ParseFloat(v, 64) + data, err := strconv.ParseFloat(v, Float64Bits) if err != nil { return nil, err } diff --git a/env_test.go b/env_test.go index 13e479b..aa60cbf 100644 --- a/env_test.go +++ b/env_test.go @@ -677,7 +677,12 @@ func TestCustomParserBasicUnsupported(t *testing.T) { assert.Zero(t, cfg.Const) assert.Error(t, err) - assert.Equal(t, ErrUnsupportedType, err) + // With the new ParseErrors, single errors are wrapped + if parseErrors, ok := err.(ParseErrors); ok && len(parseErrors) == 1 { + assert.Equal(t, ErrUnsupportedType, parseErrors[0]) + } else { + assert.Equal(t, ErrUnsupportedType, err) + } } func TestUnsupportedStructType(t *testing.T) { @@ -691,7 +696,12 @@ func TestUnsupportedStructType(t *testing.T) { err := Parse(cfg) assert.Error(t, err) - assert.Equal(t, ErrUnsupportedType, err) + // With the new ParseErrors, single errors are wrapped + if parseErrors, ok := err.(ParseErrors); ok && len(parseErrors) == 1 { + assert.Equal(t, ErrUnsupportedType, parseErrors[0]) + } else { + assert.Equal(t, ErrUnsupportedType, err) + } } func TestTextUnmarshalerError(t *testing.T) { @@ -715,3 +725,53 @@ func ExampleParse() { fmt.Println(cfg) // Output: {/tmp/fakehome 3000 false} } + +// Test ParseErrors functionality +func TestParseErrors(t *testing.T) { + // Test empty ParseErrors + var emptyErrors ParseErrors + assert.Equal(t, "", emptyErrors.Error()) + + // Test single error + singleErrors := ParseErrors{errors.New("single error")} + assert.Equal(t, "single error", singleErrors.Error()) + + // Test multiple errors + multipleErrors := ParseErrors{ + errors.New("first error"), + errors.New("second error"), + errors.New("third error"), + } + expected := `multiple parsing errors (3): + 1. first error + 2. second error + 3. third error` + assert.Equal(t, expected, multipleErrors.Error()) +} + +// Test ParseErrors in actual parsing scenario +func TestParseMultipleErrors(t *testing.T) { + type config struct { + RequiredVar1 string `env:"REQUIRED_VAR1" required:"true"` + RequiredVar2 int `env:"REQUIRED_VAR2" required:"true"` + BadInt int `env:"BAD_INT_VAR"` + } + + // Set up environment to cause multiple errors + os.Unsetenv("REQUIRED_VAR1") + os.Unsetenv("REQUIRED_VAR2") + os.Setenv("BAD_INT_VAR", "not_a_number") + + cfg := &config{} + err := Parse(cfg) + + // Should get a ParseErrors with multiple issues + assert.Error(t, err) + parseErrors, ok := err.(ParseErrors) + assert.True(t, ok, "Error should be of type ParseErrors") + assert.Greater(t, len(parseErrors), 1, "Should have multiple errors") + + // Check that error message contains information about multiple errors + errMsg := err.Error() + assert.Contains(t, errMsg, "multiple parsing errors") +} diff --git a/util.go b/util.go index fe5b2b1..3743942 100644 --- a/util.go +++ b/util.go @@ -4,10 +4,24 @@ import ( "fmt" "net/url" "os" + "regexp" "strconv" "time" ) +// Regular expression for validating environment variable names +// Typically follows pattern: [A-Z_][A-Z0-9_]* +var envVarNameRegex = regexp.MustCompile(`^[A-Z_][A-Z0-9_]*$`) + +// isValidEnvVarKey validates that an environment variable key follows standard naming conventions +// This is a helper function for internal validation - not exported to avoid breaking changes +func isValidEnvVarKey(key string) bool { + if key == "" { + return false + } + return envVarNameRegex.MatchString(key) +} + // Set - sets an environment variable func Set(key, value string) error { return os.Setenv(key, value) @@ -74,7 +88,7 @@ func MustGetBool(key string) bool { // GetInt - get an environment variable as int func GetInt(key string) (int, error) { - value, err := strconv.ParseInt(os.Getenv(key), 10, 32) + value, err := strconv.ParseInt(os.Getenv(key), DecimalBase, Int32Bits) return int(value), err } @@ -82,7 +96,7 @@ func GetInt(key string) (int, error) { func GetOrInt(key string, defaultValue int) int { strValue, ok := os.LookupEnv(key) if ok { - value, err := strconv.ParseInt(strValue, 10, 32) + value, err := strconv.ParseInt(strValue, DecimalBase, Int32Bits) if err == nil { return int(value) } @@ -94,7 +108,7 @@ func GetOrInt(key string, defaultValue int) int { func MustGetInt(key string) int { strValue, ok := os.LookupEnv(key) if ok { - value, err := strconv.ParseInt(strValue, 10, 32) + value, err := strconv.ParseInt(strValue, DecimalBase, Int32Bits) if err == nil { return int(value) } else { @@ -106,7 +120,7 @@ func MustGetInt(key string) int { // GetUint - get an environment variable as uint func GetUint(key string) (uint, error) { - value, err := strconv.ParseUint(os.Getenv(key), 10, 32) + value, err := strconv.ParseUint(os.Getenv(key), DecimalBase, Int32Bits) return uint(value), err } @@ -114,7 +128,7 @@ func GetUint(key string) (uint, error) { func GetOrUint(key string, defaultValue uint) uint { strValue, ok := os.LookupEnv(key) if ok { - value, err := strconv.ParseUint(strValue, 10, 32) + value, err := strconv.ParseUint(strValue, DecimalBase, Int32Bits) if err == nil { return uint(value) } @@ -126,7 +140,7 @@ func GetOrUint(key string, defaultValue uint) uint { func MustGetUint(key string) uint { strValue, ok := os.LookupEnv(key) if ok { - value, err := strconv.ParseUint(strValue, 10, 32) + value, err := strconv.ParseUint(strValue, DecimalBase, Int32Bits) if err == nil { return uint(value) } else { @@ -138,7 +152,7 @@ func MustGetUint(key string) uint { // GetFloat32 - get an environment variable as float32 func GetFloat32(key string) (float32, error) { - value, err := strconv.ParseFloat(os.Getenv(key), 32) + value, err := strconv.ParseFloat(os.Getenv(key), Float32Bits) return float32(value), err } @@ -146,7 +160,7 @@ func GetFloat32(key string) (float32, error) { func GetOrFloat32(key string, defaultValue float32) float32 { strValue, ok := os.LookupEnv(key) if ok { - value, err := strconv.ParseFloat(strValue, 32) + value, err := strconv.ParseFloat(strValue, Float32Bits) if err == nil { return float32(value) } @@ -158,7 +172,7 @@ func GetOrFloat32(key string, defaultValue float32) float32 { func MustGetFloat32(key string) float32 { strValue, ok := os.LookupEnv(key) if ok { - value, err := strconv.ParseFloat(strValue, 32) + value, err := strconv.ParseFloat(strValue, Float32Bits) if err == nil { return float32(value) } else { @@ -170,7 +184,7 @@ func MustGetFloat32(key string) float32 { // GetFloat64 - get an environment variable as float64 func GetFloat64(key string) (float64, error) { - value, err := strconv.ParseFloat(os.Getenv(key), 64) + value, err := strconv.ParseFloat(os.Getenv(key), Float64Bits) return float64(value), err } @@ -178,7 +192,7 @@ func GetFloat64(key string) (float64, error) { func GetOrFloat64(key string, defaultValue float64) float64 { strValue, ok := os.LookupEnv(key) if ok { - value, err := strconv.ParseFloat(strValue, 64) + value, err := strconv.ParseFloat(strValue, Float64Bits) if err == nil { return float64(value) } @@ -190,7 +204,7 @@ func GetOrFloat64(key string, defaultValue float64) float64 { func MustGetFloat64(key string) float64 { strValue, ok := os.LookupEnv(key) if ok { - value, err := strconv.ParseFloat(strValue, 64) + value, err := strconv.ParseFloat(strValue, Float64Bits) if err == nil { return float64(value) } else { @@ -202,7 +216,7 @@ func MustGetFloat64(key string) float64 { // GetInt64 - get an environment variable as int64 func GetInt64(key string) (int64, error) { - value, err := strconv.ParseInt(os.Getenv(key), 10, 64) + value, err := strconv.ParseInt(os.Getenv(key), DecimalBase, Int64Bits) return int64(value), err } @@ -210,7 +224,7 @@ func GetInt64(key string) (int64, error) { func GetOrInt64(key string, defaultValue int64) int64 { strValue, ok := os.LookupEnv(key) if ok { - value, err := strconv.ParseInt(strValue, 10, 64) + value, err := strconv.ParseInt(strValue, DecimalBase, Int64Bits) if err == nil { return int64(value) } @@ -222,7 +236,7 @@ func GetOrInt64(key string, defaultValue int64) int64 { func MustGetInt64(key string) int64 { strValue, ok := os.LookupEnv(key) if ok { - value, err := strconv.ParseInt(strValue, 10, 64) + value, err := strconv.ParseInt(strValue, DecimalBase, Int64Bits) if err == nil { return int64(value) } else { @@ -234,7 +248,7 @@ func MustGetInt64(key string) int64 { // GetUint64 - get an environment variable as uint func GetUint64(key string) (uint64, error) { - value, err := strconv.ParseUint(os.Getenv(key), 10, 64) + value, err := strconv.ParseUint(os.Getenv(key), DecimalBase, Int64Bits) return uint64(value), err } @@ -242,7 +256,7 @@ func GetUint64(key string) (uint64, error) { func GetOrUint64(key string, defaultValue uint64) uint64 { strValue, ok := os.LookupEnv(key) if ok { - value, err := strconv.ParseUint(strValue, 10, 64) + value, err := strconv.ParseUint(strValue, DecimalBase, Int64Bits) if err == nil { return uint64(value) } @@ -254,7 +268,7 @@ func GetOrUint64(key string, defaultValue uint64) uint64 { func MustGetUint64(key string) uint64 { strValue, ok := os.LookupEnv(key) if ok { - value, err := strconv.ParseUint(strValue, 10, 64) + value, err := strconv.ParseUint(strValue, DecimalBase, Int64Bits) if err == nil { return uint64(value) } else { diff --git a/util_test.go b/util_test.go index a1cf1c3..d6b6d8e 100644 --- a/util_test.go +++ b/util_test.go @@ -269,3 +269,33 @@ func TestUrlFuncs(t *testing.T) { assert.Equal(t, "google.com", GetOrUrl("BAD_URL", "http://google.com").Hostname()) assert.Panics(t, func() { MustGetUrl("BAD_URL") }, "The code did not panic") } + +// Test environment variable key validation +func TestEnvVarKeyValidation(t *testing.T) { + // Valid keys + assert.True(t, isValidEnvVarKey("TEST_VAR")) + assert.True(t, isValidEnvVarKey("_PRIVATE_VAR")) + assert.True(t, isValidEnvVarKey("VAR123")) + assert.True(t, isValidEnvVarKey("MY_APP_CONFIG")) + assert.True(t, isValidEnvVarKey("A")) + assert.True(t, isValidEnvVarKey("_")) + + // Invalid keys + assert.False(t, isValidEnvVarKey("")) // empty + assert.False(t, isValidEnvVarKey("123VAR")) // starts with number + assert.False(t, isValidEnvVarKey("my-var")) // contains hyphen + assert.False(t, isValidEnvVarKey("my.var")) // contains dot + assert.False(t, isValidEnvVarKey("my var")) // contains space + assert.False(t, isValidEnvVarKey("myvar")) // lowercase (by convention) + assert.False(t, isValidEnvVarKey("My_Var")) // mixed case + assert.False(t, isValidEnvVarKey("VAR!")) // special character +} + +// Test constants are properly defined +func TestParsingConstants(t *testing.T) { + assert.Equal(t, 10, DecimalBase) + assert.Equal(t, 32, Int32Bits) + assert.Equal(t, 64, Int64Bits) + assert.Equal(t, 32, Float32Bits) + assert.Equal(t, 64, Float64Bits) +} From 995e7dbe6b5bc080efb69f892e2e267b5c31e13b Mon Sep 17 00:00:00 2001 From: Ray Johnson Date: Fri, 26 Sep 2025 22:52:55 +0000 Subject: [PATCH 04/12] moved to use go generics, much nicer implementation! --- go.mod | 2 +- util.go | 295 ++++++++++++++++++++------------------------------------ 2 files changed, 105 insertions(+), 192 deletions(-) diff --git a/go.mod b/go.mod index 524e01b..59b5813 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/lindenlab/env -go 1.17 +go 1.18 require github.com/stretchr/testify v1.11.1 diff --git a/util.go b/util.go index 3743942..3567425 100644 --- a/util.go +++ b/util.go @@ -22,6 +22,75 @@ func isValidEnvVarKey(key string) bool { return envVarNameRegex.MatchString(key) } +// Generic types and functions for reducing code duplication + +// Parser function type for converting strings to any type +type Parser[T any] func(string) (T, error) + +// GetParsed - generic function for getting parsed environment variables +func GetParsed[T any](key string, parser Parser[T]) (T, error) { + value := os.Getenv(key) + return parser(value) +} + +// GetOrParsed - generic function with default value +func GetOrParsed[T any](key string, defaultValue T, parser Parser[T]) T { + strValue, ok := os.LookupEnv(key) + if ok { + if value, err := parser(strValue); err == nil { + return value + } + } + return defaultValue +} + +// MustGetParsed - generic function that panics on missing/invalid values +func MustGetParsed[T any](key string, parser Parser[T], typeName string) T { + strValue, ok := os.LookupEnv(key) + if ok { + if value, err := parser(strValue); err == nil { + return value + } else { + panic(fmt.Sprintf("environment variable \"%s\" could not be converted to %s", key, typeName)) + } + } + panic(fmt.Sprintf("expected environment variable \"%s\" does not exist", key)) +} + +// Type-specific parser functions +var ( + ParseBool = func(s string) (bool, error) { + return strconv.ParseBool(s) + } + ParseInt = func(s string) (int, error) { + v, err := strconv.ParseInt(s, DecimalBase, Int32Bits) + return int(v), err + } + ParseUint = func(s string) (uint, error) { + v, err := strconv.ParseUint(s, DecimalBase, Int32Bits) + return uint(v), err + } + ParseFloat32 = func(s string) (float32, error) { + v, err := strconv.ParseFloat(s, Float32Bits) + return float32(v), err + } + ParseFloat64 = func(s string) (float64, error) { + return strconv.ParseFloat(s, Float64Bits) + } + ParseInt64 = func(s string) (int64, error) { + return strconv.ParseInt(s, DecimalBase, Int64Bits) + } + ParseUint64 = func(s string) (uint64, error) { + return strconv.ParseUint(s, DecimalBase, Int64Bits) + } + ParseDuration = func(s string) (time.Duration, error) { + return time.ParseDuration(s) + } + ParseURL = func(s string) (*url.URL, error) { + return url.ParseRequestURI(s) + } +) + // Set - sets an environment variable func Set(key, value string) error { return os.Setenv(key, value) @@ -55,236 +124,102 @@ func MustGet(key string) string { panic(fmt.Sprintf("expected environment variable \"%s\" does not exist", key)) } -// GetBool - get an environment variable as boolean +// Bool functions func GetBool(key string) (bool, error) { - return strconv.ParseBool(os.Getenv(key)) + return GetParsed(key, ParseBool) } -// GetOrBool - get an environment variable or return default value if does not exist func GetOrBool(key string, defaultValue bool) bool { - strValue, ok := os.LookupEnv(key) - if ok { - value, err := strconv.ParseBool(strValue) - if err == nil { - return value - } - } - return defaultValue + return GetOrParsed(key, defaultValue, ParseBool) } -// MustGetBool - get an environment variable or panic if does not exist func MustGetBool(key string) bool { - strValue, ok := os.LookupEnv(key) - if ok { - value, err := strconv.ParseBool(strValue) - if err == nil { - return value - } else { - panic(fmt.Sprintf("environment variable \"%s\" could not be converted to boolean", key)) - } - } - panic(fmt.Sprintf("expected environment variable \"%s\" does not exist", key)) + return MustGetParsed(key, ParseBool, "bool") } -// GetInt - get an environment variable as int +// Int functions func GetInt(key string) (int, error) { - value, err := strconv.ParseInt(os.Getenv(key), DecimalBase, Int32Bits) - return int(value), err + return GetParsed(key, ParseInt) } -// GetOrInt - get an environment variable or return default value if does not exist func GetOrInt(key string, defaultValue int) int { - strValue, ok := os.LookupEnv(key) - if ok { - value, err := strconv.ParseInt(strValue, DecimalBase, Int32Bits) - if err == nil { - return int(value) - } - } - return defaultValue + return GetOrParsed(key, defaultValue, ParseInt) } -// MustGetInt - get an environment variable or panic if does not exist func MustGetInt(key string) int { - strValue, ok := os.LookupEnv(key) - if ok { - value, err := strconv.ParseInt(strValue, DecimalBase, Int32Bits) - if err == nil { - return int(value) - } else { - panic(fmt.Sprintf("environment variable \"%s\" could not be converted to int", key)) - } - } - panic(fmt.Sprintf("expected environment variable \"%s\" does not exist", key)) + return MustGetParsed(key, ParseInt, "int") } -// GetUint - get an environment variable as uint +// Uint functions func GetUint(key string) (uint, error) { - value, err := strconv.ParseUint(os.Getenv(key), DecimalBase, Int32Bits) - return uint(value), err + return GetParsed(key, ParseUint) } -// GetOrUint - get an environment variable or return default value if does not exist func GetOrUint(key string, defaultValue uint) uint { - strValue, ok := os.LookupEnv(key) - if ok { - value, err := strconv.ParseUint(strValue, DecimalBase, Int32Bits) - if err == nil { - return uint(value) - } - } - return defaultValue + return GetOrParsed(key, defaultValue, ParseUint) } -// MustGetUint - get an environment variable or panic if does not exist func MustGetUint(key string) uint { - strValue, ok := os.LookupEnv(key) - if ok { - value, err := strconv.ParseUint(strValue, DecimalBase, Int32Bits) - if err == nil { - return uint(value) - } else { - panic(fmt.Sprintf("environment variable \"%s\" could not be converted to uint", key)) - } - } - panic(fmt.Sprintf("expected environment variable \"%s\" does not exist", key)) + return MustGetParsed(key, ParseUint, "uint") } -// GetFloat32 - get an environment variable as float32 +// Float32 functions func GetFloat32(key string) (float32, error) { - value, err := strconv.ParseFloat(os.Getenv(key), Float32Bits) - return float32(value), err + return GetParsed(key, ParseFloat32) } -// GetOrFloat32 - get an environment variable or return default value if does not exist func GetOrFloat32(key string, defaultValue float32) float32 { - strValue, ok := os.LookupEnv(key) - if ok { - value, err := strconv.ParseFloat(strValue, Float32Bits) - if err == nil { - return float32(value) - } - } - return defaultValue + return GetOrParsed(key, defaultValue, ParseFloat32) } -// MustGetFloat32 - get an environment variable or panic if does not exist func MustGetFloat32(key string) float32 { - strValue, ok := os.LookupEnv(key) - if ok { - value, err := strconv.ParseFloat(strValue, Float32Bits) - if err == nil { - return float32(value) - } else { - panic(fmt.Sprintf("environment variable \"%s\" could not be converted to float32", key)) - } - } - panic(fmt.Sprintf("expected environment variable \"%s\" does not exist", key)) + return MustGetParsed(key, ParseFloat32, "float32") } -// GetFloat64 - get an environment variable as float64 +// Float64 functions func GetFloat64(key string) (float64, error) { - value, err := strconv.ParseFloat(os.Getenv(key), Float64Bits) - return float64(value), err + return GetParsed(key, ParseFloat64) } -// GetOrFloat64 - get an environment variable or return default value if does not exist func GetOrFloat64(key string, defaultValue float64) float64 { - strValue, ok := os.LookupEnv(key) - if ok { - value, err := strconv.ParseFloat(strValue, Float64Bits) - if err == nil { - return float64(value) - } - } - return defaultValue + return GetOrParsed(key, defaultValue, ParseFloat64) } -// MustGetFloat64 - get an environment variable or panic if does not exist func MustGetFloat64(key string) float64 { - strValue, ok := os.LookupEnv(key) - if ok { - value, err := strconv.ParseFloat(strValue, Float64Bits) - if err == nil { - return float64(value) - } else { - panic(fmt.Sprintf("environment variable \"%s\" could not be converted to float64", key)) - } - } - panic(fmt.Sprintf("expected environment variable \"%s\" does not exist", key)) + return MustGetParsed(key, ParseFloat64, "float64") } -// GetInt64 - get an environment variable as int64 +// Int64 functions func GetInt64(key string) (int64, error) { - value, err := strconv.ParseInt(os.Getenv(key), DecimalBase, Int64Bits) - return int64(value), err + return GetParsed(key, ParseInt64) } -// GetOrInt64 - get an environment variable or return default value if does not exist func GetOrInt64(key string, defaultValue int64) int64 { - strValue, ok := os.LookupEnv(key) - if ok { - value, err := strconv.ParseInt(strValue, DecimalBase, Int64Bits) - if err == nil { - return int64(value) - } - } - return defaultValue + return GetOrParsed(key, defaultValue, ParseInt64) } -// MustGetInt64 - get an environment variable or panic if does not exist func MustGetInt64(key string) int64 { - strValue, ok := os.LookupEnv(key) - if ok { - value, err := strconv.ParseInt(strValue, DecimalBase, Int64Bits) - if err == nil { - return int64(value) - } else { - panic(fmt.Sprintf("environment variable \"%s\" could not be converted to int64", key)) - } - } - panic(fmt.Sprintf("expected environment variable \"%s\" does not exist", key)) + return MustGetParsed(key, ParseInt64, "int64") } -// GetUint64 - get an environment variable as uint +// Uint64 functions func GetUint64(key string) (uint64, error) { - value, err := strconv.ParseUint(os.Getenv(key), DecimalBase, Int64Bits) - return uint64(value), err + return GetParsed(key, ParseUint64) } -// GetOrUint64 - get an environment variable or return default value if does not exist func GetOrUint64(key string, defaultValue uint64) uint64 { - strValue, ok := os.LookupEnv(key) - if ok { - value, err := strconv.ParseUint(strValue, DecimalBase, Int64Bits) - if err == nil { - return uint64(value) - } - } - return defaultValue + return GetOrParsed(key, defaultValue, ParseUint64) } -// MustGetUint64 - get an environment variable or panic if does not exist func MustGetUint64(key string) uint64 { - strValue, ok := os.LookupEnv(key) - if ok { - value, err := strconv.ParseUint(strValue, DecimalBase, Int64Bits) - if err == nil { - return uint64(value) - } else { - panic(fmt.Sprintf("environment variable \"%s\" could not be converted to uint64", key)) - } - } - panic(fmt.Sprintf("expected environment variable \"%s\" does not exist", key)) + return MustGetParsed(key, ParseUint64, "uint64") } -// GetDuration - get an environment variable as time.Duration +// Duration functions func GetDuration(key string) (time.Duration, error) { - value, err := time.ParseDuration(os.Getenv(key)) - return value, err + return GetParsed(key, ParseDuration) } -// GetOrDuration - get an environment variable or return default value if does not exist func GetOrDuration(key string, defaultValue string) time.Duration { strValue, ok := os.LookupEnv(key) if ok { @@ -300,27 +235,15 @@ func GetOrDuration(key string, defaultValue string) time.Duration { return defaultDuration } -// MustGetDuration - get an environment variable or panic if does not exist func MustGetDuration(key string) time.Duration { - strValue, ok := os.LookupEnv(key) - if ok { - value, err := time.ParseDuration(strValue) - if err == nil { - return value - } else { - panic(fmt.Sprintf("environment variable \"%s\" could not be converted to time.Duration", key)) - } - } - panic(fmt.Sprintf("expected environment variable \"%s\" does not exist", key)) + return MustGetParsed(key, ParseDuration, "time.Duration") } -// GetUrl - get an environment variable as url.URL +// URL functions func GetUrl(key string) (*url.URL, error) { - value, err := url.ParseRequestURI(os.Getenv(key)) - return value, err + return GetParsed(key, ParseURL) } -// GetOrUrl - get an environment variable or return default value if does not exist func GetOrUrl(key string, defaultValue string) *url.URL { strValue, ok := os.LookupEnv(key) if ok { @@ -336,16 +259,6 @@ func GetOrUrl(key string, defaultValue string) *url.URL { return defaultUrl } -// MustGetUrl - get an environment variable or panic if does not exist func MustGetUrl(key string) *url.URL { - strValue, ok := os.LookupEnv(key) - if ok { - value, err := url.ParseRequestURI(strValue) - if err == nil { - return value - } else { - panic(fmt.Sprintf("environment variable \"%s\" could not be converted to url.URL", key)) - } - } - panic(fmt.Sprintf("expected environment variable \"%s\" does not exist", key)) -} + return MustGetParsed(key, ParseURL, "url.URL") +} \ No newline at end of file From 759a9bbe62280b7f8090dd92d61af1911a2a7a83 Mon Sep 17 00:00:00 2001 From: Ray Johnson Date: Fri, 26 Sep 2025 23:52:41 +0000 Subject: [PATCH 05/12] improved the docuementation a bunch --- doc.go | 74 +++++++++++++++ env.go | 55 +++++++++-- example_test.go | 244 ++++++++++++++++++++++++++++++++++++++++++++++++ file.go | 108 +++++++++++++-------- util.go | 128 +++++++++++++++++++++---- 5 files changed, 544 insertions(+), 65 deletions(-) create mode 100644 doc.go create mode 100644 example_test.go diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..32e6960 --- /dev/null +++ b/doc.go @@ -0,0 +1,74 @@ +// Package env provides utilities for working with environment variables and loading +// configuration from .env files. +// +// This package offers three main areas of functionality: +// +// 1. Simple environment variable access with type conversion +// 2. Struct-based configuration parsing using struct tags +// 3. Loading and parsing of .env files +// +// # Basic Usage +// +// Get environment variables with automatic type conversion: +// +// port, err := env.GetInt("PORT") +// if err != nil { +// port = env.GetOrInt("PORT", 8080) // with default +// } +// +// // Or panic if missing/invalid +// dbHost := env.MustGet("DATABASE_HOST") +// +// # Struct-based Configuration +// +// Parse environment variables into structs using tags: +// +// type Config struct { +// Host string `env:"HOST" envDefault:"localhost"` +// Port int `env:"PORT" envDefault:"8080"` +// Debug bool `env:"DEBUG"` +// } +// +// var cfg Config +// if err := env.Parse(&cfg); err != nil { +// log.Fatal(err) +// } +// +// # File Loading +// +// Load environment variables from .env files: +// +// // Load from .env file +// err := env.Load() +// if err != nil { +// log.Fatal(err) +// } +// +// // Load from specific files +// err = env.Load("config.env", "local.env") +// +// # Supported Types +// +// The package supports automatic conversion for: +// - bool, int, uint, int64, uint64, float32, float64 +// - time.Duration (using time.ParseDuration format) +// - *url.URL (using url.ParseRequestURI) +// - []string and other slice types (comma-separated by default) +// - Any type implementing encoding.TextUnmarshaler +// +// # Struct Tags +// +// When using Parse functions, the following struct tags are supported: +// - env:"VAR_NAME" - specifies the environment variable name +// - envDefault:"value" - provides a default value if the variable is not set +// - required:"true" - makes the field required (parsing fails if missing) +// - envSeparator:";" - custom separator for slice types (default is comma) +// - envExpand:"true" - enables variable expansion using os.ExpandEnv +// +// # Error Handling +// +// Functions come in three variants for different error handling approaches: +// - Get*() functions return (value, error) +// - GetOr*() functions return value with a fallback default +// - MustGet*() functions panic if the variable is missing or invalid +package env \ No newline at end of file diff --git a/env.go b/env.go index af04ec2..c6fbc83 100644 --- a/env.go +++ b/env.go @@ -67,32 +67,67 @@ var ( sliceOfURLs = reflect.TypeOf([]url.URL(nil)) ) -// CustomParsers is a friendly name for the type that `ParseWithFuncs()` accepts +// CustomParsers maps Go types to custom parsing functions. +// It allows you to provide custom logic for parsing environment variables +// into specific types that aren't supported by default. +// +// The key is the reflect.Type of the target type, and the value is a ParserFunc +// that knows how to convert a string to that type. type CustomParsers map[reflect.Type]ParserFunc -// ParserFunc defines the signature of a function that can be used within `CustomParsers` +// ParserFunc defines the signature of a custom parsing function. +// It takes a string value from an environment variable and returns +// the parsed value as an interface{} and any parsing error. +// +// The returned value should be of the type that the parser is designed to handle. type ParserFunc func(v string) (interface{}, error) -// Parse parses a struct containing `env` tags and loads its values from -// environment variables. +// Parse populates a struct's fields from environment variables. +// The struct fields must be tagged with `env:"VAR_NAME"` to specify +// which environment variable to read. +// +// Supported struct tags: +// - env:"VAR_NAME" - specifies the environment variable name (required) +// - envDefault:"value" - default value if the environment variable is not set +// - required:"true" - makes the field required (causes error if missing) +// - envSeparator:"," - separator for slice types (default is comma) +// - envExpand:"true" - enables variable expansion using os.ExpandEnv +// +// The function supports nested structs and pointers to structs. +// It returns an error if required fields are missing or if type conversion fails. func Parse(v interface{}) error { return ParseWithPrefixFuncs(v, "", make(map[reflect.Type]ParserFunc)) } -// ParseWithPrefix parses a struct containing `env` tags and loads its values from -// environment variables. The actual env vars looked up include the passed in prefix. +// ParseWithPrefix populates a struct's fields from environment variables with a prefix. +// This is useful for loading different configurations for the same struct type. +// +// For example, with prefix "CLIENT2_", a field tagged `env:"ENDPOINT"` will +// read from the environment variable "CLIENT2_ENDPOINT". +// +// See Parse for details on supported struct tags and behavior. func ParseWithPrefix(v interface{}, prefix string) error { return ParseWithPrefixFuncs(v, prefix, make(map[reflect.Type]ParserFunc)) } -// ParseWithFuncs is the same as `Parse` except it also allows the user to pass -// in custom parsers. +// ParseWithFuncs populates a struct's fields from environment variables, +// using custom parsing functions for specific types. +// +// This allows you to handle types that aren't supported by default. +// The funcMap parameter maps reflect.Type values to ParserFunc implementations. +// +// See Parse for details on supported struct tags and behavior. func ParseWithFuncs(v interface{}, funcMap CustomParsers) error { return ParseWithPrefixFuncs(v, "", funcMap) } -// ParseWithPrefixFuncs is the same as `ParseWithPrefix` except it also allows the user to pass -// in custom parsers. +// ParseWithPrefixFuncs populates a struct's fields from environment variables +// with both a prefix and custom parsing functions. +// +// This combines the functionality of ParseWithPrefix and ParseWithFuncs, +// allowing both prefixed variable names and custom type parsing. +// +// See Parse for details on supported struct tags and behavior. func ParseWithPrefixFuncs(v interface{}, prefix string, funcMap CustomParsers) error { ptrRef := reflect.ValueOf(v) if ptrRef.Kind() != reflect.Ptr { diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..7ae0862 --- /dev/null +++ b/example_test.go @@ -0,0 +1,244 @@ +package env_test + +import ( + "fmt" + "log" + "os" + "time" + + "github.com/lindenlab/env" +) + +func ExampleGet() { + os.Setenv("HOME", "/home/user") + fmt.Println(env.Get("HOME")) + fmt.Println(env.Get("NONEXISTENT")) + // Output: + // /home/user + // +} + +func ExampleGetOr() { + os.Setenv("PORT", "8080") + fmt.Println(env.GetOr("PORT", "3000")) + fmt.Println(env.GetOr("MISSING_PORT", "3000")) + // Output: + // 8080 + // 3000 +} + +func ExampleGetInt() { + os.Setenv("PORT", "8080") + os.Setenv("INVALID_PORT", "not-a-number") + + port, err := env.GetInt("PORT") + if err != nil { + log.Fatal(err) + } + fmt.Println("Port:", port) + + _, err = env.GetInt("INVALID_PORT") + fmt.Println("Error:", err != nil) + // Output: + // Port: 8080 + // Error: true +} + +func ExampleGetOrInt() { + os.Setenv("PORT", "8080") + fmt.Println("Existing:", env.GetOrInt("PORT", 3000)) + fmt.Println("Missing:", env.GetOrInt("MISSING_PORT", 3000)) + fmt.Println("Invalid:", env.GetOrInt("INVALID_PORT", 3000)) + // Output: + // Existing: 8080 + // Missing: 3000 + // Invalid: 3000 +} + +func ExampleGetBool() { + os.Setenv("DEBUG", "true") + os.Setenv("VERBOSE", "1") + os.Setenv("QUIET", "false") + + debug, _ := env.GetBool("DEBUG") + verbose, _ := env.GetBool("VERBOSE") + quiet, _ := env.GetBool("QUIET") + + fmt.Println("Debug:", debug) + fmt.Println("Verbose:", verbose) + fmt.Println("Quiet:", quiet) + // Output: + // Debug: true + // Verbose: true + // Quiet: false +} + +func ExampleGetDuration() { + os.Setenv("TIMEOUT", "30s") + os.Setenv("INTERVAL", "5m30s") + + timeout, _ := env.GetDuration("TIMEOUT") + interval, _ := env.GetDuration("INTERVAL") + + fmt.Println("Timeout:", timeout) + fmt.Println("Interval:", interval) + // Output: + // Timeout: 30s + // Interval: 5m30s +} + +func ExampleLoad() { + // Create a temporary .env file + envContent := `# Database configuration +DATABASE_HOST=localhost +DATABASE_PORT=5432 +DATABASE_NAME=myapp + +# Application settings +DEBUG=true +LOG_LEVEL=info` + + err := os.WriteFile(".env", []byte(envContent), 0644) + if err != nil { + log.Fatal(err) + } + defer os.Remove(".env") + + // Load the .env file + err = env.Load() + if err != nil { + log.Fatal(err) + } + + // Access the loaded variables + fmt.Println("Database Host:", env.Get("DATABASE_HOST")) + fmt.Println("Database Port:", env.GetOrInt("DATABASE_PORT", 0)) + fmt.Println("Debug Mode:", env.GetOrBool("DEBUG", false)) + // Output: + // Database Host: localhost + // Database Port: 5432 + // Debug Mode: true +} + +func ExampleParse() { + // Set up environment variables + os.Setenv("HOME", "/tmp/fakehome") + os.Setenv("PORT", "8080") + os.Setenv("DEBUG", "true") + os.Setenv("TAGS", "web,api,database") + os.Setenv("TIMEOUT", "30s") + + type Config struct { + Home string `env:"HOME"` + Port int `env:"PORT" envDefault:"3000"` + Debug bool `env:"DEBUG"` + Tags []string `env:"TAGS" envSeparator:","` + Timeout time.Duration `env:"TIMEOUT" envDefault:"10s"` + Version string `env:"VERSION" envDefault:"1.0.0"` + Required string `env:"REQUIRED_VAR" required:"true"` + } + + // This will fail because REQUIRED_VAR is not set + var cfg Config + err := env.Parse(&cfg) + if err != nil { + fmt.Println("Error:", err != nil) + } + + // Set the required variable and try again + os.Setenv("REQUIRED_VAR", "important-value") + err = env.Parse(&cfg) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Home: %s\n", cfg.Home) + fmt.Printf("Port: %d\n", cfg.Port) + fmt.Printf("Debug: %t\n", cfg.Debug) + fmt.Printf("Tags: %v\n", cfg.Tags) + fmt.Printf("Timeout: %v\n", cfg.Timeout) + fmt.Printf("Version: %s\n", cfg.Version) + fmt.Printf("Required: %s\n", cfg.Required) + // Output: + // Error: true + // Home: /tmp/fakehome + // Port: 8080 + // Debug: true + // Tags: [web api database] + // Timeout: 30s + // Version: 1.0.0 + // Required: important-value +} + +func ExampleParseWithPrefix() { + // Set up environment variables with prefixes + os.Setenv("CLIENT1_HOST", "api.example.com") + os.Setenv("CLIENT1_PORT", "443") + os.Setenv("CLIENT2_HOST", "internal.example.com") + os.Setenv("CLIENT2_PORT", "8080") + + type ClientConfig struct { + Host string `env:"HOST" envDefault:"localhost"` + Port int `env:"PORT" envDefault:"80"` + } + + var client1, client2 ClientConfig + + env.ParseWithPrefix(&client1, "CLIENT1_") + env.ParseWithPrefix(&client2, "CLIENT2_") + + fmt.Printf("Client 1: %s:%d\n", client1.Host, client1.Port) + fmt.Printf("Client 2: %s:%d\n", client2.Host, client2.Port) + // Output: + // Client 1: api.example.com:443 + // Client 2: internal.example.com:8080 +} + +func ExampleRead() { + // Create a temporary .env file + envContent := `DATABASE_URL=postgres://localhost/myapp +REDIS_URL=redis://localhost:6379 +API_KEY=secret123 +DEBUG=true` + + err := os.WriteFile("config.env", []byte(envContent), 0644) + if err != nil { + log.Fatal(err) + } + defer os.Remove("config.env") + + // Read the file without setting environment variables + envMap, err := env.Read("config.env") + if err != nil { + log.Fatal(err) + } + + fmt.Println("Database URL:", envMap["DATABASE_URL"]) + fmt.Println("Redis URL:", envMap["REDIS_URL"]) + fmt.Println("Debug:", envMap["DEBUG"]) + // Output: + // Database URL: postgres://localhost/myapp + // Redis URL: redis://localhost:6379 + // Debug: true +} + +func ExampleMarshal() { + envMap := map[string]string{ + "DATABASE_URL": "postgres://localhost/myapp", + "DEBUG": "true", + "PORT": "8080", + "API_KEY": "secret with spaces", + } + + content, err := env.Marshal(envMap) + if err != nil { + log.Fatal(err) + } + + fmt.Println(content) + // Output: + // API_KEY="secret with spaces" + // DATABASE_URL="postgres://localhost/myapp" + // DEBUG="true" + // PORT="8080" +} \ No newline at end of file diff --git a/file.go b/file.go index a384036..17066a4 100644 --- a/file.go +++ b/file.go @@ -13,17 +13,19 @@ import ( const doubleQuoteSpecialChars = "\\\n\r\"!$`" -// Load will read your env file(s) and load them into ENV for this process. +// Load reads environment variables from .env files and sets them in the current process. +// If no filenames are provided, it defaults to loading ".env" from the current directory. // -// Call this function as close as possible to the start of your program (ideally in main) +// Multiple files can be loaded in order: // -// # If you call Load without any args it will default to loading .env in the current path +// err := env.Load("base.env", "local.env") // -// You can otherwise tell it which files to load (there can be more than one) like +// Important: Load will NOT override environment variables that are already set. +// This allows .env files to provide defaults while respecting existing environment configuration. +// Use Overload if you need to override existing variables. // -// err := env.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 +// The function returns an error if any file cannot be read or parsed. +// Call this early in your program, typically in main(). func Load(filenames ...string) (err error) { filenames = filenamesOrDefault(filenames) @@ -36,18 +38,17 @@ func Load(filenames ...string) (err error) { return } -// MustLoad 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) +// MustLoad reads environment variables from .env files and sets them in the current process. +// If no filenames are provided, it defaults to loading ".env" from the current directory. // -// If you call Load without any args it will default to loading .env in the current path. -// If there are any errors the function will panic. +// This function behaves exactly like Load, except it panics on any error instead of returning it. +// Use this when .env file loading is critical for your application to function. // -// You can otherwise tell it which files to load (there can be more than one) like +// Multiple files can be loaded in order: // -// env.MustLoad("fileone", "filetwo") +// env.MustLoad("base.env", "local.env") // -// 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 +// Like Load, this will NOT override environment variables that are already set. func MustLoad(filenames ...string) { filenames = filenamesOrDefault(filenames) @@ -59,17 +60,18 @@ func MustLoad(filenames ...string) { } } -// Overload will read your env file(s) and load them into ENV for this process. +// Overload reads environment variables from .env files and sets them in the current process, +// overriding any existing environment variables. +// If no filenames are provided, it defaults to loading ".env" from the current directory. // -// Call this function as close as possible to the start of your program (ideally in main) +// Unlike Load, this function WILL override environment variables that are already set. +// Use this when you want .env files to take precedence over existing environment configuration. // -// # If you call Overload without any args it will default to loading .env in the current path +// Multiple files can be loaded in order: // -// You can otherwise tell it which files to load (there can be more than one) like +// err := env.Overload("base.env", "production.env") // -// err := env.Overload("fileone", "filetwo") -// -// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefilly set all vars. +// The function returns an error if any file cannot be read or parsed. func Overload(filenames ...string) (err error) { filenames = filenamesOrDefault(filenames) @@ -82,17 +84,16 @@ func Overload(filenames ...string) (err error) { return } -// MustOverload 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) +// MustOverload reads environment variables from .env files and sets them in the current process, +// overriding any existing environment variables. +// If no filenames are provided, it defaults to loading ".env" from the current directory. // -// # If you call Overload without any args it will default to loading .env in the current path +// This function behaves exactly like Overload, except it panics on any error instead of returning it. +// Use this when .env file loading is critical and should override existing configuration. // -// You can otherwise tell it which files to load (there can be more than one) like +// Multiple files can be loaded in order: // -// env.MustOverload("fileone", "filetwo") -// -// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefilly set all vars. +// env.MustOverload("base.env", "production.env") func MustOverload(filenames ...string) { filenames = filenamesOrDefault(filenames) @@ -104,8 +105,20 @@ func MustOverload(filenames ...string) { } } -// Read all env (with same file loading semantics as Load) but return values as -// a map rather than automatically writing values into env +// Read parses .env files and returns the key-value pairs as a map, +// without setting them as environment variables in the current process. +// +// This is useful when you want to inspect or manipulate the values before +// applying them, or when you want to use the values in a different way. +// If no filenames are provided, it defaults to reading ".env" from the current directory. +// +// Multiple files can be read, with later files overriding earlier ones: +// +// envMap, err := env.Read("base.env", "local.env") +// if err != nil { +// log.Fatal(err) +// } +// fmt.Printf("DATABASE_URL=%s\n", envMap["DATABASE_URL"]) func Read(filenames ...string) (envMap map[string]string, err error) { filenames = filenamesOrDefault(filenames) envMap = make(map[string]string) @@ -126,7 +139,17 @@ func Read(filenames ...string) (envMap map[string]string, err error) { return } -// ParseIO reads an env file from io.Reader, returning a map of keys and values. +// ParseIO reads environment variables from an io.Reader in .env format. +// It parses the content and returns a map of key-value pairs. +// +// This function supports the standard .env file format including: +// - Comments (lines starting with #) +// - Quoted values (both single and double quotes) +// - Variable expansion (${VAR} and $VAR syntax) +// - Multiline values +// - Both KEY=value and KEY: value formats +// +// It does not set any environment variables; it only parses and returns the data. func ParseIO(r io.Reader) (envMap map[string]string, err error) { envMap = make(map[string]string) @@ -154,12 +177,20 @@ func ParseIO(r io.Reader) (envMap map[string]string, err error) { return } -// Unmarshal reads an env file from a string, returning a map of keys and values. +// Unmarshal parses environment variables from a string in .env format. +// It returns a map of key-value pairs without setting any environment variables. +// +// This is a convenience function that wraps ParseIO with a strings.NewReader. +// See ParseIO for details on the supported .env file format. func Unmarshal(str string) (envMap map[string]string, err error) { return ParseIO(strings.NewReader(str)) } -// Write serializes the given environment and writes it to a file +// Write serializes a map of environment variables to a .env file. +// The output file will contain KEY="VALUE" pairs, one per line, +// with keys sorted alphabetically and values properly escaped. +// +// This function will create or overwrite the specified file. func Write(envMap map[string]string, filename string) error { content, error := Marshal(envMap) if error != nil { @@ -173,8 +204,11 @@ func Write(envMap map[string]string, filename string) error { return err } -// Marshal outputs the given environment as a dotenv-formatted environment file. -// Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped. +// Marshal converts a map of environment variables to .env file format. +// It returns a string with KEY="VALUE" pairs, one per line, +// with keys sorted alphabetically and values properly escaped with backslashes. +// +// This function does not write to any file; use Write to save the output to disk. func Marshal(envMap map[string]string) (string, error) { lines := make([]string, 0, len(envMap)) for k, v := range envMap { diff --git a/util.go b/util.go index 3567425..4fd6f09 100644 --- a/util.go +++ b/util.go @@ -24,16 +24,26 @@ func isValidEnvVarKey(key string) bool { // Generic types and functions for reducing code duplication -// Parser function type for converting strings to any type +// Parser is a function type that converts a string value to type T. +// It is used by the generic parsing functions to provide type-safe +// conversion from environment variable string values. type Parser[T any] func(string) (T, error) -// GetParsed - generic function for getting parsed environment variables +// GetParsed retrieves an environment variable and parses it using the provided parser function. +// It returns the parsed value and any parsing error. If the environment variable is not set, +// the parser receives an empty string. +// +// This is a generic function that can be used with any type that has a corresponding parser. +// For common types, use the specific Get* functions which are more convenient. func GetParsed[T any](key string, parser Parser[T]) (T, error) { value := os.Getenv(key) return parser(value) } -// GetOrParsed - generic function with default value +// GetOrParsed retrieves an environment variable and parses it using the provided parser function. +// If the environment variable is not set or parsing fails, it returns the default value. +// +// This function never returns an error; it falls back to the default value on any failure. func GetOrParsed[T any](key string, defaultValue T, parser Parser[T]) T { strValue, ok := os.LookupEnv(key) if ok { @@ -44,7 +54,11 @@ func GetOrParsed[T any](key string, defaultValue T, parser Parser[T]) T { return defaultValue } -// MustGetParsed - generic function that panics on missing/invalid values +// MustGetParsed retrieves an environment variable and parses it using the provided parser function. +// If the environment variable is not set or parsing fails, it panics with a descriptive message. +// +// The typeName parameter is used in panic messages to identify the expected type. +// Use this function when the environment variable is required for the application to function. func MustGetParsed[T any](key string, parser Parser[T], typeName string) T { strValue, ok := os.LookupEnv(key) if ok { @@ -91,22 +105,34 @@ var ( } ) -// Set - sets an environment variable +// Set sets an environment variable to the specified value. +// It returns an error if the operation fails. +// +// This is a simple wrapper around os.Setenv for consistency with other functions in this package. func Set(key, value string) error { return os.Setenv(key, value) } -// Unset - unsets an environment variable +// Unset removes an environment variable. +// It returns an error if the operation fails. +// +// This is a simple wrapper around os.Unsetenv for consistency with other functions in this package. func Unset(key string) error { return os.Unsetenv(key) } -// Get - get an environment variable, empty string if does not exist +// Get retrieves the value of an environment variable. +// If the variable is not set, it returns an empty string. +// +// This is a simple wrapper around os.Getenv for consistency with other functions in this package. func Get(key string) string { return os.Getenv(key) } -// GetOr - get an environment variable or return default value if does not exist +// GetOr retrieves the value of an environment variable. +// If the variable is not set, it returns the provided default value. +// +// This function distinguishes between unset variables and variables set to empty strings. func GetOr(key, defaultValue string) string { value, ok := os.LookupEnv(key) if ok { @@ -115,7 +141,10 @@ func GetOr(key, defaultValue string) string { return defaultValue } -// MustGet - get an environment variable or panic if does not exist +// MustGet retrieves the value of an environment variable. +// If the variable is not set, it panics with a descriptive message. +// +// Use this function when the environment variable is required for the application to function. func MustGet(key string) string { value, ok := os.LookupEnv(key) if ok { @@ -124,102 +153,153 @@ func MustGet(key string) string { panic(fmt.Sprintf("expected environment variable \"%s\" does not exist", key)) } -// Bool functions +// GetBool retrieves an environment variable and parses it as a boolean. +// It accepts values like "true", "false", "1", "0", "t", "f", "T", "F", "TRUE", "FALSE" (case-insensitive). +// Returns the parsed boolean value and any parsing error. func GetBool(key string) (bool, error) { return GetParsed(key, ParseBool) } +// GetOrBool retrieves an environment variable and parses it as a boolean. +// If the variable is not set or parsing fails, it returns the default value. func GetOrBool(key string, defaultValue bool) bool { return GetOrParsed(key, defaultValue, ParseBool) } +// MustGetBool retrieves an environment variable and parses it as a boolean. +// If the variable is not set or parsing fails, it panics. func MustGetBool(key string) bool { return MustGetParsed(key, ParseBool, "bool") } -// Int functions +// GetInt retrieves an environment variable and parses it as a signed integer. +// It accepts decimal integers that fit in the int type (platform-dependent size). +// Returns the parsed integer value and any parsing error. func GetInt(key string) (int, error) { return GetParsed(key, ParseInt) } +// GetOrInt retrieves an environment variable and parses it as a signed integer. +// If the variable is not set or parsing fails, it returns the default value. func GetOrInt(key string, defaultValue int) int { return GetOrParsed(key, defaultValue, ParseInt) } +// MustGetInt retrieves an environment variable and parses it as a signed integer. +// If the variable is not set or parsing fails, it panics. func MustGetInt(key string) int { return MustGetParsed(key, ParseInt, "int") } -// Uint functions +// GetUint retrieves an environment variable and parses it as an unsigned integer. +// It accepts non-negative decimal integers that fit in the uint type (platform-dependent size). +// Returns the parsed unsigned integer value and any parsing error. func GetUint(key string) (uint, error) { return GetParsed(key, ParseUint) } +// GetOrUint retrieves an environment variable and parses it as an unsigned integer. +// If the variable is not set or parsing fails, it returns the default value. func GetOrUint(key string, defaultValue uint) uint { return GetOrParsed(key, defaultValue, ParseUint) } +// MustGetUint retrieves an environment variable and parses it as an unsigned integer. +// If the variable is not set or parsing fails, it panics. func MustGetUint(key string) uint { return MustGetParsed(key, ParseUint, "uint") } -// Float32 functions +// GetFloat32 retrieves an environment variable and parses it as a 32-bit floating point number. +// It accepts decimal numbers in standard or scientific notation. +// Returns the parsed float32 value and any parsing error. func GetFloat32(key string) (float32, error) { return GetParsed(key, ParseFloat32) } +// GetOrFloat32 retrieves an environment variable and parses it as a 32-bit floating point number. +// If the variable is not set or parsing fails, it returns the default value. func GetOrFloat32(key string, defaultValue float32) float32 { return GetOrParsed(key, defaultValue, ParseFloat32) } +// MustGetFloat32 retrieves an environment variable and parses it as a 32-bit floating point number. +// If the variable is not set or parsing fails, it panics. func MustGetFloat32(key string) float32 { return MustGetParsed(key, ParseFloat32, "float32") } -// Float64 functions +// GetFloat64 retrieves an environment variable and parses it as a 64-bit floating point number. +// It accepts decimal numbers in standard or scientific notation. +// Returns the parsed float64 value and any parsing error. func GetFloat64(key string) (float64, error) { return GetParsed(key, ParseFloat64) } +// GetOrFloat64 retrieves an environment variable and parses it as a 64-bit floating point number. +// If the variable is not set or parsing fails, it returns the default value. func GetOrFloat64(key string, defaultValue float64) float64 { return GetOrParsed(key, defaultValue, ParseFloat64) } +// MustGetFloat64 retrieves an environment variable and parses it as a 64-bit floating point number. +// If the variable is not set or parsing fails, it panics. func MustGetFloat64(key string) float64 { return MustGetParsed(key, ParseFloat64, "float64") } -// Int64 functions +// GetInt64 retrieves an environment variable and parses it as a 64-bit signed integer. +// It accepts decimal integers in the range -9223372036854775808 to 9223372036854775807. +// Returns the parsed int64 value and any parsing error. func GetInt64(key string) (int64, error) { return GetParsed(key, ParseInt64) } +// GetOrInt64 retrieves an environment variable and parses it as a 64-bit signed integer. +// If the variable is not set or parsing fails, it returns the default value. func GetOrInt64(key string, defaultValue int64) int64 { return GetOrParsed(key, defaultValue, ParseInt64) } +// MustGetInt64 retrieves an environment variable and parses it as a 64-bit signed integer. +// If the variable is not set or parsing fails, it panics. func MustGetInt64(key string) int64 { return MustGetParsed(key, ParseInt64, "int64") } -// Uint64 functions +// GetUint64 retrieves an environment variable and parses it as a 64-bit unsigned integer. +// It accepts non-negative decimal integers in the range 0 to 18446744073709551615. +// Returns the parsed uint64 value and any parsing error. func GetUint64(key string) (uint64, error) { return GetParsed(key, ParseUint64) } +// GetOrUint64 retrieves an environment variable and parses it as a 64-bit unsigned integer. +// If the variable is not set or parsing fails, it returns the default value. func GetOrUint64(key string, defaultValue uint64) uint64 { return GetOrParsed(key, defaultValue, ParseUint64) } +// MustGetUint64 retrieves an environment variable and parses it as a 64-bit unsigned integer. +// If the variable is not set or parsing fails, it panics. func MustGetUint64(key string) uint64 { return MustGetParsed(key, ParseUint64, "uint64") } -// Duration functions +// GetDuration retrieves an environment variable and parses it as a time.Duration. +// It accepts duration strings like "5s", "2m30s", "1h", "300ms", etc. +// See time.ParseDuration for the complete format specification. +// Returns the parsed duration value and any parsing error. func GetDuration(key string) (time.Duration, error) { return GetParsed(key, ParseDuration) } +// GetOrDuration retrieves an environment variable and parses it as a time.Duration. +// If the variable is not set or parsing fails, it parses and returns the default value. +// The default value must be a valid duration string, or the function will panic. +// +// Note: This function takes a string default value (unlike other GetOr* functions) +// to maintain backwards compatibility with existing APIs. func GetOrDuration(key string, defaultValue string) time.Duration { strValue, ok := os.LookupEnv(key) if ok { @@ -235,15 +315,25 @@ func GetOrDuration(key string, defaultValue string) time.Duration { return defaultDuration } +// MustGetDuration retrieves an environment variable and parses it as a time.Duration. +// If the variable is not set or parsing fails, it panics. func MustGetDuration(key string) time.Duration { return MustGetParsed(key, ParseDuration, "time.Duration") } -// URL functions +// GetUrl retrieves an environment variable and parses it as a URL. +// It accepts absolute URLs and parses them using url.ParseRequestURI. +// Returns the parsed *url.URL value and any parsing error. func GetUrl(key string) (*url.URL, error) { return GetParsed(key, ParseURL) } +// GetOrUrl retrieves an environment variable and parses it as a URL. +// If the variable is not set or parsing fails, it parses and returns the default value. +// The default value must be a valid URL string, or the function will panic. +// +// Note: This function takes a string default value (unlike other GetOr* functions) +// to maintain backwards compatibility with existing APIs. func GetOrUrl(key string, defaultValue string) *url.URL { strValue, ok := os.LookupEnv(key) if ok { @@ -259,6 +349,8 @@ func GetOrUrl(key string, defaultValue string) *url.URL { return defaultUrl } +// MustGetUrl retrieves an environment variable and parses it as a URL. +// If the variable is not set or parsing fails, it panics. func MustGetUrl(key string) *url.URL { return MustGetParsed(key, ParseURL, "url.URL") } \ No newline at end of file From 95e27b48c9af27933915d024184221dc78a0abc9 Mon Sep 17 00:00:00 2001 From: Ray Johnson Date: Sat, 27 Sep 2025 00:02:58 +0000 Subject: [PATCH 06/12] lint --- doc.go | 2 +- example_test.go | 16 +++++++++++----- file_test.go | 9 ++++++--- util.go | 2 +- util_test.go | 16 ++++++++-------- 5 files changed, 27 insertions(+), 18 deletions(-) diff --git a/doc.go b/doc.go index 32e6960..6487c25 100644 --- a/doc.go +++ b/doc.go @@ -71,4 +71,4 @@ // - Get*() functions return (value, error) // - GetOr*() functions return value with a fallback default // - MustGet*() functions panic if the variable is missing or invalid -package env \ No newline at end of file +package env diff --git a/example_test.go b/example_test.go index 7ae0862..879961d 100644 --- a/example_test.go +++ b/example_test.go @@ -98,7 +98,7 @@ DATABASE_NAME=myapp DEBUG=true LOG_LEVEL=info` - err := os.WriteFile(".env", []byte(envContent), 0644) + err := os.WriteFile(".env", []byte(envContent), 0600) if err != nil { log.Fatal(err) } @@ -184,8 +184,14 @@ func ExampleParseWithPrefix() { var client1, client2 ClientConfig - env.ParseWithPrefix(&client1, "CLIENT1_") - env.ParseWithPrefix(&client2, "CLIENT2_") + err := env.ParseWithPrefix(&client1, "CLIENT1_") + if err != nil { + log.Fatal(err) + } + err = env.ParseWithPrefix(&client2, "CLIENT2_") + if err != nil { + log.Fatal(err) + } fmt.Printf("Client 1: %s:%d\n", client1.Host, client1.Port) fmt.Printf("Client 2: %s:%d\n", client2.Host, client2.Port) @@ -201,7 +207,7 @@ REDIS_URL=redis://localhost:6379 API_KEY=secret123 DEBUG=true` - err := os.WriteFile("config.env", []byte(envContent), 0644) + err := os.WriteFile("config.env", []byte(envContent), 0600) if err != nil { log.Fatal(err) } @@ -241,4 +247,4 @@ func ExampleMarshal() { // DATABASE_URL="postgres://localhost/myapp" // DEBUG="true" // PORT="8080" -} \ No newline at end of file +} diff --git a/file_test.go b/file_test.go index 1699e07..6b2b8ac 100644 --- a/file_test.go +++ b/file_test.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "reflect" + "strconv" "strings" "testing" @@ -492,7 +493,7 @@ TEST_VAR8=value8 TEST_VAR9=value9 TEST_VAR10=value10` - err := os.WriteFile(tempFile, []byte(envContent), 0644) + err := os.WriteFile(tempFile, []byte(envContent), 0600) if err != nil { b.Fatalf("Failed to create temp file: %v", err) } @@ -526,7 +527,7 @@ TEST_VAR3=new_value3 EXISTING_VAR1=overridden1 EXISTING_VAR2=overridden2` - err := os.WriteFile(tempFile, []byte(envContent), 0644) + err := os.WriteFile(tempFile, []byte(envContent), 0600) if err != nil { b.Fatalf("Failed to create temp file: %v", err) } @@ -553,7 +554,9 @@ func BenchmarkParseUint64s(b *testing.B) { // Create test data with many uint64 values testData := make([]string, 100) for i := 0; i < 100; i++ { - testData[i] = fmt.Sprintf("%d", uint64(i*12345)) + // Use strconv.Itoa to avoid conversion issues + baseVal := i * 12345 + testData[i] = strconv.Itoa(baseVal) } b.ResetTimer() diff --git a/util.go b/util.go index 4fd6f09..d3995e7 100644 --- a/util.go +++ b/util.go @@ -353,4 +353,4 @@ func GetOrUrl(key string, defaultValue string) *url.URL { // If the variable is not set or parsing fails, it panics. func MustGetUrl(key string) *url.URL { return MustGetParsed(key, ParseURL, "url.URL") -} \ No newline at end of file +} diff --git a/util_test.go b/util_test.go index d6b6d8e..214f808 100644 --- a/util_test.go +++ b/util_test.go @@ -281,14 +281,14 @@ func TestEnvVarKeyValidation(t *testing.T) { assert.True(t, isValidEnvVarKey("_")) // Invalid keys - assert.False(t, isValidEnvVarKey("")) // empty - assert.False(t, isValidEnvVarKey("123VAR")) // starts with number - assert.False(t, isValidEnvVarKey("my-var")) // contains hyphen - assert.False(t, isValidEnvVarKey("my.var")) // contains dot - assert.False(t, isValidEnvVarKey("my var")) // contains space - assert.False(t, isValidEnvVarKey("myvar")) // lowercase (by convention) - assert.False(t, isValidEnvVarKey("My_Var")) // mixed case - assert.False(t, isValidEnvVarKey("VAR!")) // special character + assert.False(t, isValidEnvVarKey("")) // empty + assert.False(t, isValidEnvVarKey("123VAR")) // starts with number + assert.False(t, isValidEnvVarKey("my-var")) // contains hyphen + assert.False(t, isValidEnvVarKey("my.var")) // contains dot + assert.False(t, isValidEnvVarKey("my var")) // contains space + assert.False(t, isValidEnvVarKey("myvar")) // lowercase (by convention) + assert.False(t, isValidEnvVarKey("My_Var")) // mixed case + assert.False(t, isValidEnvVarKey("VAR!")) // special character } // Test constants are properly defined From 8762dca4666a898adb56b105c1a06f04d7a2cb59 Mon Sep 17 00:00:00 2001 From: Ray Johnson Date: Sat, 27 Sep 2025 00:16:44 +0000 Subject: [PATCH 07/12] missing space --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 9ae4f00..7ef1c9e 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,7 @@ LINT_OPTS ?= --fix .PHONEY: build build: ${SRCS} @go build + .PHONY: tests tests: ## Run test suite @go test -race ${PKGS} From cdf27fcd99e0d3fe08b7f169dbd9ba34287e695c Mon Sep 17 00:00:00 2001 From: Ray Johnson Date: Sat, 27 Sep 2025 00:20:53 +0000 Subject: [PATCH 08/12] update version to build with in workflow --- .github/workflows/buildandtest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/buildandtest.yml b/.github/workflows/buildandtest.yml index 619e894..3987eb6 100644 --- a/.github/workflows/buildandtest.yml +++ b/.github/workflows/buildandtest.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.17 + go-version: 1.18 - name: Build run: make build From b960214fae1cf988df3bb0b79d911e7c05118480 Mon Sep 17 00:00:00 2001 From: Ray Johnson Date: Sat, 27 Sep 2025 23:01:19 +0000 Subject: [PATCH 09/12] adjust when tag job runs --- .github/workflows/tag-on-merge.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tag-on-merge.yml b/.github/workflows/tag-on-merge.yml index fbd86ae..29d5314 100644 --- a/.github/workflows/tag-on-merge.yml +++ b/.github/workflows/tag-on-merge.yml @@ -4,6 +4,7 @@ on: push: branches: - main + pull_request: jobs: create-tag: From 4c18601ea07fa2103986ec96acf1ef2b3b99fe80 Mon Sep 17 00:00:00 2001 From: Ray Johnson Date: Sat, 27 Sep 2025 23:08:07 +0000 Subject: [PATCH 10/12] verbup --- Version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Version b/Version index 0bfccb0..ef52a64 100644 --- a/Version +++ b/Version @@ -1 +1 @@ -0.4.5 +0.4.6 From 1bd0db43b3582184c0248d3f7f4c96b58e67667b Mon Sep 17 00:00:00 2001 From: Ray Johnson Date: Sat, 27 Sep 2025 23:19:22 +0000 Subject: [PATCH 11/12] adjust tag logic --- .github/workflows/tag-on-merge.yml | 9 +++++++-- tag.sh | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tag-on-merge.yml b/.github/workflows/tag-on-merge.yml index 29d5314..da2693f 100644 --- a/.github/workflows/tag-on-merge.yml +++ b/.github/workflows/tag-on-merge.yml @@ -22,5 +22,10 @@ jobs: git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - - name: Run tag script - run: ./tag.sh + - name: Ensure version bump + run: | + ./tag.sh check_version $(find . -name "Version" -or -name "Versions") || (echo 'Version Bump Required' >> $FAIL_REASON;exit 1) && exit 0 + + - name: Tag this release + run: | + ./tag.sh $(find . -name "Version" -or -name "Versions" | cut -d/ -f2-) || (echo 'Failed to tag' >> $FAIL_REASON;exit 1) && exit 0 diff --git a/tag.sh b/tag.sh index 6cba75c..5d00249 100755 --- a/tag.sh +++ b/tag.sh @@ -56,7 +56,7 @@ fi DEFAULT_BRANCH=$( git remote show $( git config --get remote.origin.url ) | grep 'HEAD branch' | cut -d' ' -f5 ) echo "Default branch is: $DEFAULT_BRANCH" -if [ "${1-}" = "check_version" ]; then +if [ "$1" = "check_version" ]; then blue "\\nChecking version and verify if we need to update the version file." shift VERSION_FILES=$* From dc016196f0bbba6b83801269b75dbe273ea3395b Mon Sep 17 00:00:00 2001 From: Ray Johnson Date: Sat, 27 Sep 2025 23:30:37 +0000 Subject: [PATCH 12/12] moved tag.sh --- .github/workflows/tag-on-merge.yml | 4 ++-- tag.sh => .github/workflows/tag.sh | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename tag.sh => .github/workflows/tag.sh (100%) diff --git a/.github/workflows/tag-on-merge.yml b/.github/workflows/tag-on-merge.yml index da2693f..a18000b 100644 --- a/.github/workflows/tag-on-merge.yml +++ b/.github/workflows/tag-on-merge.yml @@ -24,8 +24,8 @@ jobs: - name: Ensure version bump run: | - ./tag.sh check_version $(find . -name "Version" -or -name "Versions") || (echo 'Version Bump Required' >> $FAIL_REASON;exit 1) && exit 0 + .github/workflows/tag.sh check_version $(find . -name "Version" -or -name "Versions") || (echo 'Version Bump Required' >> $FAIL_REASON;exit 1) && exit 0 - name: Tag this release run: | - ./tag.sh $(find . -name "Version" -or -name "Versions" | cut -d/ -f2-) || (echo 'Failed to tag' >> $FAIL_REASON;exit 1) && exit 0 + .github/workflows/tag.sh $(find . -name "Version" -or -name "Versions" | cut -d/ -f2-) || (echo 'Failed to tag' >> $FAIL_REASON;exit 1) && exit 0 diff --git a/tag.sh b/.github/workflows/tag.sh similarity index 100% rename from tag.sh rename to .github/workflows/tag.sh