docs: cerrar issue 0018 — Config & Env Management implementado
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user