auto(0129): agents_dashboard — secret_store_cpp_infra + CMakeLists register #4
@@ -0,0 +1,48 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
const secretMask = "***"
|
||||
|
||||
// ConfigDump converts a configuration struct to a map[string]string.
|
||||
// Exported fields are included. Fields tagged with secret:"true" have their
|
||||
// value replaced with "***". Nested structs are not traversed — only top-level
|
||||
// exported fields are included.
|
||||
//
|
||||
// The map key is the field name. Values are formatted with fmt.Sprintf("%v", ...).
|
||||
func ConfigDump(cfg any) map[string]string {
|
||||
result := make(map[string]string)
|
||||
|
||||
v := reflect.ValueOf(cfg)
|
||||
if v.Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
return result
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
if v.Kind() != reflect.Struct {
|
||||
return result
|
||||
}
|
||||
|
||||
t := v.Type()
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
key := field.Name
|
||||
|
||||
if field.Tag.Get("secret") == "true" {
|
||||
result[key] = secretMask
|
||||
continue
|
||||
}
|
||||
|
||||
fv := v.Field(i)
|
||||
result[key] = fmt.Sprintf("%v", fv.Interface())
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: config_dump
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "func ConfigDump(cfg any) map[string]string"
|
||||
description: "Convierte una struct de configuracion a map[string]string para logging o inspeccion. Campos con tag secret:\"true\" aparecen como \"***\". Solo campos exportados de nivel superior."
|
||||
tags: [config, dump, debug, logging, infra, reflect]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: [fmt, reflect]
|
||||
params:
|
||||
- name: cfg
|
||||
desc: "struct de configuracion (valor o puntero) cuyos campos exportados se van a serializar a strings"
|
||||
output: "mapa string→string con cada campo exportado; valores secretos reemplazados con ***"
|
||||
tested: true
|
||||
tests:
|
||||
- "campos normales se incluyen como string"
|
||||
- "campos secret aparecen como tres asteriscos"
|
||||
- "campos no exportados no aparecen"
|
||||
- "puntero a struct funciona"
|
||||
- "struct con bool y float se formatean correctamente"
|
||||
test_file_path: "functions/infra/config_dump_test.go"
|
||||
file_path: "functions/infra/config_dump.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
type AppConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
APIKey string `secret:"true"`
|
||||
}
|
||||
|
||||
cfg := AppConfig{Host: "localhost", Port: 8080, APIKey: "sk-abc123"}
|
||||
dump := ConfigDump(cfg)
|
||||
// dump = {"Host": "localhost", "Port": "8080", "APIKey": "***"}
|
||||
log.Printf("startup config: %v", dump)
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Funcion pura. Usa fmt.Sprintf("%v") para formatear todos los tipos. No desciende a structs anidados. Util para logging seguro de configuracion al inicio de una app.
|
||||
@@ -0,0 +1,67 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigDump(t *testing.T) {
|
||||
t.Run("campos normales se incluyen como string", func(t *testing.T) {
|
||||
type Cfg struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
got := ConfigDump(Cfg{Host: "localhost", Port: 8080})
|
||||
if got["Host"] != "localhost" {
|
||||
t.Errorf("expected Host=localhost, got %q", got["Host"])
|
||||
}
|
||||
if got["Port"] != "8080" {
|
||||
t.Errorf("expected Port=8080, got %q", got["Port"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("campos secret aparecen como tres asteriscos", func(t *testing.T) {
|
||||
type Cfg struct {
|
||||
APIKey string `secret:"true"`
|
||||
}
|
||||
got := ConfigDump(Cfg{APIKey: "super-secret-key"})
|
||||
if got["APIKey"] != "***" {
|
||||
t.Errorf("expected ***, got %q", got["APIKey"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("campos no exportados no aparecen", func(t *testing.T) {
|
||||
type Cfg struct {
|
||||
Public string
|
||||
private string //nolint:structcheck
|
||||
}
|
||||
_ = Cfg{} // ensure it compiles
|
||||
got := ConfigDump(Cfg{Public: "yes"})
|
||||
if _, ok := got["private"]; ok {
|
||||
t.Error("private field should not be in dump")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("puntero a struct funciona", func(t *testing.T) {
|
||||
type Cfg struct {
|
||||
Name string
|
||||
}
|
||||
got := ConfigDump(&Cfg{Name: "test"})
|
||||
if got["Name"] != "test" {
|
||||
t.Errorf("expected Name=test, got %q", got["Name"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("struct con bool y float se formatean correctamente", func(t *testing.T) {
|
||||
type Cfg struct {
|
||||
Debug bool
|
||||
Timeout float64
|
||||
}
|
||||
got := ConfigDump(Cfg{Debug: true, Timeout: 3.14})
|
||||
if got["Debug"] != "true" {
|
||||
t.Errorf("expected Debug=true, got %q", got["Debug"])
|
||||
}
|
||||
if got["Timeout"] != "3.14" {
|
||||
t.Errorf("expected Timeout=3.14, got %q", got["Timeout"])
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package infra
|
||||
|
||||
// ConfigError represents a single validation error for a configuration field.
|
||||
type ConfigError struct {
|
||||
Field string // struct field name or env var name
|
||||
Message string // human-readable error description
|
||||
Tag string // validation tag that triggered the error (e.g. "required", "format")
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package infra
|
||||
|
||||
// ConfigMerge merges two string maps, with override taking precedence over base.
|
||||
// Keys present in both maps take the value from override.
|
||||
// Keys present only in base are kept. Keys present only in override are added.
|
||||
// Neither input map is mutated — a new map is returned.
|
||||
func ConfigMerge(base, override map[string]string) map[string]string {
|
||||
result := make(map[string]string, len(base)+len(override))
|
||||
for k, v := range base {
|
||||
result[k] = v
|
||||
}
|
||||
for k, v := range override {
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
name: config_merge
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "func ConfigMerge(base, override map[string]string) map[string]string"
|
||||
description: "Merge de dos map[string]string. El map override tiene precedencia sobre base en claves comunes. Ninguno de los inputs es mutado — retorna un mapa nuevo."
|
||||
tags: [config, merge, map, infra]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
params:
|
||||
- name: base
|
||||
desc: "mapa de configuracion base, valores por defecto o de menor prioridad"
|
||||
- name: override
|
||||
desc: "mapa de configuracion con mayor prioridad, sobreescribe claves de base"
|
||||
output: "nuevo mapa con todas las claves de base y override, donde override gana en conflictos"
|
||||
tested: true
|
||||
tests:
|
||||
- "override gana sobre base en claves comunes"
|
||||
- "claves solo en base se mantienen"
|
||||
- "claves solo en override se agregan"
|
||||
- "no muta el map base"
|
||||
- "merge con maps vacios retorna mapa vacio"
|
||||
test_file_path: "functions/infra/config_merge_test.go"
|
||||
file_path: "functions/infra/config_merge.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
defaults := map[string]string{"host": "localhost", "port": "5432", "debug": "false"}
|
||||
env := map[string]string{"port": "9999", "debug": "true"}
|
||||
merged := ConfigMerge(defaults, env)
|
||||
// merged = {"host": "localhost", "port": "9999", "debug": "true"}
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Funcion pura. Util para combinar defaults hardcodeados con valores del archivo .env o de variables de entorno. Patron tipico: ConfigMerge(hardcoded_defaults, dotenv_values).
|
||||
@@ -0,0 +1,53 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigMerge(t *testing.T) {
|
||||
t.Run("override gana sobre base en claves comunes", func(t *testing.T) {
|
||||
base := map[string]string{"host": "localhost", "port": "5432"}
|
||||
over := map[string]string{"port": "9999"}
|
||||
got := ConfigMerge(base, over)
|
||||
if got["port"] != "9999" {
|
||||
t.Errorf("expected port=9999, got %q", got["port"])
|
||||
}
|
||||
if got["host"] != "localhost" {
|
||||
t.Errorf("expected host=localhost, got %q", got["host"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("claves solo en base se mantienen", func(t *testing.T) {
|
||||
base := map[string]string{"a": "1", "b": "2"}
|
||||
over := map[string]string{}
|
||||
got := ConfigMerge(base, over)
|
||||
if got["a"] != "1" || got["b"] != "2" {
|
||||
t.Errorf("unexpected result: %v", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("claves solo en override se agregan", func(t *testing.T) {
|
||||
base := map[string]string{}
|
||||
over := map[string]string{"x": "new"}
|
||||
got := ConfigMerge(base, over)
|
||||
if got["x"] != "new" {
|
||||
t.Errorf("expected x=new, got %q", got["x"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no muta el map base", func(t *testing.T) {
|
||||
base := map[string]string{"key": "original"}
|
||||
over := map[string]string{"key": "changed"}
|
||||
_ = ConfigMerge(base, over)
|
||||
if base["key"] != "original" {
|
||||
t.Errorf("base was mutated: got %q", base["key"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("merge con maps vacios retorna mapa vacio", func(t *testing.T) {
|
||||
got := ConfigMerge(map[string]string{}, map[string]string{})
|
||||
if len(got) != 0 {
|
||||
t.Errorf("expected empty map, got %v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// emailRE is a basic email format check.
|
||||
var emailRE = regexp.MustCompile(`^[^@\s]+@[^@\s]+\.[^@\s]+$`)
|
||||
|
||||
// urlRE checks for http:// or https:// prefix.
|
||||
var urlRE = regexp.MustCompile(`^https?://`)
|
||||
|
||||
// ConfigValidate validates a configuration struct populated with values.
|
||||
// It reads struct field tags to determine validation rules:
|
||||
// - required:"true" — field must be non-zero / non-empty
|
||||
// - format:"email" — field value must match email pattern
|
||||
// - format:"url" — field value must start with http:// or https://
|
||||
//
|
||||
// Only exported fields are inspected. Validation errors are accumulated; all
|
||||
// fields are checked even if earlier ones fail. Returns a ConfigValidation
|
||||
// with IsValid=true when no errors are found.
|
||||
func ConfigValidate(cfg any) ConfigValidation {
|
||||
var errs []ConfigError
|
||||
|
||||
v := reflect.ValueOf(cfg)
|
||||
if v.Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
return ConfigValidation{Errors: []ConfigError{{Field: "", Message: "cfg is nil", Tag: "required"}}, IsValid: false}
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
if v.Kind() != reflect.Struct {
|
||||
return ConfigValidation{Errors: []ConfigError{{Field: "", Message: "cfg must be a struct", Tag: "required"}}, IsValid: false}
|
||||
}
|
||||
|
||||
t := v.Type()
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
fv := v.Field(i)
|
||||
fieldName := field.Name
|
||||
|
||||
// required check
|
||||
required := field.Tag.Get("required")
|
||||
if required == "true" {
|
||||
if isZero(fv) {
|
||||
errs = append(errs, ConfigError{
|
||||
Field: fieldName,
|
||||
Message: fmt.Sprintf("field %s is required", fieldName),
|
||||
Tag: "required",
|
||||
})
|
||||
continue // skip format check when value is missing
|
||||
}
|
||||
}
|
||||
|
||||
// format check (only for string fields)
|
||||
format := field.Tag.Get("format")
|
||||
if format != "" && fv.Kind() == reflect.String {
|
||||
val := fv.String()
|
||||
switch format {
|
||||
case "email":
|
||||
if !emailRE.MatchString(val) {
|
||||
errs = append(errs, ConfigError{
|
||||
Field: fieldName,
|
||||
Message: fmt.Sprintf("field %s has invalid email format: %q", fieldName, val),
|
||||
Tag: "format",
|
||||
})
|
||||
}
|
||||
case "url":
|
||||
if !urlRE.MatchString(val) {
|
||||
errs = append(errs, ConfigError{
|
||||
Field: fieldName,
|
||||
Message: fmt.Sprintf("field %s has invalid URL format (must start with http:// or https://): %q", fieldName, val),
|
||||
Tag: "format",
|
||||
})
|
||||
}
|
||||
default:
|
||||
errs = append(errs, ConfigError{
|
||||
Field: fieldName,
|
||||
Message: fmt.Sprintf("field %s has unknown format tag: %q", fieldName, format),
|
||||
Tag: "format",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// min/max for numeric fields
|
||||
minTag := field.Tag.Get("min")
|
||||
maxTag := field.Tag.Get("max")
|
||||
if (minTag != "" || maxTag != "") && isNumeric(fv) {
|
||||
val := toFloat(fv)
|
||||
if minTag != "" {
|
||||
var minVal float64
|
||||
fmt.Sscanf(minTag, "%f", &minVal)
|
||||
if val < minVal {
|
||||
errs = append(errs, ConfigError{
|
||||
Field: fieldName,
|
||||
Message: fmt.Sprintf("field %s value %v is below minimum %v", fieldName, val, minVal),
|
||||
Tag: "min",
|
||||
})
|
||||
}
|
||||
}
|
||||
if maxTag != "" {
|
||||
var maxVal float64
|
||||
fmt.Sscanf(maxTag, "%f", &maxVal)
|
||||
if val > maxVal {
|
||||
errs = append(errs, ConfigError{
|
||||
Field: fieldName,
|
||||
Message: fmt.Sprintf("field %s value %v is above maximum %v", fieldName, val, maxVal),
|
||||
Tag: "max",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// oneof check for string fields
|
||||
oneof := field.Tag.Get("oneof")
|
||||
if oneof != "" && fv.Kind() == reflect.String {
|
||||
val := fv.String()
|
||||
allowed := strings.Split(oneof, " ")
|
||||
found := false
|
||||
for _, a := range allowed {
|
||||
if a == val {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
errs = append(errs, ConfigError{
|
||||
Field: fieldName,
|
||||
Message: fmt.Sprintf("field %s value %q is not one of: %s", fieldName, val, oneof),
|
||||
Tag: "oneof",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ConfigValidation{
|
||||
Errors: errs,
|
||||
IsValid: len(errs) == 0,
|
||||
}
|
||||
}
|
||||
|
||||
func isZero(v reflect.Value) bool {
|
||||
switch v.Kind() {
|
||||
case reflect.String:
|
||||
return v.Len() == 0
|
||||
case reflect.Slice, reflect.Map:
|
||||
return v.IsNil() || v.Len() == 0
|
||||
case reflect.Ptr, reflect.Interface:
|
||||
return v.IsNil()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return v.Int() == 0
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return v.Uint() == 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return v.Float() == 0
|
||||
case reflect.Bool:
|
||||
return !v.Bool()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isNumeric(v reflect.Value) bool {
|
||||
switch v.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
||||
reflect.Float32, reflect.Float64:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func toFloat(v reflect.Value) float64 {
|
||||
switch v.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return float64(v.Int())
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return float64(v.Uint())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return v.Float()
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: config_validate
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "func ConfigValidate(cfg any) ConfigValidation"
|
||||
description: "Valida una struct de configuracion usando struct tags. Acumula todos los errores (required, format:email, format:url, min, max, oneof) sin detener en el primero. Retorna ConfigValidation con IsValid=true si no hay errores."
|
||||
tags: [config, validation, env, infra, reflect]
|
||||
uses_functions: []
|
||||
uses_types: [config_validation_go_infra, config_error_go_infra]
|
||||
returns: [config_validation_go_infra]
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: [fmt, reflect, regexp, strings]
|
||||
params:
|
||||
- name: cfg
|
||||
desc: "struct de configuracion (valor o puntero) con tags required, format, min, max, oneof en sus campos exportados"
|
||||
output: "ConfigValidation con la lista de errores acumulados y el flag IsValid"
|
||||
tested: true
|
||||
tests:
|
||||
- "struct valido retorna IsValid true"
|
||||
- "campo required vacio acumula error"
|
||||
- "format email invalido produce error"
|
||||
- "format email valido pasa"
|
||||
- "format url invalido produce error"
|
||||
- "puntero a struct funciona igual"
|
||||
- "todos los errores acumulados aunque falle el primero"
|
||||
test_file_path: "functions/infra/config_validate_test.go"
|
||||
file_path: "functions/infra/config_validate.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
type ServerConfig struct {
|
||||
Host string `required:"true"`
|
||||
Port int `required:"true" min:"1" max:"65535"`
|
||||
Email string `format:"email"`
|
||||
}
|
||||
|
||||
v := ConfigValidate(ServerConfig{Host: "localhost", Port: 8080})
|
||||
if !v.IsValid {
|
||||
for _, e := range v.Errors {
|
||||
log.Printf("[%s] %s: %s", e.Tag, e.Field, e.Message)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Funcion pura — usa solo reflect, sin I/O. Soporta tags: `required:"true"`, `format:"email"`, `format:"url"`, `min:"N"`, `max:"N"`, `oneof:"a b c"`. Solo inspecciona campos exportados. No es recursiva — no baja a structs anidados.
|
||||
@@ -0,0 +1,98 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigValidate(t *testing.T) {
|
||||
t.Run("struct valido retorna IsValid true", func(t *testing.T) {
|
||||
type Cfg struct {
|
||||
Host string `required:"true"`
|
||||
Port int `required:"true"`
|
||||
}
|
||||
got := ConfigValidate(Cfg{Host: "localhost", Port: 8080})
|
||||
if !got.IsValid {
|
||||
t.Errorf("expected IsValid=true, errors: %v", got.Errors)
|
||||
}
|
||||
if len(got.Errors) != 0 {
|
||||
t.Errorf("expected no errors, got %v", got.Errors)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("campo required vacio acumula error", func(t *testing.T) {
|
||||
type Cfg struct {
|
||||
Host string `required:"true"`
|
||||
Key string `required:"true"`
|
||||
}
|
||||
got := ConfigValidate(Cfg{})
|
||||
if got.IsValid {
|
||||
t.Error("expected IsValid=false")
|
||||
}
|
||||
if len(got.Errors) != 2 {
|
||||
t.Errorf("expected 2 errors, got %d: %v", len(got.Errors), got.Errors)
|
||||
}
|
||||
for _, e := range got.Errors {
|
||||
if e.Tag != "required" {
|
||||
t.Errorf("expected tag 'required', got %q", e.Tag)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("format email invalido produce error", func(t *testing.T) {
|
||||
type Cfg struct {
|
||||
Email string `format:"email"`
|
||||
}
|
||||
got := ConfigValidate(Cfg{Email: "not-an-email"})
|
||||
if got.IsValid {
|
||||
t.Error("expected IsValid=false")
|
||||
}
|
||||
if len(got.Errors) == 0 || got.Errors[0].Tag != "format" {
|
||||
t.Errorf("expected format error, got %v", got.Errors)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("format email valido pasa", func(t *testing.T) {
|
||||
type Cfg struct {
|
||||
Email string `format:"email"`
|
||||
}
|
||||
got := ConfigValidate(Cfg{Email: "user@example.com"})
|
||||
if !got.IsValid {
|
||||
t.Errorf("expected IsValid=true, errors: %v", got.Errors)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("format url invalido produce error", func(t *testing.T) {
|
||||
type Cfg struct {
|
||||
BaseURL string `format:"url"`
|
||||
}
|
||||
got := ConfigValidate(Cfg{BaseURL: "ftp://bad.url"})
|
||||
if got.IsValid {
|
||||
t.Error("expected IsValid=false")
|
||||
}
|
||||
if len(got.Errors) == 0 || got.Errors[0].Tag != "format" {
|
||||
t.Errorf("expected format error, got %v", got.Errors)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("puntero a struct funciona igual", func(t *testing.T) {
|
||||
type Cfg struct {
|
||||
Name string `required:"true"`
|
||||
}
|
||||
got := ConfigValidate(&Cfg{Name: "test"})
|
||||
if !got.IsValid {
|
||||
t.Errorf("expected IsValid=true, errors: %v", got.Errors)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("todos los errores acumulados aunque falle el primero", func(t *testing.T) {
|
||||
type Cfg struct {
|
||||
A string `required:"true"`
|
||||
B string `required:"true"`
|
||||
C string `required:"true"`
|
||||
}
|
||||
got := ConfigValidate(Cfg{})
|
||||
if len(got.Errors) != 3 {
|
||||
t.Errorf("expected 3 errors, got %d", len(got.Errors))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package infra
|
||||
|
||||
// ConfigValidation holds the result of validating a configuration struct.
|
||||
type ConfigValidation struct {
|
||||
Errors []ConfigError // all validation errors found; empty if valid
|
||||
IsValid bool // true if Errors is empty
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: config_error
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
algebraic: product
|
||||
definition: |
|
||||
type ConfigError struct {
|
||||
Field string
|
||||
Message string
|
||||
Tag string
|
||||
}
|
||||
description: "Error de validacion de un campo de configuracion. Field identifica el campo, Message describe el problema, Tag indica la regla que fallo (required, format, etc.)."
|
||||
tags: [config, validation, error, infra]
|
||||
uses_types: []
|
||||
file_path: "functions/infra/config_error.go"
|
||||
---
|
||||
|
||||
## Notas
|
||||
|
||||
Tipo producto — todos los campos siempre presentes. Se acumula en ConfigValidation.Errors. Tag toma valores como "required" o "format" para que el consumidor pueda filtrar por tipo de error.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: config_validation
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
algebraic: product
|
||||
definition: |
|
||||
type ConfigValidation struct {
|
||||
Errors []ConfigError
|
||||
IsValid bool
|
||||
}
|
||||
description: "Resultado de validar una struct de configuracion. Contiene todos los errores acumulados y un bool de conveniencia. IsValid es true solo cuando Errors esta vacio."
|
||||
tags: [config, validation, result, infra]
|
||||
uses_types: [config_error_go_infra]
|
||||
file_path: "functions/infra/config_validation.go"
|
||||
---
|
||||
|
||||
## Notas
|
||||
|
||||
Tipo producto. IsValid se computa derivadamente de len(Errors)==0. Permite al consumidor tanto iterar sobre todos los errores como hacer un check rapido con IsValid.
|
||||
Reference in New Issue
Block a user