1use std::collections::BTreeMap;
4use std::fs;
5use std::io::Write;
6use std::path::{Path, PathBuf};
7
8use phasesmith_model::{ProjectRecord, RadiationProbe};
9use serde::Serialize;
10
11use crate::{PROJECT_FORMAT_VERSION, PersistenceError};
12
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
15pub struct ProjectReportSaveOptions {
16 pub overwrite: bool,
18}
19
20#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
22pub struct HistogramSummary {
23 pub histogram_id: String,
25 pub name: String,
27 pub sample_count: usize,
29 pub has_observations: bool,
31 pub probe: String,
33 pub phase_ids: Vec<String>,
35}
36
37#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
39pub struct PhaseSummary {
40 pub phase_id: String,
42 pub name: String,
44 pub reflection_count: usize,
46 pub site_count: usize,
48 pub required_providers: Vec<String>,
50}
51
52#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
54pub struct ProjectSummaryReport {
55 pub format_version: u32,
57 pub project_id: String,
59 pub revision: u64,
61 pub name: String,
63 pub histogram_count: usize,
65 pub phase_count: usize,
67 pub total_sample_count: usize,
69 pub histograms: Vec<HistogramSummary>,
71 pub phases: Vec<PhaseSummary>,
73 pub metadata: BTreeMap<String, String>,
75}
76
77impl ProjectSummaryReport {
78 pub fn from_project(project: &ProjectRecord) -> Result<Self, PersistenceError> {
85 project.validate().map_err(PersistenceError::Domain)?;
86 let total_sample_count =
87 project
88 .histograms
89 .iter()
90 .try_fold(0_usize, |total, histogram| {
91 total
92 .checked_add(histogram.pattern.sample_count())
93 .ok_or_else(|| PersistenceError::InvalidRecord {
94 message: "project summary sample count overflow".to_owned(),
95 })
96 })?;
97 Ok(Self {
98 format_version: PROJECT_FORMAT_VERSION,
99 project_id: project.project_id.as_str().to_owned(),
100 revision: project.revision,
101 name: project.name.clone(),
102 histogram_count: project.histograms.len(),
103 phase_count: project.phases.len(),
104 total_sample_count,
105 histograms: project
106 .histograms
107 .iter()
108 .map(|histogram| HistogramSummary {
109 histogram_id: histogram.histogram_id.as_str().to_owned(),
110 name: histogram.name.clone(),
111 sample_count: histogram.pattern.sample_count(),
112 has_observations: histogram.pattern.observed_y.is_some(),
113 probe: match histogram.experiment.radiation.probe() {
114 RadiationProbe::Xray => "xray",
115 RadiationProbe::Neutron => "neutron",
116 }
117 .to_owned(),
118 phase_ids: histogram
119 .phase_ids
120 .iter()
121 .map(|value| value.as_str().to_owned())
122 .collect(),
123 })
124 .collect(),
125 phases: project
126 .phases
127 .iter()
128 .map(|phase| PhaseSummary {
129 phase_id: phase.phase_id.as_str().to_owned(),
130 name: phase.name.clone(),
131 reflection_count: phase.definition.hkl.len(),
132 site_count: phase.definition.fractional_xyz.len(),
133 required_providers: phase
134 .required_providers
135 .iter()
136 .map(|requirement| {
137 format!(
138 "{}@{}",
139 requirement.provider_id, requirement.provider_version
140 )
141 })
142 .collect(),
143 })
144 .collect(),
145 metadata: project.metadata.clone(),
146 })
147 }
148}
149
150pub fn project_summary_json(project: &ProjectRecord) -> Result<String, PersistenceError> {
156 let report = ProjectSummaryReport::from_project(project)?;
157 let mut encoded = serde_json::to_string_pretty(&report)?;
158 encoded.push('\n');
159 Ok(encoded)
160}
161
162pub fn write_project_summary_json(
169 project: &ProjectRecord,
170 path: impl AsRef<Path>,
171) -> Result<PathBuf, PersistenceError> {
172 write_project_summary_json_with_options(
173 project,
174 path,
175 ProjectReportSaveOptions { overwrite: true },
176 )
177}
178
179pub fn write_project_summary_json_with_options(
186 project: &ProjectRecord,
187 path: impl AsRef<Path>,
188 options: ProjectReportSaveOptions,
189) -> Result<PathBuf, PersistenceError> {
190 let path = path.as_ref();
191 if let Some(parent) = path.parent()
192 && !parent.as_os_str().is_empty()
193 {
194 fs::create_dir_all(parent)?;
195 }
196 let encoded = project_summary_json(project)?;
197 if options.overwrite {
198 fs::write(path, encoded)?;
199 } else {
200 fs::OpenOptions::new()
201 .write(true)
202 .create_new(true)
203 .open(path)
204 .map_err(|error| {
205 if error.kind() == std::io::ErrorKind::AlreadyExists {
206 PersistenceError::InvalidDestination {
207 message: format!("report destination already exists: {}", path.display()),
208 }
209 } else {
210 PersistenceError::Io(error)
211 }
212 })?
213 .write_all(encoded.as_bytes())?;
214 }
215 Ok(path.to_owned())
216}