use crate::payload::{
BadgeData, Bar, BarsData, Body, CalendarData, EntriesData, Entry, HeatmapData, ImageLinkedItem,
ImageLinkedListData, LinkedLine, LinkedTextBlockData, MarkdownTextBlockData, NumberSeriesData,
PointSeries, PointSeriesData, RatioData, Status, TextBlockData, TextData, TimelineData,
TimelineEvent,
};
use crate::render::Shape;
pub fn canonical_sample(shape: Shape) -> Option<Body> {
Some(match shape {
Shape::Text => text("splashboard"),
Shape::TextBlock => text_block(&["splashboard", "greetings on cd"]),
Shape::MarkdownTextBlock => markdown(
"# splashboard\n\nMarkdown body with **bold**, *italic*, and `code`.\n\n- item one\n- item two",
),
Shape::LinkedTextBlock => linked_text_block(&[
("splashboard greets on cd", Some("https://example.com/")),
("recent commit landed", None),
]),
Shape::ImageLinkedList => image_linked_list(&[
(
"splashboard greets on cd",
Some("https://example.com/"),
None,
Some("docs · 2h"),
),
("recent commit landed", None, None, None),
]),
Shape::Entries => entries(&[("key", "value"), ("foo", "bar"), ("baz", "qux")]),
Shape::Ratio => ratio(0.67, "used"),
Shape::NumberSeries => {
number_series(&[2, 5, 3, 8, 4, 9, 6, 11, 7, 3, 4, 6, 2, 5, 8, 10, 7, 4, 9, 6])
}
Shape::PointSeries => sine_points(),
Shape::Bars => bars(&[("rust", 42), ("go", 28), ("ts", 17), ("py", 13)]),
Shape::Image => return None,
Shape::Calendar => calendar(2026, 4, Some(22), &[10, 15, 30]),
Shape::Heatmap => heatmap_grid(3, 7),
Shape::Badge => badge(Status::Ok, "passing"),
Shape::Timeline => timeline(&[
(1_700_000_000, "merged #42", Some("feat(render): heatmap")),
(1_699_990_000, "opened #41", None),
(1_699_900_000, "reverted #40", None),
]),
Shape::Error => return None,
})
}
pub fn text(s: &str) -> Body {
Body::Text(TextData {
value: s.to_string(),
})
}
pub fn text_block(ss: &[&str]) -> Body {
Body::TextBlock(TextBlockData {
lines: ss.iter().map(|s| (*s).to_string()).collect(),
})
}
pub fn markdown(s: &str) -> Body {
Body::MarkdownTextBlock(MarkdownTextBlockData {
value: s.to_string(),
})
}
pub fn linked_text_block(rows: &[(&str, Option<&str>)]) -> Body {
Body::LinkedTextBlock(LinkedTextBlockData {
items: rows
.iter()
.map(|(text, url)| LinkedLine {
text: (*text).to_string(),
url: url.map(String::from),
})
.collect(),
})
}
pub type SampleImageLinkedRow<'a> = (&'a str, Option<&'a str>, Option<&'a str>, Option<&'a str>);
pub fn image_linked_list(rows: &[SampleImageLinkedRow<'_>]) -> Body {
Body::ImageLinkedList(ImageLinkedListData {
items: rows
.iter()
.map(|(title, url, thumb, sub)| ImageLinkedItem {
title: (*title).to_string(),
url: url.map(String::from),
thumbnail_path: thumb.map(String::from),
subtitle: sub.map(String::from),
})
.collect(),
})
}
pub fn entries(kvs: &[(&str, &str)]) -> Body {
Body::Entries(EntriesData {
items: kvs
.iter()
.map(|(k, v)| Entry {
key: (*k).into(),
value: Some((*v).into()),
status: None,
})
.collect(),
})
}
pub fn ratio(value: f64, label: &str) -> Body {
Body::Ratio(RatioData {
value,
label: Some(label.into()),
denominator: None,
})
}
pub fn number_series(vs: &[u64]) -> Body {
Body::NumberSeries(NumberSeriesData {
values: vs.to_vec(),
})
}
pub fn bars(bs: &[(&str, u64)]) -> Body {
Body::Bars(BarsData {
bars: bs
.iter()
.map(|(l, v)| Bar {
label: (*l).into(),
value: *v,
value_label: None,
})
.collect(),
})
}
pub fn badge(status: Status, label: &str) -> Body {
Body::Badge(BadgeData {
status,
label: label.into(),
})
}
pub fn calendar(year: i32, month: u8, day: Option<u8>, events: &[u8]) -> Body {
Body::Calendar(CalendarData {
year,
month,
day,
events: events.to_vec(),
})
}
pub fn heatmap_grid(rows: u32, cols: u32) -> Body {
let cells: Vec<Vec<u32>> = (0..rows)
.map(|r| (0..cols).map(|c| (r * 3 + c * 2 + (r ^ c)) % 10).collect())
.collect();
Body::Heatmap(HeatmapData {
cells,
thresholds: None,
row_labels: None,
col_labels: None,
})
}
pub fn sine_points() -> Body {
Body::PointSeries(PointSeriesData {
series: vec![PointSeries {
name: "series".into(),
points: (0..20)
.map(|i| {
let x = i as f64;
(x, (x / 3.0).sin() * 5.0 + 10.0)
})
.collect(),
}],
})
}
pub fn timeline(events: &[(i64, &str, Option<&str>)]) -> Body {
Body::Timeline(TimelineData {
events: events
.iter()
.map(|(ts, title, detail)| TimelineEvent {
timestamp: *ts,
title: (*title).into(),
detail: detail.map(String::from),
status: None,
})
.collect(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::shape_of;
#[test]
fn canonical_sample_matches_requested_shape() {
[
Shape::Text,
Shape::TextBlock,
Shape::MarkdownTextBlock,
Shape::LinkedTextBlock,
Shape::ImageLinkedList,
Shape::Entries,
Shape::Ratio,
Shape::NumberSeries,
Shape::PointSeries,
Shape::Bars,
Shape::Calendar,
Shape::Heatmap,
Shape::Badge,
Shape::Timeline,
]
.into_iter()
.for_each(|shape| {
let body = canonical_sample(shape)
.unwrap_or_else(|| panic!("missing sample for {}", shape.as_str()));
assert_eq!(shape_of(&body), shape);
});
}
#[test]
fn canonical_sample_skips_non_portable_shapes() {
[Shape::Image, Shape::Error]
.into_iter()
.for_each(|shape| assert!(canonical_sample(shape).is_none()));
}
}