Skip to main content

joy_core/
templates.rs

1// Copyright (c) 2026 Joydev GmbH (joydev.com)
2// SPDX-License-Identifier: MIT
3
4use chrono::Utc;
5use minijinja::Environment;
6
7use crate::error::JoyError;
8use crate::model::item::{Item, ItemType};
9
10// Embedded item templates
11const 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
32/// Render an item template for the given type, filling in id and title.
33/// Returns a deserialized Item ready for further modification.
34pub 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); // plan, implement, review
74    }
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}