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: "::". 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 }