feat(api): reshape raw events into TimelineItem
GET /v1/events now returns the presentation form rather than raw
upstream payloads. The frontend stays dumb: it renders title /
subtitle / body segments and picks an icon from a small kebab-case
enum. Title and subtitle are arrays of {text} | {text, url} segments
so the UI can interleave plain copy with anchors without parsing.
New entities (in moments-entities):
TimelineItem — id, source, action, occurred_at, icon, title, subtitle, body
TitleSegment — Text | Link
TimelineBody — Markdown | Commits | Links
CommitSummary — sha, short_sha, message, url, author
TimelineIcon — kebab-case enum; UI falls back to Generic on unknowns
Reshape lives in moments-core::presentation, dispatched by source.
github.rs covers the event types observed on grenade's feed:
PushEvent, PullRequest{,Review,ReviewComment}Event, Issues{,Comment}Event,
Create/Delete/Fork/Watch/Release/CommitComment/PublicEvent.
Anything else falls back to a generic "<action> on <repo>" line.
Other sources (gitea, hg, bugzilla) currently use a stub fallback;
they get their own reshape modules in steps 5 and 6.
4 unit tests cover the load-bearing cases (push commit list, merged
PR icon swap, issue-comment markdown body, unknown-event fallback).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -74,3 +74,79 @@ pub struct SourceSummary {
|
||||
pub earliest: Option<DateTime<Utc>>,
|
||||
pub latest: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Presentation shape — what `GET /v1/events` actually returns.
|
||||
// The API reshapes raw payloads into these so the frontend stays dumb.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TimelineItem {
|
||||
pub id: String,
|
||||
pub source: Source,
|
||||
pub action: String,
|
||||
pub occurred_at: DateTime<Utc>,
|
||||
pub icon: TimelineIcon,
|
||||
/// Primary headline. Mixed plain text + inline links so the UI can
|
||||
/// render the right anchors without parsing.
|
||||
pub title: Vec<TitleSegment>,
|
||||
pub subtitle: Option<Vec<TitleSegment>>,
|
||||
pub body: Option<TimelineBody>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(tag = "kind", rename_all = "lowercase")]
|
||||
pub enum TitleSegment {
|
||||
Text { text: String },
|
||||
Link { text: String, url: String },
|
||||
}
|
||||
|
||||
impl TitleSegment {
|
||||
pub fn text(s: impl Into<String>) -> Self {
|
||||
Self::Text { text: s.into() }
|
||||
}
|
||||
pub fn link(text: impl Into<String>, url: impl Into<String>) -> Self {
|
||||
Self::Link {
|
||||
text: text.into(),
|
||||
url: url.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "lowercase")]
|
||||
pub enum TimelineBody {
|
||||
Markdown { text: String },
|
||||
Commits { commits: Vec<CommitSummary> },
|
||||
Links { items: Vec<TitleSegment> },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CommitSummary {
|
||||
pub sha: String,
|
||||
pub short_sha: String,
|
||||
pub message: String,
|
||||
pub url: String,
|
||||
pub author: Option<String>,
|
||||
}
|
||||
|
||||
/// UI icon hint. The frontend maps these to its own icon set; new variants
|
||||
/// here require a frontend update but never break existing renders (the UI
|
||||
/// falls back to the generic icon for unknown values).
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum TimelineIcon {
|
||||
GitPush,
|
||||
GitCommit,
|
||||
GitMerge,
|
||||
GitFork,
|
||||
GitBranchCreate,
|
||||
GitBranchDelete,
|
||||
PullRequest,
|
||||
Issue,
|
||||
Comment,
|
||||
Star,
|
||||
Release,
|
||||
Bug,
|
||||
Generic,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user