Removed redundant event.go (we use feedkit's upstream implementation).
This commit is contained in:
@@ -1,212 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrInvalidEvent is a sentinel error used for errors.Is checks.
|
|
||||||
var ErrInvalidEvent = errors.New("invalid event")
|
|
||||||
|
|
||||||
// EventValidationError reports one or more problems with an Event.
|
|
||||||
//
|
|
||||||
// We keep this structured because it makes debugging faster than a single
|
|
||||||
// "invalid event" string; you get all issues in one pass.
|
|
||||||
type EventValidationError struct {
|
|
||||||
Problems []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *EventValidationError) 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 *EventValidationError) Is(target error) bool {
|
|
||||||
return target == ErrInvalidEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event is the normalized unit your pipeline moves around.
|
|
||||||
// It wraps exactly one of Observation/Forecast/Alert plus metadata.
|
|
||||||
type Event struct {
|
|
||||||
ID string // stable dedupe/storage key (source-defined or computed)
|
|
||||||
Kind Kind
|
|
||||||
Source string // configured source name (e.g. "NWSObservationKSTL")
|
|
||||||
EmittedAt time.Time // when *your* system emitted this event
|
|
||||||
EffectiveAt *time.Time // optional: “time the event applies”
|
|
||||||
|
|
||||||
// Union payload: EXACTLY ONE must be non-nil.
|
|
||||||
Observation *WeatherObservation
|
|
||||||
Forecast *WeatherForecast
|
|
||||||
Alert *WeatherAlert
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate enforces Event invariants.
|
|
||||||
//
|
|
||||||
// This is intentionally strict. If an event is invalid, we want to find out
|
|
||||||
// immediately rather than letting it drift into sinks or storage.
|
|
||||||
//
|
|
||||||
// Invariants enforced:
|
|
||||||
// - ID is non-empty
|
|
||||||
// - Kind is known
|
|
||||||
// - Source is non-empty
|
|
||||||
// - EmittedAt is non-zero
|
|
||||||
// - Exactly one payload pointer is non-nil
|
|
||||||
// - Kind matches the non-nil payload
|
|
||||||
func (e Event) Validate() error {
|
|
||||||
var problems []string
|
|
||||||
|
|
||||||
if strings.TrimSpace(e.ID) == "" {
|
|
||||||
problems = append(problems, "ID is required")
|
|
||||||
}
|
|
||||||
if !e.Kind.IsKnown() {
|
|
||||||
problems = append(problems, fmt.Sprintf("Kind %q is not recognized", string(e.Kind)))
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(e.Source) == "" {
|
|
||||||
problems = append(problems, "Source is required")
|
|
||||||
}
|
|
||||||
if e.EmittedAt.IsZero() {
|
|
||||||
problems = append(problems, "EmittedAt must be set (non-zero)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count payloads and ensure Kind matches.
|
|
||||||
payloadCount := 0
|
|
||||||
if e.Observation != nil {
|
|
||||||
payloadCount++
|
|
||||||
if e.Kind != KindObservation {
|
|
||||||
problems = append(problems, fmt.Sprintf("Observation payload present but Kind=%q", string(e.Kind)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if e.Forecast != nil {
|
|
||||||
payloadCount++
|
|
||||||
if e.Kind != KindForecast {
|
|
||||||
problems = append(problems, fmt.Sprintf("Forecast payload present but Kind=%q", string(e.Kind)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if e.Alert != nil {
|
|
||||||
payloadCount++
|
|
||||||
if e.Kind != KindAlert {
|
|
||||||
problems = append(problems, fmt.Sprintf("Alert payload present but Kind=%q", string(e.Kind)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if payloadCount == 0 {
|
|
||||||
problems = append(problems, "exactly one payload must be set; all payloads are nil")
|
|
||||||
} else if payloadCount > 1 {
|
|
||||||
problems = append(problems, "exactly one payload must be set; multiple payloads are non-nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(problems) > 0 {
|
|
||||||
return &EventValidationError{Problems: problems}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewObservationEvent constructs a valid observation Event.
|
|
||||||
//
|
|
||||||
// If emittedAt is zero, it defaults to time.Now().UTC().
|
|
||||||
// effectiveAt is optional (nil allowed).
|
|
||||||
//
|
|
||||||
// The returned Event is guaranteed valid (or you get an error).
|
|
||||||
func NewObservationEvent(
|
|
||||||
id string,
|
|
||||||
source string,
|
|
||||||
emittedAt time.Time,
|
|
||||||
effectiveAt *time.Time,
|
|
||||||
obs *WeatherObservation,
|
|
||||||
) (Event, error) {
|
|
||||||
if obs == nil {
|
|
||||||
return Event{}, fmt.Errorf("%w: observation payload is nil", ErrInvalidEvent)
|
|
||||||
}
|
|
||||||
|
|
||||||
if emittedAt.IsZero() {
|
|
||||||
emittedAt = time.Now().UTC()
|
|
||||||
}
|
|
||||||
|
|
||||||
e := Event{
|
|
||||||
ID: strings.TrimSpace(id),
|
|
||||||
Kind: KindObservation,
|
|
||||||
Source: strings.TrimSpace(source),
|
|
||||||
EmittedAt: emittedAt,
|
|
||||||
EffectiveAt: effectiveAt,
|
|
||||||
Observation: obs,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := e.Validate(); err != nil {
|
|
||||||
return Event{}, err
|
|
||||||
}
|
|
||||||
return e, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewForecastEvent constructs a valid forecast Event.
|
|
||||||
func NewForecastEvent(
|
|
||||||
id string,
|
|
||||||
source string,
|
|
||||||
emittedAt time.Time,
|
|
||||||
effectiveAt *time.Time,
|
|
||||||
fc *WeatherForecast,
|
|
||||||
) (Event, error) {
|
|
||||||
if fc == nil {
|
|
||||||
return Event{}, fmt.Errorf("%w: forecast payload is nil", ErrInvalidEvent)
|
|
||||||
}
|
|
||||||
|
|
||||||
if emittedAt.IsZero() {
|
|
||||||
emittedAt = time.Now().UTC()
|
|
||||||
}
|
|
||||||
|
|
||||||
e := Event{
|
|
||||||
ID: strings.TrimSpace(id),
|
|
||||||
Kind: KindForecast,
|
|
||||||
Source: strings.TrimSpace(source),
|
|
||||||
EmittedAt: emittedAt,
|
|
||||||
EffectiveAt: effectiveAt,
|
|
||||||
Forecast: fc,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := e.Validate(); err != nil {
|
|
||||||
return Event{}, err
|
|
||||||
}
|
|
||||||
return e, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAlertEvent constructs a valid alert Event.
|
|
||||||
func NewAlertEvent(
|
|
||||||
id string,
|
|
||||||
source string,
|
|
||||||
emittedAt time.Time,
|
|
||||||
effectiveAt *time.Time,
|
|
||||||
a *WeatherAlert,
|
|
||||||
) (Event, error) {
|
|
||||||
if a == nil {
|
|
||||||
return Event{}, fmt.Errorf("%w: alert payload is nil", ErrInvalidEvent)
|
|
||||||
}
|
|
||||||
|
|
||||||
if emittedAt.IsZero() {
|
|
||||||
emittedAt = time.Now().UTC()
|
|
||||||
}
|
|
||||||
|
|
||||||
e := Event{
|
|
||||||
ID: strings.TrimSpace(id),
|
|
||||||
Kind: KindAlert,
|
|
||||||
Source: strings.TrimSpace(source),
|
|
||||||
EmittedAt: emittedAt,
|
|
||||||
EffectiveAt: effectiveAt,
|
|
||||||
Alert: a,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := e.Validate(); err != nil {
|
|
||||||
return Event{}, err
|
|
||||||
}
|
|
||||||
return e, nil
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user