use serde::{Deserialize, Serialize};
use crate::render::TokenSpan;
use crate::traits::core::PluginResult;
pub type FragmentSlot = String;
pub const FRAGMENT_SLOT_REGISTRY: &[&str] = &[
"mr_detail.stack_panel",
"mr_detail.linked_issues",
"mr_detail.ci_status",
"mr_list.backend_pill",
"mr_list.stack_indicator",
"dashboard.profile_card",
"dashboard.releases_card",
"file_tree.node_decoration",
"review.draft_pill",
];
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FragmentContext {
pub slot: FragmentSlot,
pub data: serde_json::Value,
pub max_rows: usize,
pub max_cols: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RenderFragment {
pub lines: Vec<Vec<TokenSpan>>,
pub ready: bool,
pub actions: Vec<FragmentAction>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FragmentAction {
pub key: String,
pub label: String,
pub command: String,
}
pub trait FragmentRenderer {
fn fragment_slots(&self) -> Vec<FragmentSlot>;
fn render_fragment(&mut self, ctx: &FragmentContext) -> PluginResult<RenderFragment>;
}
pub fn slot_is_valid(slot: &str) -> bool {
FRAGMENT_SLOT_REGISTRY.iter().any(|&s| s == slot)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_slots_validate() {
for slot in FRAGMENT_SLOT_REGISTRY {
assert!(slot_is_valid(slot), "slot {slot} should validate");
}
}
#[test]
fn unknown_slot_rejected() {
assert!(!slot_is_valid("unknown.slot"));
assert!(!slot_is_valid(""));
}
#[test]
fn render_fragment_serde_round_trip() {
let f = RenderFragment {
lines: vec![],
ready: true,
actions: vec![FragmentAction {
key: "Ctrl+L".into(),
label: "Land stack".into(),
command: "stack-land".into(),
}],
};
let s = serde_json::to_string(&f).unwrap();
let back: RenderFragment = serde_json::from_str(&s).unwrap();
assert!(back.ready);
assert_eq!(back.actions.len(), 1);
assert_eq!(back.actions[0].command, "stack-land");
}
}