fix(fn-run): propagar stdout/stderr de bash functions library-style #1

Open
dataforge wants to merge 537 commits from auto/0077-fn-run-bash-mudo into master
Showing only changes of commit 7670b671f2 - Show all commits
+205
View File
@@ -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.