diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index a2bc30a..c17a918 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -14,15 +14,15 @@ jobs: test: strategy: matrix: - go-version: [1.22.x,1.21.x,1.20.x,1.19.x,1.18.x,1.17.x] + go-version: [1.25.x,1.24.x,1.23.x,1.22.x,1.21.x,1.20.x,1.19.x,1.18.x,1.17.x] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: ${{ matrix.go-version }} @@ -33,8 +33,8 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 with: go-version: stable - name: golangci-lint diff --git a/os/env.go b/os/env.go new file mode 100644 index 0000000..9e07fae --- /dev/null +++ b/os/env.go @@ -0,0 +1,79 @@ +//go:build go1.22 +// +build go1.22 + +package osext + +import ( + "os" + "reflect" + "strconv" + + constraintsext "github.com/go-playground/pkg/v5/constraints" + . "github.com/go-playground/pkg/v5/values/option" + . "github.com/go-playground/pkg/v5/values/result" +) + +// EnvDefaults interface defines the supported types that can be used for environment variables conversions. +type EnvDefaults interface { + constraintsext.Number | ~string +} + +// Env retrieves the value of the environment variable named by the key. +// +// If the variable is not set, or if the conversion fails due to incorrect value, +// a default value is returned. +func Env[T EnvDefaults](key string, defaultValue T) T { + return GetEnv[T](key).UnwrapOr(defaultValue) +} + +// GetEnv retrieves the value of the environment variable named by the key. +// +// If the variable is not set, or if the conversion fails it returns `None`, otherwise `Some`. +func GetEnv[T EnvDefaults](key string) Option[T] { + r := LookupEnv[T](key) + if r.IsErr() { + return None[T]() + } + return r.Unwrap() +} + +// LookupEnv retrieves the value of the environment variable named by the key. +// +// If the variable is not present it returns Ok(None) +// If the variable is present and conversion is successful, the value Ok(Some) is returned +// If the variable is present and conversion fails it returns Err(error) +func LookupEnv[T EnvDefaults](key string) Result[Option[T], error] { + if v, ok := os.LookupEnv(key); ok { + ty := reflect.TypeFor[T]() + elem := reflect.New(ty).Elem() + + switch ty.Kind() { + case reflect.String: + elem.SetString(v) + return Ok[Option[T], error](Some(elem.Interface().(T))) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + i, err := strconv.ParseInt(v, 10, ty.Bits()) + if err != nil { + return Err[Option[T], error](err) + } + elem.SetInt(i) + return Ok[Option[T], error](Some(elem.Interface().(T))) + + case reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + i, err := strconv.ParseUint(v, 10, ty.Bits()) + if err != nil { + return Err[Option[T], error](err) + } + elem.SetUint(i) + return Ok[Option[T], error](Some(elem.Interface().(T))) + case reflect.Float32, reflect.Float64: + f, err := strconv.ParseFloat(v, ty.Bits()) + if err != nil { + return Err[Option[T], error](err) + } + elem.SetFloat(f) + return Ok[Option[T], error](Some(elem.Interface().(T))) + } + } + return Ok[Option[T], error](None[T]()) +} diff --git a/os/env_test.go b/os/env_test.go new file mode 100644 index 0000000..07a6685 --- /dev/null +++ b/os/env_test.go @@ -0,0 +1,98 @@ +//go:build go1.22 +// +build go1.22 + +package osext + +import ( + "fmt" + "math/rand" + "testing" +) + +type ( + CustomInt int + CustomInt8 int8 + CustomInt16 int16 + CustomInt32 int32 + CustomInt64 int64 + CustomUint uint + CustomUint8 uint8 + CustomUint16 uint16 + CustomUint32 uint32 + CustomUint64 uint64 + CustomUintptr uintptr + CustomFloat32 float32 + CustomFloat64 float64 + CustomString string +) + +func TestEnv(t *testing.T) { + tests := []struct { + name string + testFunc func() error + }{ + // Integer types + {"int", func() error { return SetAndUnsetTest(t, "24", 24, 42) }}, + {"CustomInt", func() error { return SetAndUnsetTest(t, "24", CustomInt(24), CustomInt(42)) }}, + {"int8", func() error { return SetAndUnsetTest(t, "24", int8(24), int8(42)) }}, + {"CustomInt8", func() error { return SetAndUnsetTest(t, "24", CustomInt8(24), CustomInt8(42)) }}, + {"int16", func() error { return SetAndUnsetTest(t, "24", int16(24), int16(42)) }}, + {"CustomInt16", func() error { return SetAndUnsetTest(t, "24", CustomInt16(24), CustomInt16(42)) }}, + {"int32", func() error { return SetAndUnsetTest(t, "24", int32(24), int32(42)) }}, + {"CustomInt32", func() error { return SetAndUnsetTest(t, "24", CustomInt32(24), CustomInt32(42)) }}, + {"int64", func() error { return SetAndUnsetTest(t, "24", int64(24), int64(42)) }}, + {"CustomInt64", func() error { return SetAndUnsetTest(t, "24", CustomInt64(24), CustomInt64(42)) }}, + {"uint", func() error { return SetAndUnsetTest(t, "24", uint(24), uint(42)) }}, + {"CustomUint", func() error { return SetAndUnsetTest(t, "24", CustomUint(24), CustomUint(42)) }}, + {"uint8", func() error { return SetAndUnsetTest(t, "24", uint8(24), uint8(42)) }}, + {"CustomUint8", func() error { return SetAndUnsetTest(t, "24", CustomUint8(24), CustomUint8(42)) }}, + {"uint16", func() error { return SetAndUnsetTest(t, "24", uint16(24), uint16(42)) }}, + {"CustomUint16", func() error { return SetAndUnsetTest(t, "24", CustomUint16(24), CustomUint16(42)) }}, + {"uint32", func() error { return SetAndUnsetTest(t, "24", uint32(24), uint32(42)) }}, + {"CustomUint32", func() error { return SetAndUnsetTest(t, "24", CustomUint32(24), CustomUint32(42)) }}, + {"uint64", func() error { return SetAndUnsetTest(t, "24", uint64(24), uint64(42)) }}, + {"CustomUint64", func() error { return SetAndUnsetTest(t, "24", CustomUint64(24), CustomUint64(42)) }}, + {"uintptr", func() error { return SetAndUnsetTest(t, "24", uintptr(24), uintptr(42)) }}, + {"CustomUintptr", func() error { return SetAndUnsetTest(t, "24", CustomUintptr(24), CustomUintptr(42)) }}, + + // Float types + {"float32", func() error { return SetAndUnsetTest(t, "24.5", float32(24.5), float32(42.5)) }}, + {"CustomFloat32", func() error { return SetAndUnsetTest(t, "24.5", CustomFloat32(24.5), CustomFloat32(42.5)) }}, + {"float64", func() error { return SetAndUnsetTest(t, "24.5", float64(24.5), float64(42.5)) }}, + {"CustomFloat64", func() error { return SetAndUnsetTest(t, "24.5", CustomFloat64(24.5), CustomFloat64(42.5)) }}, + + // String types + {"string", func() error { return SetAndUnsetTest(t, "hello", "hello", "world") }}, + {"CustomString", func() error { return SetAndUnsetTest(t, "hello", CustomString("hello"), CustomString("world")) }}, + + // Conversion failure test cases - these should return default values when conversion fails + {"int_conversion_failure", func() error { return SetAndUnsetTest(t, "not_a_number", 42, 42) }}, + {"float32_conversion_failure", func() error { return SetAndUnsetTest(t, "not_a_float", float32(42.5), float32(42.5)) }}, + {"float64_conversion_failure", func() error { return SetAndUnsetTest(t, "not_a_float", float64(42.5), float64(42.5)) }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.testFunc(); err != nil { + t.Errorf("Test %s failed: %v", tt.name, err) + } + }) + } +} + +func SetAndUnsetTest[T EnvDefaults](t *testing.T, envValueSet string, expectedSet T, expectedDefault T) error { + constTestEnvKey := fmt.Sprintf("TEST_ENV_%d", rand.Intn(1000000)) + + v := Env(constTestEnvKey, expectedDefault) + if v != expectedDefault { + return fmt.Errorf("default value mismatch: got %v, want %v", v, expectedDefault) + } + + t.Setenv(constTestEnvKey, envValueSet) + + v = Env(constTestEnvKey, expectedDefault) + if v != expectedSet { + return fmt.Errorf("set value mismatch: got %v, want %v", v, expectedSet) + } + return nil +}