---
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.
