floe_core/report/
output.rs1use std::path::{Path, PathBuf};
2
3use crate::io::storage::{paths, CloudClient, Target};
4use crate::report::{JsonReportFormatter, ReportFormatter};
5use crate::{config, report, FloeResult};
6
7pub struct ReportOutput<'a> {
8 target: &'a Target,
9 formatter: &'a dyn ReportFormatter,
10 cloud: &'a mut CloudClient,
11 resolver: &'a config::StorageResolver,
12}
13
14impl<'a> ReportOutput<'a> {
15 pub fn new(
16 target: &'a Target,
17 formatter: &'a dyn ReportFormatter,
18 cloud: &'a mut CloudClient,
19 resolver: &'a config::StorageResolver,
20 ) -> Self {
21 Self {
22 target,
23 formatter,
24 cloud,
25 resolver,
26 }
27 }
28
29 pub fn write_entity_report(
30 &mut self,
31 run_id: &str,
32 entity: &config::EntityConfig,
33 report: &report::RunReport,
34 ) -> FloeResult<String> {
35 let relative = report::ReportWriter::report_relative_path(run_id, &entity.name);
36 write_report(
37 self.target,
38 &relative,
39 self.formatter,
40 ReportPayload::Entity(report),
41 self.cloud,
42 self.resolver,
43 &format!("entity.name={}", entity.name),
44 )
45 }
46
47 pub fn write_summary_report(
48 &mut self,
49 run_id: &str,
50 report: &report::RunSummaryReport,
51 ) -> FloeResult<String> {
52 let relative = report::ReportWriter::summary_relative_path(run_id);
53 write_report(
54 self.target,
55 &relative,
56 self.formatter,
57 ReportPayload::Summary(report),
58 self.cloud,
59 self.resolver,
60 "report",
61 )
62 }
63}
64
65pub fn write_entity_report(
66 target: &Target,
67 run_id: &str,
68 entity: &config::EntityConfig,
69 report: &report::RunReport,
70 cloud: &mut CloudClient,
71 resolver: &config::StorageResolver,
72) -> FloeResult<String> {
73 let formatter = JsonReportFormatter;
74 let mut output = ReportOutput::new(target, &formatter, cloud, resolver);
75 output.write_entity_report(run_id, entity, report)
76}
77
78pub fn write_summary_report(
79 target: &Target,
80 run_id: &str,
81 report: &report::RunSummaryReport,
82 cloud: &mut CloudClient,
83 resolver: &config::StorageResolver,
84) -> FloeResult<String> {
85 let formatter = JsonReportFormatter;
86 let mut output = ReportOutput::new(target, &formatter, cloud, resolver);
87 output.write_summary_report(run_id, report)
88}
89
90enum ReportPayload<'a> {
91 Entity(&'a report::RunReport),
92 Summary(&'a report::RunSummaryReport),
93}
94
95fn write_report(
96 target: &Target,
97 relative: &str,
98 formatter: &dyn ReportFormatter,
99 payload: ReportPayload<'_>,
100 cloud: &mut CloudClient,
101 resolver: &config::StorageResolver,
102 context: &str,
103) -> FloeResult<String> {
104 let content = match payload {
106 ReportPayload::Entity(report) => formatter.serialize_run(report)?,
107 ReportPayload::Summary(report) => formatter.serialize_summary(report)?,
108 };
109
110 match target {
111 Target::Local { base_path, .. } => {
112 let output_path = paths::resolve_output_dir_path(base_path, relative);
113 write_text_file(&output_path, &content)?;
114 Ok(output_path.display().to_string())
115 }
116 _ => {
117 let uri = target.join_relative(relative);
118 let temp_dir = tempfile::tempdir()?;
119 let filename = Path::new(relative)
120 .file_name()
121 .and_then(|name| name.to_str())
122 .unwrap_or("report.json");
123 let temp_path = temp_dir.path().join(filename);
124 write_text_file(&temp_path, &content)?;
125 let client = cloud.client_for_context(resolver, target.storage(), context)?;
126 client.upload_from_path(&temp_path, &uri)?;
127 Ok(uri)
128 }
129 }
130}
131
132fn write_text_file(path: &Path, content: &str) -> FloeResult<()> {
133 if let Some(parent) = path.parent() {
134 std::fs::create_dir_all(parent)?;
135 }
136 let tmp_path = temp_path(path);
137 let mut file = std::fs::File::create(&tmp_path)?;
138 use std::io::Write;
139 file.write_all(content.as_bytes())?;
140 file.sync_all()?;
141 std::fs::rename(&tmp_path, path)?;
142 Ok(())
143}
144
145fn temp_path(path: &Path) -> PathBuf {
146 let file_name = path
147 .file_name()
148 .and_then(|name| name.to_str())
149 .unwrap_or("report.json");
150 let tmp_name = format!("{file_name}.tmp-{}", unique_suffix());
151 path.parent().unwrap_or(path).join(tmp_name)
152}
153
154fn unique_suffix() -> String {
155 use std::time::{SystemTime, UNIX_EPOCH};
156 let nanos = SystemTime::now()
157 .duration_since(UNIX_EPOCH)
158 .map(|duration| duration.as_nanos())
159 .unwrap_or(0);
160 format!("{}-{}", std::process::id(), nanos)
161}