feedkit now contains a reusable core, while weatherfeeder is a concrete implementation that includes weather-specific functions.
102 lines
2.9 KiB
Go
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
|
|
}
|