feat(infra): funciones impuras Go para carga de env/config (dotenv, env_require, config_from_env, config_from_file)
- dotenv_load: parser .env con no-sobreescritura y soporte de comillas - env_require: os.Getenv con error descriptivo fail-fast - env_require_all: verifica multiples vars y lista todas las faltantes - config_from_env: reflection sobre struct tags env/default/required/secret, 5 tipos soportados - config_from_file: JSON via stdlib, YAML stub con not-implemented - 25 tests unitarios, todos PASS Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DotenvLoad parses a .env file and sets environment variables via os.Setenv.
|
||||
// It does NOT overwrite variables that are already set in the environment.
|
||||
//
|
||||
// Parsing rules:
|
||||
// - Lines starting with # (after trimming) are ignored as comments.
|
||||
// - Blank lines are ignored.
|
||||
// - Lines must follow the KEY=VALUE format.
|
||||
// - Values may be optionally quoted with single or double quotes; quotes are stripped.
|
||||
// - Inline comments (# after value) are NOT stripped — the raw value after = is used.
|
||||
//
|
||||
// Returns an error if the file cannot be opened or if a line has no '=' separator.
|
||||
func DotenvLoad(path string) error {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dotenv_load: open %q: %w", path, err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
lineNum := 0
|
||||
for scanner.Scan() {
|
||||
lineNum++
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
// skip blank lines and comments
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
idx := strings.IndexByte(line, '=')
|
||||
if idx < 0 {
|
||||
return fmt.Errorf("dotenv_load: %q line %d: missing '=' in %q", path, lineNum, line)
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(line[:idx])
|
||||
val := line[idx+1:]
|
||||
|
||||
// strip surrounding quotes from value
|
||||
val = stripQuotes(val)
|
||||
|
||||
// only set if not already present
|
||||
if os.Getenv(key) == "" {
|
||||
if err := os.Setenv(key, val); err != nil {
|
||||
return fmt.Errorf("dotenv_load: setenv %q: %w", key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return fmt.Errorf("dotenv_load: scan %q: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// stripQuotes removes surrounding single or double quotes from a string.
|
||||
func stripQuotes(s string) string {
|
||||
if len(s) >= 2 {
|
||||
if (s[0] == '"' && s[len(s)-1] == '"') ||
|
||||
(s[0] == '\'' && s[len(s)-1] == '\'') {
|
||||
return s[1 : len(s)-1]
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user