Files
gpx.studio/gpx-rs/src/engine/command/file/load.rs
T

233 lines
6.9 KiB
Rust
Raw Normal View History

2026-10-04 19:14:41 +02:00
use crate::{Apply, CommandError, File, State, TrackpointCategories, parse, produce};
2026-10-03 16:40:17 +02:00
#[derive(Debug)]
pub struct Load<'a> {
pub data: &'a [u8],
2026-10-04 11:07:12 +02:00
/// Name of the file when the data has none (typically the name of the file on disk, without
/// its extension).
pub name: &'a str,
2026-10-03 16:40:17 +02:00
}
2026-10-04 16:11:44 +02:00
impl Load<'_> {
2026-10-04 19:14:41 +02:00
fn parse(self, categories: &mut TrackpointCategories) -> Result<File, CommandError> {
let mut file = parse(self.data, categories)
.map_err(|err| CommandError::InvalidData(err.to_string()))?;
2026-10-04 11:07:12 +02:00
if file.info.name.trim().is_empty() {
file.info.name = self.name.to_owned();
}
2026-10-04 16:11:44 +02:00
Ok(file)
}
}
impl Apply for Load<'_> {
fn apply(self, state: &mut State) -> Result<(), CommandError> {
LoadFiles { files: vec![self] }.apply(state)
}
}
/// Loads several files at once, as a single command (one undo step): the files are added at the
/// end of the list, in the given order, and the first one is selected.
///
/// The files that cannot be read are skipped, unless none of them can: then nothing is loaded
/// and the error is the one of the first file.
#[derive(Debug)]
pub struct LoadFiles<'a> {
pub files: Vec<Load<'a>>,
}
impl Apply for LoadFiles<'_> {
fn apply(self, state: &mut State) -> Result<(), CommandError> {
let mut files = Vec::with_capacity(self.files.len());
let mut first_error = None;
for load in self.files {
2026-10-04 19:14:41 +02:00
match load.parse(state.categories) {
2026-10-04 16:11:44 +02:00
Ok(file) => files.push(file),
Err(error) => {
first_error.get_or_insert(error);
}
}
}
if files.is_empty() {
return Err(first_error.unwrap_or(CommandError::NothingToDo));
}
produce(state, |_| files);
2026-10-03 16:40:17 +02:00
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::engine::command::fixture::Fixture;
use super::*;
#[test]
fn test_load() {
let mut fx = Fixture::default();
let data = std::fs::read("data/with_tracks.gpx").unwrap();
2026-10-04 11:07:12 +02:00
assert!(
Load {
data: &data,
name: "file",
}
.apply(&mut fx.state())
.is_ok()
);
2026-10-03 16:40:17 +02:00
assert_eq!(fx.files.len(), 1);
let file = fx.files.values().next().unwrap();
assert!(!file.trk.is_empty());
assert_eq!(fx.selected_files(), [file.id].into());
}
#[test]
fn test_load_invalid_file_changes_nothing() {
let mut fx = Fixture::default();
2026-10-07 17:58:17 +02:00
crate::New {
name: "keep",
trackpoint: None,
}
.apply(&mut fx.state())
.unwrap();
2026-10-03 16:40:17 +02:00
let selected = fx.selected_files();
let result = Load {
data: b"<gpx><trk></gpx>",
2026-10-04 11:07:12 +02:00
name: "file",
2026-10-03 16:40:17 +02:00
}
.apply(&mut fx.state());
assert!(matches!(result, Err(CommandError::InvalidData(_))));
assert_eq!(fx.files.len(), 1);
assert_eq!(fx.order.0.len(), 1);
assert_eq!(fx.selected_files(), selected);
}
2026-10-04 11:07:12 +02:00
fn load_with_name(data: &[u8]) -> String {
let mut fx = Fixture::default();
Load {
data,
name: "from disk",
}
.apply(&mut fx.state())
.unwrap();
fx.files.values().next().unwrap().info.name.clone()
}
#[test]
fn test_load_keeps_the_name_of_the_file() {
let data = br#"<gpx version="1.1"><metadata><name>in the file</name></metadata></gpx>"#;
assert_eq!(load_with_name(data), "in the file");
}
#[test]
fn test_load_without_name_uses_the_given_name() {
let without = br#"<gpx version="1.1"><trk><trkseg></trkseg></trk></gpx>"#;
assert_eq!(load_with_name(without), "from disk");
let empty = br#"<gpx version="1.1"><metadata><name> </name></metadata></gpx>"#;
assert_eq!(load_with_name(empty), "from disk");
}
2026-10-04 16:11:44 +02:00
fn gpx(name: &str) -> Vec<u8> {
format!(r#"<gpx version="1.1"><metadata><name>{name}</name></metadata></gpx>"#).into_bytes()
}
fn load<'a>(data: &'a [u8], name: &'a str) -> Load<'a> {
Load { data, name }
}
#[test]
fn test_load_several_files() {
let mut fx = Fixture::default();
2026-10-07 17:58:17 +02:00
crate::New {
name: "before",
trackpoint: None,
}
.apply(&mut fx.state())
.unwrap();
2026-10-04 16:11:44 +02:00
let before = fx.order.0[0];
let (a, b, c) = (gpx("a"), gpx("b"), gpx("c"));
LoadFiles {
files: vec![load(&a, "x"), load(&b, "x"), load(&c, "x")],
}
.apply(&mut fx.state())
.unwrap();
// added after the others, in the order they were given
let names: Vec<_> = fx
.order
.0
.iter()
.map(|id| fx.files[id].info.name.clone())
.collect();
assert_eq!(names, ["before", "a", "b", "c"]);
assert_eq!(fx.order.0[0], before);
// the first one is selected
assert_eq!(fx.selected_files(), [fx.order.0[1]].into());
}
#[test]
fn test_load_several_files_skips_the_invalid_ones() {
let mut fx = Fixture::default();
let (a, c) = (gpx("a"), gpx("c"));
LoadFiles {
files: vec![load(&a, "x"), load(b"<gpx><trk></gpx>", "x"), load(&c, "x")],
}
.apply(&mut fx.state())
.unwrap();
let names: Vec<_> = fx
.order
.0
.iter()
.map(|id| fx.files[id].info.name.clone())
.collect();
assert_eq!(names, ["a", "c"]);
}
#[test]
fn test_load_several_invalid_files_changes_nothing() {
let mut fx = Fixture::default();
2026-10-07 17:58:17 +02:00
crate::New {
name: "keep",
trackpoint: None,
}
.apply(&mut fx.state())
.unwrap();
2026-10-04 16:11:44 +02:00
let selected = fx.selected_files();
let result = LoadFiles {
files: vec![
load(b"<gpx><trk></gpx>", "x"),
load(b"<gpx><wpt></gpx>", "y"),
],
}
.apply(&mut fx.state());
assert!(matches!(result, Err(CommandError::InvalidData(_))));
assert_eq!(fx.files.len(), 1);
assert_eq!(fx.order.0.len(), 1);
assert_eq!(fx.selected_files(), selected);
// nothing to load
assert_eq!(
LoadFiles { files: vec![] }.apply(&mut fx.state()),
Err(CommandError::NothingToDo)
);
}
#[test]
fn test_each_file_without_name_gets_its_own_name() {
let mut fx = Fixture::default();
let unnamed = br#"<gpx version="1.1"><trk><trkseg></trkseg></trk></gpx>"#;
LoadFiles {
files: vec![load(unnamed, "first"), load(unnamed, "second")],
}
.apply(&mut fx.state())
.unwrap();
let names: Vec<_> = fx
.order
.0
.iter()
.map(|id| fx.files[id].info.name.clone())
.collect();
assert_eq!(names, ["first", "second"]);
}
2026-10-03 16:40:17 +02:00
}