ebf246beb0
- dotenv_load: parser .env con no-sobreescritura y soporte de comillas - env_require: os.Getenv con error descriptivo fail-fast - env_require_all: verifica multiples vars y lista todas las faltantes - config_from_env: reflection sobre struct tags env/default/required/secret, 5 tipos soportados - config_from_file: JSON via stdlib, YAML stub con not-implemented - 25 tests unitarios, todos PASS Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
134 lines
3.6 KiB
Go
134 lines
3.6 KiB
Go
package infra
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// ConfigFromEnv populates a configuration struct from environment variables
|
|
// using struct field tags. The target must be a non-nil pointer to a struct.
|
|
//
|
|
// Supported tags:
|
|
// - env:"VAR_NAME" — environment variable to read (required tag)
|
|
// - default:"value" — value used when env var is unset/empty
|
|
// - required:"true" — error if env var is unset/empty and no default
|
|
// - secret:"true" — documented only; no runtime effect on loading
|
|
//
|
|
// Supported field types: string, int, int64, bool, float64, []string.
|
|
// []string fields are parsed from comma-separated values.
|
|
//
|
|
// Example struct:
|
|
//
|
|
// type Config struct {
|
|
// Port int `env:"PORT" default:"8080" required:"true"`
|
|
// APIKey string `env:"API_KEY" required:"true" secret:"true"`
|
|
// Tags []string `env:"TAGS" default:"a,b"`
|
|
// }
|
|
func ConfigFromEnv(target any) error {
|
|
v := reflect.ValueOf(target)
|
|
if v.Kind() != reflect.Ptr || v.IsNil() {
|
|
return fmt.Errorf("config_from_env: target must be a non-nil pointer to a struct")
|
|
}
|
|
v = v.Elem()
|
|
if v.Kind() != reflect.Struct {
|
|
return fmt.Errorf("config_from_env: target must point to a struct, got %s", v.Kind())
|
|
}
|
|
|
|
t := v.Type()
|
|
var errs []string
|
|
|
|
for i := 0; i < t.NumField(); i++ {
|
|
field := t.Field(i)
|
|
if !field.IsExported() {
|
|
continue
|
|
}
|
|
|
|
envKey := field.Tag.Get("env")
|
|
if envKey == "" {
|
|
continue // no env tag → skip
|
|
}
|
|
|
|
defaultVal := field.Tag.Get("default")
|
|
required := field.Tag.Get("required") == "true"
|
|
|
|
raw := os.Getenv(envKey)
|
|
if raw == "" {
|
|
if defaultVal != "" {
|
|
raw = defaultVal
|
|
} else if required {
|
|
errs = append(errs, fmt.Sprintf("field %s: env var %q is required but not set", field.Name, envKey))
|
|
continue
|
|
} else {
|
|
continue // optional and unset — leave field at zero value
|
|
}
|
|
}
|
|
|
|
fv := v.Field(i)
|
|
if err := setField(fv, field.Name, raw); err != nil {
|
|
errs = append(errs, err.Error())
|
|
}
|
|
}
|
|
|
|
if len(errs) > 0 {
|
|
return fmt.Errorf("config_from_env: %s", strings.Join(errs, "; "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// setField converts the string raw to the appropriate type and sets fv.
|
|
func setField(fv reflect.Value, fieldName, raw string) error {
|
|
switch fv.Kind() {
|
|
case reflect.String:
|
|
fv.SetString(raw)
|
|
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
n, err := strconv.ParseInt(raw, 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("field %s: cannot parse %q as int: %w", fieldName, raw, err)
|
|
}
|
|
fv.SetInt(n)
|
|
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
n, err := strconv.ParseUint(raw, 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("field %s: cannot parse %q as uint: %w", fieldName, raw, err)
|
|
}
|
|
fv.SetUint(n)
|
|
|
|
case reflect.Float32, reflect.Float64:
|
|
f, err := strconv.ParseFloat(raw, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("field %s: cannot parse %q as float: %w", fieldName, raw, err)
|
|
}
|
|
fv.SetFloat(f)
|
|
|
|
case reflect.Bool:
|
|
b, err := strconv.ParseBool(raw)
|
|
if err != nil {
|
|
return fmt.Errorf("field %s: cannot parse %q as bool (use true/false/1/0): %w", fieldName, raw, err)
|
|
}
|
|
fv.SetBool(b)
|
|
|
|
case reflect.Slice:
|
|
if fv.Type().Elem().Kind() != reflect.String {
|
|
return fmt.Errorf("field %s: slice of non-string is not supported, use []string", fieldName)
|
|
}
|
|
parts := strings.Split(raw, ",")
|
|
trimmed := make([]string, 0, len(parts))
|
|
for _, p := range parts {
|
|
t := strings.TrimSpace(p)
|
|
if t != "" {
|
|
trimmed = append(trimmed, t)
|
|
}
|
|
}
|
|
fv.Set(reflect.ValueOf(trimmed))
|
|
|
|
default:
|
|
return fmt.Errorf("field %s: unsupported type %s", fieldName, fv.Kind())
|
|
}
|
|
return nil
|
|
}
|