Files
feedkit/event/event.go
Eric Rakestraw 0cc2862170 feedkit: split the former maximumdirect.net/weatherd project in two.
feedkit now contains a reusable core, while weatherfeeder is a concrete implementation that includes weather-specific functions.
2026-01-13 10:40:01 -06:00

102 lines
2.9 KiB
Go

package event
import (
"errors"
"strings"
"time"
)
// ErrInvalidEvent is a sentinel error so callers can do:
//
// if errors.Is(err, event.ErrInvalidEvent) { ... }
var ErrInvalidEvent = errors.New("invalid event")
// ValidationError reports one or more problems with an Event.
//
// We keep this structured so you get ALL issues in one pass rather than fixing
// them one-by-one.
type ValidationError struct {
Problems []string
}
func (e *ValidationError) Error() string {
if e == nil || len(e.Problems) == 0 {
return "invalid event"
}
var b strings.Builder
b.WriteString("invalid event:\n")
for _, p := range e.Problems {
b.WriteString(" - ")
b.WriteString(p)
b.WriteString("\n")
}
return strings.TrimRight(b.String(), "\n")
}
// Is lets errors.Is(err, ErrInvalidEvent) work.
func (e *ValidationError) Is(target error) bool {
return target == ErrInvalidEvent
}
// Event is the domain-agnostic envelope moved through scheduler → pipeline → dispatcher → sinks.
//
// Payload is intentionally "any":
// - domain code can set it to a struct (recommended), a map, or some other JSON-marshable type.
// - feedkit infrastructure never type-asserts Payload.
//
// If you plan to persist/emit events as JSON, ensure Payload is JSON-marshable.
type Event struct {
// ID should be stable for dedupe/storage purposes.
// Often: "<provider>:<source>:<upstream-id-or-timestamp>".
ID string `json:"id"`
// Kind is used for routing/policy.
Kind Kind `json:"kind"`
// Source is the configured source name (e.g. "OpenMeteoObservation", "NWSAlertsSTL").
Source string `json:"source"`
// EmittedAt is when *your daemon* emitted this event (typically time.Now().UTC()).
EmittedAt time.Time `json:"emitted_at"`
// EffectiveAt is optional: the timestamp the payload is "about" (e.g. observation time).
EffectiveAt *time.Time `json:"effective_at,omitempty"`
// Schema is optional but strongly recommended once multiple domains exist.
// Examples:
// "weather.observation.v1"
// "news.article.v1"
// It helps sinks and downstream consumers interpret Payload.
Schema string `json:"schema,omitempty"`
// Payload is domain-defined and must be non-nil.
Payload any `json:"payload"`
}
// Validate enforces basic invariants that infrastructure depends on.
// Domain-specific validation belongs in domain code (or domain processors).
func (e Event) Validate() error {
var problems []string
if strings.TrimSpace(e.ID) == "" {
problems = append(problems, "ID is required")
}
if strings.TrimSpace(string(e.Kind)) == "" {
problems = append(problems, "Kind is required")
}
if strings.TrimSpace(e.Source) == "" {
problems = append(problems, "Source is required")
}
if e.EmittedAt.IsZero() {
problems = append(problems, "EmittedAt must be set (non-zero)")
}
if e.Payload == nil {
problems = append(problems, "Payload must be non-nil")
}
if len(problems) > 0 {
return &ValidationError{Problems: problems}
}
return nil
}