Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
2 changes: 1 addition & 1 deletion .github/workflows/buildandtest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ LINT_OPTS ?= --fix
.PHONEY: build
build: ${SRCS}
@go build

.PHONY: tests
tests: ## Run test suite
@go test -race ${PKGS}
Expand Down
74 changes: 74 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
@@ -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
123 changes: 95 additions & 28 deletions env.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -35,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 {
Expand All @@ -75,7 +142,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)
Expand All @@ -89,29 +156,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) {
Expand Down Expand Up @@ -185,25 +252,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
}
Expand All @@ -216,14 +283,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
}
Expand Down Expand Up @@ -328,7 +395,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
}
Expand All @@ -341,7 +408,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
}
Expand All @@ -351,10 +418,10 @@ 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)
uintValue, err := strconv.ParseUint(v, DecimalBase, Int64Bits)
if err != nil {
return nil, err
}
Expand All @@ -367,7 +434,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
}
Expand All @@ -380,7 +447,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
}
Expand Down
Loading