Files

9.0 KiB

id, title, status, type, domain, scope, priority, depends, blocks, related, created, updated, tags
id title status type domain scope priority depends blocks related created updated tags
0018 Config & Env Management completado feature
multi-app media
2026-05-17 2026-05-17

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.