Skip to main content

ralph_workflow/rendering/xml/
mod.rs

1//! Semantic XML renderers for user-friendly output.
2//!
3//! This module routes XML rendering to type-specific modules.
4//! Each XML output type (`DevelopmentResult`, `DevelopmentPlan`, etc.) has
5//! a dedicated renderer that transforms raw XML into user-friendly
6//! terminal output.
7//!
8//! # Graceful Degradation
9//!
10//! If XML parsing fails, renderers fall back to displaying the raw XML
11//! with a warning message. This ensures users always see output even if
12//! the format is unexpected.
13
14mod commit_message;
15mod development_plan;
16mod development_result;
17mod fix_result;
18mod helpers;
19mod review_issues;
20
21use crate::files::llm_output_extraction::SkillsMcp;
22use crate::reducer::ui_event::{XmlOutputContext, XmlOutputType};
23
24/// Render skills-mcp recommendations in markdown format.
25///
26/// Renders structured skills and MCPs with optional reasons, or raw content if no structured data.
27#[must_use]
28pub fn render_skills_mcp_markdown(skills_mcp: Option<&SkillsMcp>) -> String {
29    let Some(sm) = skills_mcp else {
30        return String::new();
31    };
32
33    let has_structured = !sm.skills.is_empty() || !sm.mcps.is_empty();
34    if !(has_structured || sm.raw_content.is_some()) {
35        return String::new();
36    }
37
38    // Build skills lines using iterator pipeline
39    let skills_lines: Vec<String> = sm
40        .skills
41        .iter()
42        .map(|skill| {
43            if let Some(ref reason) = skill.reason {
44                format!("    - skill: {} \u{2014} {}", skill.name, reason)
45            } else {
46                format!("    - skill: {}", skill.name)
47            }
48        })
49        .collect();
50
51    // Build MCP lines using iterator pipeline
52    let mcp_lines: Vec<String> = sm
53        .mcps
54        .iter()
55        .map(|mcp| {
56            if let Some(ref reason) = mcp.reason {
57                format!("    - mcp: {} \u{2014} {}", mcp.name, reason)
58            } else {
59                format!("    - mcp: {}", mcp.name)
60            }
61        })
62        .collect();
63
64    // Build raw content line if applicable
65    let raw_line: Option<String> = sm.raw_content.as_ref().and_then(|raw| {
66        let trimmed: &str = raw.trim();
67        if trimmed.is_empty() || has_structured {
68            None
69        } else {
70            Some(format!("    - {}", trimmed))
71        }
72    });
73
74    // Combine all parts using chain and collect
75    std::iter::once("  - Skills & MCP:".to_string())
76        .chain(skills_lines)
77        .chain(mcp_lines)
78        .chain(raw_line)
79        .collect::<Vec<_>>()
80        .join("\n")
81}
82
83/// Render XML content based on its type.
84///
85/// Returns formatted string for terminal display.
86/// Falls back to raw XML with warning if parsing fails.
87#[must_use]
88pub fn render_xml(
89    xml_type: &XmlOutputType,
90    content: &str,
91    output_context: &Option<XmlOutputContext>,
92) -> String {
93    match xml_type {
94        XmlOutputType::DevelopmentResult => {
95            development_result::render(content, output_context.as_ref())
96        }
97        XmlOutputType::DevelopmentPlan => development_plan::render(content),
98        XmlOutputType::ReviewIssues => review_issues::render(content, output_context.as_ref()),
99        XmlOutputType::FixResult => fix_result::render(content, output_context.as_ref()),
100        XmlOutputType::CommitMessage => commit_message::render(content),
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn test_render_xml_routes_to_development_result() {
110        let content = r"<ralph-development-result>
111<ralph-status>completed</ralph-status>
112<ralph-summary>Done</ralph-summary>
113</ralph-development-result>";
114
115        let output = render_xml(&XmlOutputType::DevelopmentResult, content, &None);
116        assert!(
117            output.contains("✅"),
118            "Should route to development result renderer"
119        );
120    }
121
122    #[test]
123    fn test_render_xml_routes_to_review_issues() {
124        let content = r"<ralph-issues>
125<ralph-issue>Test issue</ralph-issue>
126</ralph-issues>";
127
128        let output = render_xml(&XmlOutputType::ReviewIssues, content, &None);
129        assert!(
130            output.contains("1 issue"),
131            "Should route to issues renderer"
132        );
133    }
134
135    #[test]
136    fn test_render_xml_routes_to_commit_message() {
137        let content = r"<ralph-commit>
138<ralph-subject>feat: add feature</ralph-subject>
139</ralph-commit>";
140
141        let output = render_xml(&XmlOutputType::CommitMessage, content, &None);
142        assert!(
143            output.contains("feat: add feature"),
144            "Should route to commit renderer"
145        );
146    }
147}