--- id: "0018" title: "Config & Env Management" status: completado type: feature domain: [] scope: multi-app priority: media depends: [] blocks: [] related: [] created: 2026-05-17 updated: 2026-05-17 tags: [] --- # 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 ```go 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`). ```python @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 ```go // 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 } ``` ```go // 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 # 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.