Files
fn_registry/dev/issues/completed/0018-config-env.md
T
2026-04-13 02:02:28 +02:00

8.8 KiB

0018 — Config & Env Management

Metadata

Campo Valor
ID 0018
Estado pendiente
Prioridad media
Tipo feature

Dependencias

Ninguna.


Objetivo

Funciones reutilizables para cargar, validar y gestionar configuracion de apps usando struct tags en Go y dataclasses en Python. Reemplazar los os.Getenv dispersos por un patron consistente con validacion, defaults y soporte para .env.

Contexto

  • Las apps cargan config ad-hoc: os.Getenv repetidos, sin validacion, sin defaults, sin mensajes de error descriptivos.
  • Cuando falta una variable de entorno, el error aparece mucho despues (conexion fallida, panic) en vez de al arrancar.
  • No hay soporte para archivos .env — cada desarrollador tiene que exportar variables manualmente.
  • Patron deseado: definir un struct con tags, poblarlo automaticamente, validar al arrancar, fallar rapido con errores claros.

Funciones Go (dominio infra)

Funcion Purity Firma (simplificada)
config_from_env impure (dst any) error
config_from_file impure (path string, dst any) error
config_validate pure (cfg any) (ConfigValidation, error)
config_merge pure (base, override map[string]string) map[string]string
config_dump pure (cfg any) map[string]string
env_require impure (key string) (string, error)
env_require_all impure (keys []string) (map[string]string, error)
dotenv_load impure (path string) error

Struct tags

type AppConfig struct {
    Port     int    `env:"PORT"     default:"8080" required:"true"`
    DBPath   string `env:"DB_PATH"  default:"data.db"`
    APIKey   string `env:"API_KEY"  required:"true" secret:"true"`
    LogLevel string `env:"LOG_LEVEL" default:"info"`
}
  • env:"VAR" — nombre de la variable de entorno
  • default:"valor" — valor si la variable no existe
  • required:"true" — error si no tiene valor (ni env ni default)
  • secret:"true"config_dump redacta el valor como ***

Detalle por funcion

config_from_env — Recorre campos del struct via reflection. Para cada campo lee os.Getenv(tag_env), aplica default si vacio, convierte al tipo del campo (int, bool, string, float64, []string). Error si un campo required queda vacio.

config_from_file — Detecta formato por extension (.json, .yaml). Deserializa al struct destino. Solo JSON usa stdlib; YAML solo si ya existe dependencia en el proyecto (sino stub con error).

config_validate — Valida un struct ya poblado: campos required no vacios, rangos numericos (futuro: tag min/max), formatos basicos (URL, email via regex simple). Retorna ConfigValidation con todos los errores acumulados.

config_merge — Merge de dos maps string→string donde override gana. Util para: defaults → file → env (cada capa overridea la anterior).

config_dump — Convierte struct a map[string]string para logging. Campos con secret:"true" se muestran como ***. Util para imprimir config al arrancar sin leakear secrets.

env_require / env_require_all — Helpers ligeros para cuando no se necesita un struct completo. env_require_all acumula todos los errores en vez de fallar en el primero.

dotenv_load — Parsea un archivo .env (lineas KEY=VALUE, ignora comentarios # y lineas vacias) y hace os.Setenv para cada entrada. No sobreescribe variables que ya existen en el entorno.

Funciones Python (dominio infra)

Funcion Purity Firma (simplificada)
config_from_env impure (cls: type[T]) -> T
dotenv_load impure (path: str) -> None

config_from_env_py_infra — Recibe una dataclass con field metadata y la puebla desde os.environ. Soporta defaults de la dataclass y type hints para conversion (int, float, bool, str).

@dataclass
class AppConfig:
    port: int = field(default=8080, metadata={"env": "PORT"})
    db_path: str = field(default="data.db", metadata={"env": "DB_PATH"})
    api_key: str = field(default="", metadata={"env": "API_KEY", "required": True})

dotenv_load_py_infra — Mismo comportamiento que la version Go: parsea .env, setea en os.environ, no sobreescribe existentes.

Tipos (dominio infra)

Tipo Algebraic Campos
ConfigError product field string, message string, tag string
ConfigValidation product errors []ConfigError, is_valid bool

ConfigError.tag indica que validacion fallo (required, format, range). Permite que el caller filtre o agrupe errores programaticamente.

Tareas

Fase 1: Tipos y funciones puras

  • 1.1 Crear tipos ConfigError, ConfigValidation en functions/infra/ + .md en types/infra/
  • 1.2 config_validate — validacion de struct con acumulacion de errores
  • 1.3 config_merge — merge de maps con prioridad
  • 1.4 config_dump — dump a map con redaccion de secrets
  • 1.5 Tests unitarios para las tres funciones puras

Fase 2: Funciones impuras Go

  • 2.1 dotenv_load — parser de .env + os.Setenv
  • 2.2 env_require y env_require_all — getenv con errores descriptivos
  • 2.3 config_from_env — reflection sobre struct tags + conversion de tipos
  • 2.4 config_from_file — JSON (stdlib), YAML (stub si no hay dep)
  • 2.5 Tests con t.Setenv para aislar variables de entorno

Fase 3: Funciones Python + indexado

  • 3.1 config_from_env_py_infra — dataclass + os.environ
  • 3.2 dotenv_load_py_infra — parser .env
  • 3.3 fn index y verificar todas las funciones en registry.db
  • 3.4 go vet -tags fts5 limpio

Ejemplo de uso

// 1. Definir config como struct con tags
type ServerConfig struct {
    Port     int    `env:"PORT"      default:"8080" required:"true"`
    DBPath   string `env:"DB_PATH"   default:"registry.db"`
    APIKey   string `env:"API_KEY"   required:"true" secret:"true"`
    LogLevel string `env:"LOG_LEVEL" default:"info"`
}

// 2. Cargar .env si existe, poblar struct, validar
func main() {
    _ = infra.DotenvLoad(".env")

    var cfg ServerConfig
    if err := infra.ConfigFromEnv(&cfg); err != nil {
        log.Fatalf("config: %v", err)
    }

    validation, _ := infra.ConfigValidate(&cfg)
    if !validation.IsValid {
        for _, e := range validation.Errors {
            log.Printf("  %s: %s (%s)", e.Field, e.Message, e.Tag)
        }
        os.Exit(1)
    }

    // 3. Log config al arrancar (secrets redactados)
    for k, v := range infra.ConfigDump(&cfg) {
        log.Printf("  %s=%s", k, v)
    }
    // Output: PORT=8080  DB_PATH=registry.db  API_KEY=***  LOG_LEVEL=info
}
// Caso simple: solo necesito dos variables
dbURL, err := infra.EnvRequire("DATABASE_URL")
if err != nil {
    log.Fatal(err) // "DATABASE_URL: variable de entorno requerida no definida"
}

vars, err := infra.EnvRequireAll([]string{"DB_HOST", "DB_PORT", "DB_NAME"})
if err != nil {
    log.Fatal(err) // "variables no definidas: DB_HOST, DB_NAME"
}
# Python
from infra import config_from_env, dotenv_load

@dataclass
class Config:
    port: int = field(default=8080, metadata={"env": "PORT"})
    api_key: str = field(default="", metadata={"env": "API_KEY", "required": True})

dotenv_load(".env")
cfg = config_from_env(Config)
print(f"Listening on :{cfg.port}")

Decisiones de diseno

  • Solo stdlib para Go: reflection + os + encoding/json. Sin viper, envconfig, ni dependencias externas. YAML queda como stub hasta que el proyecto lo necesite.
  • Struct tags como fuente de verdad: la definicion del struct ES la documentacion de la config. Un nuevo campo = una linea de codigo.
  • Fail-fast con errores acumulados: config_validate reporta TODOS los problemas, no solo el primero. Asi el usuario los arregla todos de una vez.
  • dotenv_load no sobreescribe: las variables ya seteadas en el entorno tienen prioridad. Esto permite overrides en CI/deploy sin tocar el .env.
  • secret tag para redaccion: evita leakear API keys en logs. config_dump es seguro para imprimir al arrancar.
  • Python usa dataclasses nativo: sin pydantic ni attrs. Consistente con el enfoque zero-deps del registry.

Riesgos

  • Reflection en Go es fragil: tipos no soportados (nested structs, maps) darian panics. Mitigado limitando v1 a tipos planos (string, int, bool, float64, []string) y documentando la restriccion.
  • Parser de .env demasiado simple: no soportaria valores multilinea, comillas con escape, ni interpolacion. Suficiente para el 95% de casos; documentar limitaciones.
  • Duplicacion Go/Python: dos implementaciones del mismo concepto. Mitigado porque son simples y el comportamiento esta bien definido en esta spec.