diff --git a/src/input.rs b/src/input.rs index 400bdb19..0e185b20 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,7 +1,7 @@ //! Common routines for handling input data. use crate::agent::AssetPool; use crate::model::{Model, ModelFile}; -use anyhow::{ensure, Context, Result}; +use anyhow::{bail, ensure, Context, Result}; use float_cmp::approx_eq; use indexmap::IndexMap; use itertools::Itertools; @@ -26,19 +26,41 @@ pub use time_slice::read_time_slice_info; /// Read a series of type `T`s from a CSV file. /// +/// Will raise an error if the file is empty. +/// /// # Arguments /// /// * `file_path` - Path to the CSV file pub fn read_csv<'a, T: DeserializeOwned + 'a>( file_path: &'a Path, ) -> Result + 'a> { + let vec = _read_csv_internal(file_path)?; + if vec.is_empty() { + bail!("CSV file {} cannot be empty", file_path.display()); + } + Ok(vec.into_iter()) +} + +/// Read a series of type `T`s from a CSV file. +/// +/// # Arguments +/// +/// * `file_path` - Path to the CSV file +pub fn read_csv_optional<'a, T: DeserializeOwned + 'a>( + file_path: &'a Path, +) -> Result + 'a> { + let vec = _read_csv_internal(file_path)?; + Ok(vec.into_iter()) +} + +fn _read_csv_internal<'a, T: DeserializeOwned + 'a>(file_path: &'a Path) -> Result> { let vec = csv::Reader::from_path(file_path) .with_context(|| input_err_msg(file_path))? .into_deserialize() .process_results(|iter| iter.collect_vec()) .with_context(|| input_err_msg(file_path))?; - Ok(vec.into_iter()) + Ok(vec) } /// Parse a TOML file at the specified path. @@ -285,6 +307,14 @@ mod tests { } ] ); + + // File with no data (only column headers) + let file_path = create_csv_file(dir.path(), "id,value\n"); + assert!(read_csv::(&file_path).is_err()); + assert!(read_csv_optional::(&file_path) + .unwrap() + .next() + .is_none()); } #[test] diff --git a/src/input/agent/search_space.rs b/src/input/agent/search_space.rs index 0f19f805..ad1ac477 100644 --- a/src/input/agent/search_space.rs +++ b/src/input/agent/search_space.rs @@ -81,7 +81,7 @@ pub fn read_agent_search_space( milestone_years: &[u32], ) -> Result, Vec>> { let file_path = model_dir.join(AGENT_SEARCH_SPACE_FILE_NAME); - let iter = read_csv::(&file_path)?; + let iter = read_csv_optional::(&file_path)?; read_agent_search_space_from_iter(iter, agents, process_ids, commodities, milestone_years) .with_context(|| input_err_msg(&file_path)) }