1use chrono::Utc;
5use minijinja::Environment;
6
7use crate::error::JoyError;
8use crate::model::item::{Item, ItemType};
9
10const BASE_TEMPLATE: &str = include_str!("../data/items/_base.yaml");
12const EPIC_TEMPLATE: &str = include_str!("../data/items/epic.yaml");
13const STORY_TEMPLATE: &str = include_str!("../data/items/story.yaml");
14const TASK_TEMPLATE: &str = include_str!("../data/items/task.yaml");
15const BUG_TEMPLATE: &str = include_str!("../data/items/bug.yaml");
16const REWORK_TEMPLATE: &str = include_str!("../data/items/rework.yaml");
17const DECISION_TEMPLATE: &str = include_str!("../data/items/decision.yaml");
18const IDEA_TEMPLATE: &str = include_str!("../data/items/idea.yaml");
19
20fn template_for_type(item_type: &ItemType) -> (&'static str, &'static str) {
21 match item_type {
22 ItemType::Epic => ("epic.yaml", EPIC_TEMPLATE),
23 ItemType::Story => ("story.yaml", STORY_TEMPLATE),
24 ItemType::Task => ("task.yaml", TASK_TEMPLATE),
25 ItemType::Bug => ("bug.yaml", BUG_TEMPLATE),
26 ItemType::Rework => ("rework.yaml", REWORK_TEMPLATE),
27 ItemType::Decision => ("decision.yaml", DECISION_TEMPLATE),
28 ItemType::Idea => ("idea.yaml", IDEA_TEMPLATE),
29 }
30}
31
32pub fn render_item(item_type: &ItemType, id: &str, title: &str) -> Result<Item, JoyError> {
35 let mut env = Environment::new();
36 env.add_template("_base.yaml", BASE_TEMPLATE)
37 .map_err(|e| JoyError::Template(e.to_string()))?;
38
39 let (name, source) = template_for_type(item_type);
40 env.add_template(name, source)
41 .map_err(|e| JoyError::Template(e.to_string()))?;
42
43 let now = Utc::now().to_rfc3339();
44 let tmpl = env
45 .get_template(name)
46 .map_err(|e| JoyError::Template(e.to_string()))?;
47
48 let escaped_title = title.replace('\\', "\\\\").replace('"', "\\\"");
49 let yaml = tmpl
50 .render(minijinja::context! {
51 id => id,
52 title => escaped_title,
53 now => &now,
54 })
55 .map_err(|e| JoyError::Template(e.to_string()))?;
56
57 let item: Item =
58 serde_yaml_ng::from_str(&yaml).map_err(|e| JoyError::Template(e.to_string()))?;
59
60 Ok(item)
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66
67 #[test]
68 fn render_story_template() {
69 let item = render_item(&ItemType::Story, "JOY-0001", "Login page").unwrap();
70 assert_eq!(item.id, "JOY-0001");
71 assert_eq!(item.title, "Login page");
72 assert_eq!(item.item_type, ItemType::Story);
73 assert_eq!(item.capabilities.len(), 3); }
75
76 #[test]
77 fn render_idea_template() {
78 let item = render_item(&ItemType::Idea, "JOY-0002", "Wild thought").unwrap();
79 assert_eq!(item.item_type, ItemType::Idea);
80 assert!(item.capabilities.is_empty());
81 }
82
83 #[test]
84 fn render_title_with_colon() {
85 let item = render_item(&ItemType::Bug, "JOY-005A", "Fix: crash on startup").unwrap();
86 assert_eq!(item.title, "Fix: crash on startup");
87 }
88
89 #[test]
90 fn render_title_with_special_yaml_chars() {
91 let item = render_item(
92 &ItemType::Task,
93 "JOY-0099",
94 r#"Handle "quotes" & {braces} [brackets]"#,
95 )
96 .unwrap();
97 assert_eq!(item.title, r#"Handle "quotes" & {braces} [brackets]"#);
98 }
99
100 #[test]
101 fn render_all_types() {
102 let types = [
103 ItemType::Epic,
104 ItemType::Story,
105 ItemType::Task,
106 ItemType::Bug,
107 ItemType::Rework,
108 ItemType::Decision,
109 ItemType::Idea,
110 ];
111 for t in &types {
112 let item = render_item(t, "TEST-0001", "Test item").unwrap();
113 assert_eq!(item.item_type, *t);
114 }
115 }
116}