Files
egutierrez ebf246beb0 feat(infra): funciones impuras Go para carga de env/config (dotenv, env_require, config_from_env, config_from_file)
- 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>
2026-04-13 02:01:50 +02:00

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
}