use std::collections::BTreeMap;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use phasesmith_model::{ProjectRecord, RadiationProbe};
use serde::Serialize;
use crate::{PROJECT_FORMAT_VERSION, PersistenceError};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ProjectReportSaveOptions {
pub overwrite: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct HistogramSummary {
pub histogram_id: String,
pub name: String,
pub sample_count: usize,
pub has_observations: bool,
pub probe: String,
pub phase_ids: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct PhaseSummary {
pub phase_id: String,
pub name: String,
pub reflection_count: usize,
pub site_count: usize,
pub required_providers: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct ProjectSummaryReport {
pub format_version: u32,
pub project_id: String,
pub revision: u64,
pub name: String,
pub histogram_count: usize,
pub phase_count: usize,
pub total_sample_count: usize,
pub histograms: Vec<HistogramSummary>,
pub phases: Vec<PhaseSummary>,
pub metadata: BTreeMap<String, String>,
}
impl ProjectSummaryReport {
pub fn from_project(project: &ProjectRecord) -> Result<Self, PersistenceError> {
project.validate().map_err(PersistenceError::Domain)?;
let total_sample_count =
project
.histograms
.iter()
.try_fold(0_usize, |total, histogram| {
total
.checked_add(histogram.pattern.sample_count())
.ok_or_else(|| PersistenceError::InvalidRecord {
message: "project summary sample count overflow".to_owned(),
})
})?;
Ok(Self {
format_version: PROJECT_FORMAT_VERSION,
project_id: project.project_id.as_str().to_owned(),
revision: project.revision,
name: project.name.clone(),
histogram_count: project.histograms.len(),
phase_count: project.phases.len(),
total_sample_count,
histograms: project
.histograms
.iter()
.map(|histogram| HistogramSummary {
histogram_id: histogram.histogram_id.as_str().to_owned(),
name: histogram.name.clone(),
sample_count: histogram.pattern.sample_count(),
has_observations: histogram.pattern.observed_y.is_some(),
probe: match histogram.experiment.radiation.probe() {
RadiationProbe::Xray => "xray",
RadiationProbe::Neutron => "neutron",
}
.to_owned(),
phase_ids: histogram
.phase_ids
.iter()
.map(|value| value.as_str().to_owned())
.collect(),
})
.collect(),
phases: project
.phases
.iter()
.map(|phase| PhaseSummary {
phase_id: phase.phase_id.as_str().to_owned(),
name: phase.name.clone(),
reflection_count: phase.definition.hkl.len(),
site_count: phase.definition.fractional_xyz.len(),
required_providers: phase
.required_providers
.iter()
.map(|requirement| {
format!(
"{}@{}",
requirement.provider_id, requirement.provider_version
)
})
.collect(),
})
.collect(),
metadata: project.metadata.clone(),
})
}
}
pub fn project_summary_json(project: &ProjectRecord) -> Result<String, PersistenceError> {
let report = ProjectSummaryReport::from_project(project)?;
let mut encoded = serde_json::to_string_pretty(&report)?;
encoded.push('\n');
Ok(encoded)
}
pub fn write_project_summary_json(
project: &ProjectRecord,
path: impl AsRef<Path>,
) -> Result<PathBuf, PersistenceError> {
write_project_summary_json_with_options(
project,
path,
ProjectReportSaveOptions { overwrite: true },
)
}
pub fn write_project_summary_json_with_options(
project: &ProjectRecord,
path: impl AsRef<Path>,
options: ProjectReportSaveOptions,
) -> Result<PathBuf, PersistenceError> {
let path = path.as_ref();
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
{
fs::create_dir_all(parent)?;
}
let encoded = project_summary_json(project)?;
if options.overwrite {
fs::write(path, encoded)?;
} else {
fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
.map_err(|error| {
if error.kind() == std::io::ErrorKind::AlreadyExists {
PersistenceError::InvalidDestination {
message: format!("report destination already exists: {}", path.display()),
}
} else {
PersistenceError::Io(error)
}
})?
.write_all(encoded.as_bytes())?;
}
Ok(path.to_owned())
}