146 lines
3.2 KiB
Go
146 lines
3.2 KiB
Go
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 "<Source>:<EffectiveAt>" when available.
|
|
// - If EffectiveAt is unavailable, fall back to "<Source>:<EmittedAt>".
|
|
//
|
|
// 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
|
|
}
|
|
|
|
if ks, ok := in.(KindSource); ok {
|
|
kinds[ks.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
|
|
}
|