- add new `processors` package with canonical `Processor` interface - add `processors.Registry` with Register/Build/BuildChain factory model - switch `pipeline.Pipeline` to `[]processors.Processor` - replace `normalize.Registry` + registry adapter with direct `normalize.Processor` - remove `normalize/registry.go` - update root docs to position normalize as one optional processing stage - add tests for processors registry, normalize processor behavior, and pipeline flow BREAKING CHANGE: - `pipeline.Processor` removed; use `processors.Processor` - `normalize.Registry` and old normalize processor adapter APIs removed - downstream daemons must update processor wiring to new `processors.Registry` and `normalize.NewProcessor(...)`
140 lines
3.3 KiB
Go
140 lines
3.3 KiB
Go
package normalize
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/ejr/feedkit/event"
|
|
)
|
|
|
|
func TestProcessorFirstMatchWins(t *testing.T) {
|
|
var firstCalls, secondCalls int
|
|
|
|
p := NewProcessor([]Normalizer{
|
|
Func{
|
|
MatchFn: func(event.Event) bool { return true },
|
|
NormalizeFn: func(_ context.Context, in event.Event) (*event.Event, error) {
|
|
firstCalls++
|
|
out := in
|
|
out.Schema = "normalized.first.v1"
|
|
return &out, nil
|
|
},
|
|
},
|
|
Func{
|
|
MatchFn: func(event.Event) bool { return true },
|
|
NormalizeFn: func(_ context.Context, in event.Event) (*event.Event, error) {
|
|
secondCalls++
|
|
out := in
|
|
out.Schema = "normalized.second.v1"
|
|
return &out, nil
|
|
},
|
|
},
|
|
}, false)
|
|
|
|
out, err := p.Process(context.Background(), testEvent())
|
|
if err != nil {
|
|
t.Fatalf("Process error: %v", err)
|
|
}
|
|
if out == nil {
|
|
t.Fatalf("expected output event, got nil")
|
|
}
|
|
if out.Schema != "normalized.first.v1" {
|
|
t.Fatalf("unexpected schema: %q", out.Schema)
|
|
}
|
|
if firstCalls != 1 {
|
|
t.Fatalf("expected first normalizer called once, got %d", firstCalls)
|
|
}
|
|
if secondCalls != 0 {
|
|
t.Fatalf("expected second normalizer skipped, got %d calls", secondCalls)
|
|
}
|
|
}
|
|
|
|
func TestProcessorNoMatchPassThroughAndRequireMatch(t *testing.T) {
|
|
in := testEvent()
|
|
in.Schema = "raw.schema.v1"
|
|
|
|
passThrough := NewProcessor([]Normalizer{
|
|
Func{
|
|
MatchFn: func(event.Event) bool { return false },
|
|
NormalizeFn: func(_ context.Context, in event.Event) (*event.Event, error) {
|
|
out := in
|
|
out.Schema = "should.not.run"
|
|
return &out, nil
|
|
},
|
|
},
|
|
}, false)
|
|
|
|
out, err := passThrough.Process(context.Background(), in)
|
|
if err != nil {
|
|
t.Fatalf("pass-through Process error: %v", err)
|
|
}
|
|
if out == nil {
|
|
t.Fatalf("expected pass-through output event, got nil")
|
|
}
|
|
if out.Schema != "raw.schema.v1" {
|
|
t.Fatalf("expected unchanged schema, got %q", out.Schema)
|
|
}
|
|
|
|
required := NewProcessor(nil, true)
|
|
_, err = required.Process(context.Background(), in)
|
|
if err == nil {
|
|
t.Fatalf("expected require-match error")
|
|
}
|
|
if !strings.Contains(err.Error(), "no normalizer matched") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestProcessorDropAndErrorPropagation(t *testing.T) {
|
|
t.Run("drop", func(t *testing.T) {
|
|
p := NewProcessor([]Normalizer{
|
|
Func{
|
|
MatchFn: func(event.Event) bool { return true },
|
|
NormalizeFn: func(context.Context, event.Event) (*event.Event, error) {
|
|
return nil, nil
|
|
},
|
|
},
|
|
}, false)
|
|
|
|
out, err := p.Process(context.Background(), testEvent())
|
|
if err != nil {
|
|
t.Fatalf("Process error: %v", err)
|
|
}
|
|
if out != nil {
|
|
t.Fatalf("expected nil output for dropped event, got %#v", out)
|
|
}
|
|
})
|
|
|
|
t.Run("error", func(t *testing.T) {
|
|
p := NewProcessor([]Normalizer{
|
|
Func{
|
|
MatchFn: func(event.Event) bool { return true },
|
|
NormalizeFn: func(context.Context, event.Event) (*event.Event, error) {
|
|
return nil, errors.New("map failed")
|
|
},
|
|
},
|
|
}, false)
|
|
|
|
_, err := p.Process(context.Background(), testEvent())
|
|
if err == nil {
|
|
t.Fatalf("expected error")
|
|
}
|
|
if !strings.Contains(err.Error(), "normalizer failed") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func testEvent() event.Event {
|
|
return event.Event{
|
|
ID: "evt-normalize-1",
|
|
Kind: event.Kind("observation"),
|
|
Source: "source-1",
|
|
EmittedAt: time.Now().UTC(),
|
|
Payload: map[string]any{"x": 1},
|
|
}
|
|
}
|