use crate::OxigisDesktopApp;
use crate::file_dialog::Ask;
pub(crate) enum PendingFileWrite {
Processing(oxigis_ui::ProcessingFileRequest),
Export(oxigis_ui::ExportRequest),
}
impl PendingFileWrite {
fn ask(&self) -> Ask {
match self {
Self::Processing(_) => Ask::SaveGeoJson,
Self::Export(request) => match request.kind {
oxigis_ui::ExportKind::GeoJson => Ask::SaveGeoJson,
oxigis_ui::ExportKind::Csv => Ask::SaveCsv,
},
}
}
fn suggested_name(&self) -> String {
match self {
Self::Processing(request) => format!("{}.geojson", request.name),
Self::Export(request) => request.suggested_file_name.clone(),
}
}
fn bytes(&self) -> Vec<u8> {
match self {
Self::Processing(request) => request.content.clone().into_bytes(),
Self::Export(request) => request.content_bytes(),
}
}
}
impl OxigisDesktopApp {
pub(crate) fn resolve_file_write(&mut self, write: PendingFileWrite) {
if self.asking_for_a_path() {
self.report_file_write_cancelled(
&write,
"Finish the file prompt that is already open first.",
);
return;
}
let ask = write.ask();
let suggested = write.suggested_name();
match self.ask_for_path(ask, &suggested) {
Some(path) => {
let _written = self.write_data_file(write, path);
}
None if self.path_prompt.is_some() => self.pending_file_write = Some(write),
None => self.report_file_write_cancelled(&write, "Nothing was exported."),
}
}
pub(crate) fn report_file_write_cancelled(&mut self, write: &PendingFileWrite, reason: &str) {
match write {
PendingFileWrite::Processing(_) => self.inner.set_status(reason.to_string()),
PendingFileWrite::Export(_) => {
self.inner.cancel_pending_export();
self.inner.set_status(reason.to_string());
}
}
}
pub(crate) fn write_data_file(
&mut self,
write: PendingFileWrite,
path: std::path::PathBuf,
) -> bool {
match std::fs::write(&path, write.bytes()) {
Ok(()) => {
tracing::info!(
path = %path.display(),
"OxiGIS desktop: data file written",
);
self.note_dialog_directory(&path);
match &write {
PendingFileWrite::Processing(request) => {
let features = request.features;
let plural = if features == 1 { "feature" } else { "features" };
self.inner
.set_status(format!("Wrote {} ({features} {plural}).", path.display()));
}
PendingFileWrite::Export(_) => self.inner.confirm_export_written(&path),
}
true
}
Err(error) => {
let reason = format!("could not write {}: {error}", path.display());
match &write {
PendingFileWrite::Processing(_) => self.inner.set_status(reason),
PendingFileWrite::Export(_) => self.inner.report_export_failed(&reason),
}
self.pending_file_write = Some(write);
false
}
}
}
}
#[cfg(test)]
mod tests {
use super::PendingFileWrite;
use crate::file_dialog::Ask;
use crate::{OxigisDesktopApp, session};
fn scratch_dir(label: &str) -> std::path::PathBuf {
let stamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |elapsed| elapsed.as_nanos());
let path = std::env::temp_dir().join(format!("oxigis-file-write-{label}-{stamp}"));
std::fs::create_dir_all(&path).expect("the scratch directory is creatable");
path
}
fn shell() -> OxigisDesktopApp {
OxigisDesktopApp::new(Vec::new(), session::SessionState::default())
}
fn export() -> oxigis_ui::ExportRequest {
oxigis_ui::ExportRequest {
suggested_file_name: "cities.geojson".to_string(),
content: r#"{"type":"FeatureCollection","features":[]}"#.to_string(),
kind: oxigis_ui::ExportKind::GeoJson,
}
}
fn processing() -> oxigis_ui::ProcessingFileRequest {
oxigis_ui::ProcessingFileRequest {
name: "buffer_result".to_string(),
content: r#"{"type":"FeatureCollection","features":[]}"#.to_string(),
features: 3,
}
}
#[test]
fn the_ask_and_the_suggested_name_follow_the_seam_and_its_format() {
assert_eq!(
PendingFileWrite::Export(export()).ask(),
Ask::SaveGeoJson,
"a GeoJSON export asks for a GeoJSON destination"
);
let csv = oxigis_ui::ExportRequest {
suggested_file_name: "cities.csv".to_string(),
kind: oxigis_ui::ExportKind::Csv,
..export()
};
assert_eq!(PendingFileWrite::Export(csv).ask(), Ask::SaveCsv);
let write = PendingFileWrite::Processing(processing());
assert_eq!(write.ask(), Ask::SaveGeoJson);
assert_eq!(
write.suggested_name(),
"buffer_result.geojson",
"the seam carries a basename; choosing the extension is the writer's job"
);
}
#[test]
fn an_export_is_written_and_reported_through_the_apps_own_confirmation() {
let dir = scratch_dir("export");
let mut app = shell();
let path = dir.join("cities.geojson");
assert!(app.write_data_file(PendingFileWrite::Export(export()), path.clone()));
assert_eq!(
std::fs::read_to_string(&path).expect("the file is on disk"),
export().content,
"the bytes the app parked are the bytes on disk"
);
let status = app.inner.status().unwrap_or_default().to_string();
assert!(status.starts_with("Exported "), "{status}");
assert!(status.contains("cities.geojson"), "{status}");
assert!(
app.pending_file_write.is_none(),
"a settled write leaves nothing parked"
);
let _removed = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_processing_result_is_written_and_counts_its_features() {
let dir = scratch_dir("processing");
let mut app = shell();
let path = dir.join("buffer_result.geojson");
assert!(app.write_data_file(PendingFileWrite::Processing(processing()), path.clone()));
assert_eq!(
std::fs::read_to_string(&path).expect("the file is on disk"),
processing().content
);
let status = app.inner.status().unwrap_or_default().to_string();
assert!(status.starts_with("Wrote "), "{status}");
assert!(
status.contains("(3 features)"),
"the seam's own count reaches the user: {status}"
);
let _removed = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_failed_write_re_parks_the_bytes_so_a_corrected_path_still_saves_them() {
let dir = scratch_dir("refused");
let mut app = shell();
let occupied = dir.join("occupied.geojson");
std::fs::create_dir(&occupied).expect("the directory is creatable");
assert!(
!app.write_data_file(PendingFileWrite::Export(export()), occupied),
"a failed write must not close the prompt"
);
let status = app.inner.status().unwrap_or_default().to_string();
assert!(status.starts_with("Export failed: "), "{status}");
assert!(
app.pending_file_write.is_some(),
"the bytes stay parked for a corrected path"
);
let good = dir.join("cities.geojson");
let parked = app.pending_file_write.take().expect("just asserted");
assert!(app.write_data_file(parked, good.clone()));
assert_eq!(
std::fs::read_to_string(&good).expect("the file is on disk"),
export().content
);
let _removed = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_refused_export_says_why_and_not_merely_that_it_was_cancelled() {
let mut app = shell();
app.report_file_write_cancelled(
&PendingFileWrite::Export(export()),
"Finish the file prompt that is already open first.",
);
let status = app.inner.status().unwrap_or_default().to_string();
assert_eq!(status, "Finish the file prompt that is already open first.");
}
#[test]
fn a_prompt_already_on_screen_refuses_the_write_rather_than_stacking_it() {
let mut app = shell();
app.path_prompt = Some(crate::file_dialog::PathPrompt::new(
Ask::SaveProject,
"project.oxigis.json",
std::env::temp_dir(),
));
app.resolve_file_write(PendingFileWrite::Export(export()));
assert!(
app.pending_file_write.is_none(),
"the request is refused, not parked behind the other prompt"
);
let status = app.inner.status().unwrap_or_default().to_string();
assert!(status.contains("already open"), "{status}");
}
}