Skip to main content

xtalforge_modules/
report.rs

1//! Run reports (Tasks 5.10.4 and 5.10.6).
2//!
3//! A report is presentation, not results: every number in it was already
4//! written to the run folder. [`Report::to_markdown`] is the one renderer
5//! needed by the CLI and the docs; the structured form serializes to JSON for
6//! the bindings.
7
8use serde::{Deserialize, Serialize};
9
10/// One headed section of a report.
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub struct ReportSection {
13    /// Section heading.
14    pub heading: String,
15    /// Section body, in Markdown.
16    pub body: String,
17}
18
19/// A human-readable report attached to a run.
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21pub struct Report {
22    /// Stable id of the run that produced it.
23    pub run_id: String,
24    /// Report title.
25    pub title: String,
26    /// One-paragraph summary.
27    pub summary: String,
28    /// Body sections in presentation order.
29    pub sections: Vec<ReportSection>,
30    /// Citations of methods, programs and databases used by the run.
31    pub references: Vec<String>,
32}
33
34impl Report {
35    /// Renders the report as Markdown.
36    ///
37    /// Empty summaries and reference lists are omitted; sections keep their
38    /// order. The output ends with exactly one newline.
39    #[must_use]
40    pub fn to_markdown(&self) -> String {
41        let mut out = String::new();
42        out.push_str(&format!("# {}\n\n", self.title));
43        if !self.run_id.trim().is_empty() {
44            out.push_str(&format!("Run: `{}`\n\n", self.run_id));
45        }
46        if !self.summary.trim().is_empty() {
47            out.push_str(self.summary.trim());
48            out.push_str("\n\n");
49        }
50        for section in &self.sections {
51            out.push_str(&format!("## {}\n\n", section.heading));
52            let body = section.body.trim();
53            if !body.is_empty() {
54                out.push_str(body);
55                out.push_str("\n\n");
56            }
57        }
58        if !self.references.is_empty() {
59            out.push_str("## References\n\n");
60            for reference in &self.references {
61                out.push_str(&format!("- {reference}\n"));
62            }
63        }
64        let mut trimmed = out.trim_end().to_owned();
65        trimmed.push('\n');
66        trimmed
67    }
68}