Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}

Expand All @@ -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
Expand Down
79 changes: 79 additions & 0 deletions os/env.go
Original file line number Diff line number Diff line change
@@ -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]())
}
98 changes: 98 additions & 0 deletions os/env_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
Comment on lines +94 to +96
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use assert.Equal like other test files?

return nil
}
Loading