dispatch: allow empty route kinds (match all) + add routing tests

- config: permit routes[].kinds to be omitted/empty; treat as "all kinds"
- dispatch: compile empty kinds to Route{Kinds:nil} (match all kinds)
- tests: add coverage for route compilation + config validation edge cases

Files:
- config/load.go
- config/config.go
- dispatch/routes.go
- config/validate_test.go
- dispatch/routes_test.go
This commit is contained in:
2026-01-15 18:26:45 -06:00
parent a6c133319a
commit 1d43adcfa0
6 changed files with 143 additions and 22 deletions

View File

@@ -54,7 +54,10 @@ type RouteConfig struct {
Sink string `yaml:"sink"` // sink name
// Kinds is domain-defined. feedkit only enforces that each entry is non-empty.
// Whether a given daemon "recognizes" a kind is domain-specific validation.
//
// If Kinds is omitted or empty, the route matches ALL kinds.
// This is useful when you want explicit per-sink routing rules even when a
// particular sink should receive everything.
Kinds []string `yaml:"kinds"`
}

View File

@@ -133,16 +133,12 @@ func (c *Config) Validate() error {
m.Add(fieldErr(path+".sink", fmt.Sprintf("references unknown sink %q (define it under sinks:)", r.Sink)))
}
if len(r.Kinds) == 0 {
// You could relax this later (e.g. empty == "all kinds"), but for now
// keeping it strict prevents accidental "route does nothing".
m.Add(fieldErr(path+".kinds", "must contain at least one kind"))
} else {
for j, k := range r.Kinds {
kpath := fmt.Sprintf("%s.kinds[%d]", path, j)
if strings.TrimSpace(k) == "" {
m.Add(fieldErr(kpath, "kind cannot be empty"))
}
// Kinds is optional. If omitted or empty, the route matches ALL kinds.
// If provided, each entry must be non-empty.
for j, k := range r.Kinds {
kpath := fmt.Sprintf("%s.kinds[%d]", path, j)
if strings.TrimSpace(k) == "" {
m.Add(fieldErr(kpath, "kind cannot be empty"))
}
}
}

48
config/validate_test.go Normal file
View File

@@ -0,0 +1,48 @@
package config
import (
"strings"
"testing"
"time"
)
func TestValidate_RouteKindsEmptyIsAllowed(t *testing.T) {
cfg := &Config{
Sources: []SourceConfig{
{Name: "src1", Driver: "driver1", Every: Duration{Duration: time.Minute}},
},
Sinks: []SinkConfig{
{Name: "sink1", Driver: "stdout"},
},
Routes: []RouteConfig{
{Sink: "sink1", Kinds: nil}, // omitted
{Sink: "sink1", Kinds: []string{}}, // explicit empty
},
}
if err := cfg.Validate(); err != nil {
t.Fatalf("expected no error, got: %v", err)
}
}
func TestValidate_RouteKindsRejectsBlankEntries(t *testing.T) {
cfg := &Config{
Sources: []SourceConfig{
{Name: "src1", Driver: "driver1", Every: Duration{Duration: time.Minute}},
},
Sinks: []SinkConfig{
{Name: "sink1", Driver: "stdout"},
},
Routes: []RouteConfig{
{Sink: "sink1", Kinds: []string{"observation", " ", "alert"}},
},
}
err := cfg.Validate()
if err == nil {
t.Fatalf("expected error, got nil")
}
if !strings.Contains(err.Error(), "routes[0].kinds[1]") {
t.Fatalf("expected error to mention blank kind entry, got: %v", err)
}
}