package sources import ( "fmt" "sort" "strings" "time" "gitea.maximumdirect.net/ejr/feedkit/config" "gitea.maximumdirect.net/ejr/feedkit/event" ) // DefaultEventID applies feedkit's default Event.ID policy: // // - If upstream provides an ID, use it (trimmed). // - Otherwise, ID is ":" when available. // - If EffectiveAt is unavailable, fall back to ":". // // Timestamps are encoded as RFC3339Nano in UTC. func DefaultEventID(upstreamID, sourceName string, effectiveAt *time.Time, emittedAt time.Time) string { if id := strings.TrimSpace(upstreamID); id != "" { return id } src := strings.TrimSpace(sourceName) if src == "" { src = "UNKNOWN_SOURCE" } if effectiveAt != nil && !effectiveAt.IsZero() { return fmt.Sprintf("%s:%s", src, effectiveAt.UTC().Format(time.RFC3339Nano)) } t := emittedAt.UTC() if t.IsZero() { t = time.Now().UTC() } return fmt.Sprintf("%s:%s", src, t.Format(time.RFC3339Nano)) } // SingleEvent constructs, validates, and returns a slice containing exactly one event. func SingleEvent( kind event.Kind, sourceName string, schema string, id string, emittedAt time.Time, effectiveAt *time.Time, payload any, ) ([]event.Event, error) { if emittedAt.IsZero() { emittedAt = time.Now().UTC() } else { emittedAt = emittedAt.UTC() } e := event.Event{ ID: id, Kind: kind, Source: sourceName, EmittedAt: emittedAt, EffectiveAt: effectiveAt, Schema: schema, Payload: payload, } if err := e.Validate(); err != nil { return nil, err } return []event.Event{e}, nil } // ValidateExpectedKinds checks that configured source expected kinds are a subset // of the kinds advertised by the built source, when the source exposes kind // metadata. If the source does not advertise kinds, the check is skipped. func ValidateExpectedKinds(cfg config.SourceConfig, in Input) error { expectedKinds, err := parseExpectedKinds(cfg.ExpectedKinds()) if err != nil { return err } if len(expectedKinds) == 0 { return nil } advertisedKinds := advertisedSourceKinds(in) if len(advertisedKinds) == 0 { return nil } for kind := range expectedKinds { if !advertisedKinds[kind] { return fmt.Errorf( "configured expected kind %q not advertised by source (configured=%v advertised=%v)", kind, sortedKinds(expectedKinds), sortedKinds(advertisedKinds), ) } } return nil } func parseExpectedKinds(raw []string) (map[event.Kind]bool, error) { kinds := map[event.Kind]bool{} for i, k := range raw { kind, err := event.ParseKind(k) if err != nil { return nil, fmt.Errorf("invalid expected kind at index %d (%q): %w", i, k, err) } kinds[kind] = true } return kinds, nil } func advertisedSourceKinds(in Input) map[event.Kind]bool { if in == nil { return nil } kinds := map[event.Kind]bool{} if ks, ok := in.(KindsSource); ok { for _, kind := range ks.Kinds() { kinds[kind] = true } return kinds } return nil } func sortedKinds(kindSet map[event.Kind]bool) []string { out := make([]string, 0, len(kindSet)) for kind := range kindSet { out = append(out, string(kind)) } sort.Strings(out) return out }