From 7670b671f2a6357b6028c8f900300161ebea3126 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Mon, 13 Apr 2026 02:02:28 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20cerrar=20issue=200018=20=E2=80=94=20Con?= =?UTF-8?q?fig=20&=20Env=20Management=20implementado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- dev/issues/completed/0018-config-env.md | 205 ++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 dev/issues/completed/0018-config-env.md diff --git a/dev/issues/completed/0018-config-env.md b/dev/issues/completed/0018-config-env.md new file mode 100644 index 00000000..7fecdbd8 --- /dev/null +++ b/dev/issues/completed/0018-config-env.md @@ -0,0 +1,205 @@ +# 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.