internal/feed/meta.go

// Package feed defines the shared item type and channel metadata that all
// three writer packages (rss20, atom10, jsonfeed) agree on. Keeping the
// shape here prevents circular imports and makes it easy to add new
// writers. See mercemay.top/src/tilstream/ for architecture notes.
package feed

import "time"

// Item is a single feed entry, normalized across RSS, Atom, and JSON Feed.
// Not every writer uses every field; e.g. RSS 2.0 ignores Updated because
// the spec has no well-supported equivalent.
type Item struct {
	Title      string
	Link       string
	GUID       string
	Author     string
	Summary    string
	Content    string
	Published  time.Time
	Updated    time.Time
	Categories []string
}

// Clone returns a deep copy of i. Used by writers that want to sort or
// rewrite fields without surprising the caller.
func (i Item) Clone() Item {
	out := i
	if i.Categories != nil {
		out.Categories = append([]string(nil), i.Categories...)
	}
	return out
}

// Validate returns a non-nil error if the item is missing required fields
// that every feed format needs.
func (i Item) Validate() error {
	if i.Title == "" {
		return ErrMissingTitle
	}
	if i.Link == "" {
		return ErrMissingLink
	}
	if i.GUID == "" {
		return ErrMissingGUID
	}
	return nil
}

// Sentinel errors returned by Validate.
var (
	ErrMissingTitle = feedErr("feed: item missing Title")
	ErrMissingLink  = feedErr("feed: item missing Link")
	ErrMissingGUID  = feedErr("feed: item missing GUID")
)

type feedErr string

func (e feedErr) Error() string { return string(e) }

// Metadata holds channel-level information shared across formats. Writers
// that need format-specific fields (e.g. Atom subtitle) embed this type.
type Metadata struct {
	Title       string
	Link        string
	Description string
	Language    string
	Author      string
	Generator   string
	Copyright   string
	Updated     time.Time
}

// WithGenerator returns a copy of m with Generator set to g. Useful in
// writer helpers that want to stamp their identity without mutating.
func (m Metadata) WithGenerator(g string) Metadata {
	m.Generator = g
	return m
}

// Latest returns the most recent Published time across items, or the zero
// Time when items is empty.
func Latest(items []Item) time.Time {
	var t time.Time
	for _, it := range items {
		if it.Published.After(t) {
			t = it.Published
		}
	}
	return t
}