diff --git a/Cargo.toml b/Cargo.toml index 42e288a..9454fc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,10 +29,10 @@ process_path = "0.1.4" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" quick-xml = { version = "0.38", features = ["serialize"], optional = true } -anyhow = "1" chrono = { version = "0.4", optional = true } [dev-dependencies] +anyhow = "1" rand = "0.9.2" approx = "0.5" rand_distr = "0.5.1" diff --git a/python/src/mod_utils.rs b/python/src/mod_utils.rs index e8ad7ef..49183e8 100644 --- a/python/src/mod_utils.rs +++ b/python/src/mod_utils.rs @@ -50,7 +50,8 @@ fn update_datafiles(kwds: Option<&Bound<'_, PyDict>>) -> Result<()> { }, }; - satkit::utils::update_datafiles(datadir, overwrite_files) + satkit::utils::update_datafiles(datadir, overwrite_files)?; + Ok(()) } /// Get directory where astronomy data is stored diff --git a/python/src/pysgp4.rs b/python/src/pysgp4.rs index 888ea55..acd707f 100644 --- a/python/src/pysgp4.rs +++ b/python/src/pysgp4.rs @@ -389,12 +389,12 @@ pub fn sgp4( let tmarray = time.to_time_vec()?; let results: Vec = plist .iter() - .map(|item| { + .map(|item| -> Result { if item.is_instance_of::() { let mut stle: PyRefMut = item.extract().map_err(|e| { pyo3::exceptions::PyValueError::new_err(format!("Invalid TLE: {}", e)) })?; - psgp4::sgp4(&mut stle.0, tmarray.as_slice()) + Ok(psgp4::sgp4(&mut stle.0, tmarray.as_slice())?) } else if item.is_instance_of::() { let dict: &Bound<'_, PyDict> = item.cast().map_err(|e| { pyo3::exceptions::PyValueError::new_err(format!( @@ -403,7 +403,7 @@ pub fn sgp4( )) })?; let mut omm = omm_from_pydict(dict)?; - psgp4::sgp4(&mut omm, tmarray.as_slice()) + Ok(psgp4::sgp4(&mut omm, tmarray.as_slice())?) } else { bail!("Invalid TLE in list"); } diff --git a/src/earth_orientation_params.rs b/src/earth_orientation_params.rs index ee76d85..e059fde 100644 --- a/src/earth_orientation_params.rs +++ b/src/earth_orientation_params.rs @@ -73,16 +73,8 @@ pub enum Error { #[error(transparent)] Datadir(#[from] crate::utils::datadir::Error), - /// Wraps an [`anyhow::Error`] surfaced by the (still-anyhow) download - /// helpers in [`crate::utils::download`]. - #[error("Download failed: {0}")] - Download(anyhow::Error), -} - -impl From for Error { - fn from(e: anyhow::Error) -> Self { - Self::Download(e) - } + #[error(transparent)] + Download(#[from] crate::utils::download::Error), } /// Convenient type alias used throughout the diff --git a/src/earthgravity.rs b/src/earthgravity.rs index 3047980..9236951 100644 --- a/src/earthgravity.rs +++ b/src/earthgravity.rs @@ -33,16 +33,8 @@ pub enum Error { #[error(transparent)] Datadir(#[from] crate::utils::datadir::Error), - /// Wraps an [`anyhow::Error`] surfaced by the (still-anyhow) download - /// helpers in [`crate::utils::download`]. - #[error("Download failed: {0}")] - Download(anyhow::Error), -} - -impl From for Error { - fn from(e: anyhow::Error) -> Self { - Self::Download(e) - } + #[error(transparent)] + Download(#[from] crate::utils::download::Error), } /// Convenient type alias used throughout the `earthgravity` module. diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..4b7020b --- /dev/null +++ b/src/error.rs @@ -0,0 +1,80 @@ +//! Top-level error type for the satkit crate. +//! +//! Most functions return module-scoped error types +//! (e.g. [`tle::Error`](crate::tle::Error), +//! [`orbitprop::Error`](crate::orbitprop::Error)). For downstream apps +//! that consume multiple modules and don't want to define their own +//! outer error, this façade has `From` impls for every public module +//! error and can be used as a single result type. +//! +//! ```rust,ignore +//! fn do_thing() -> Result<(), satkit::Error> { +//! let tle = satkit::TLE::from_url(url)?; // tle::Error +//! let states = satkit::orbitprop::propagate(...)?; // orbitprop::Error +//! Ok(()) +//! } +//! ``` + +use thiserror::Error; + +/// Top-level satkit error covering every public module-scoped error. +#[derive(Debug, Error)] +pub enum Error { + #[error(transparent)] + Tle(#[from] crate::tle::Error), + + #[error(transparent)] + Omm(#[from] crate::omm::Error), + + #[error(transparent)] + Frames(#[from] crate::frames::Error), + + #[error(transparent)] + Itrfcoord(#[from] crate::itrfcoord::Error), + + #[error(transparent)] + Orbitprop(#[from] crate::orbitprop::Error), + + #[error(transparent)] + Time(#[from] crate::time::InstantError), + + #[error(transparent)] + Kepler(#[from] crate::kepler::Error), + + #[error(transparent)] + Sgp4(#[from] crate::sgp4::Error), + + #[error(transparent)] + Frametransform(#[from] crate::frametransform::Error), + + #[error(transparent)] + SpaceWeather(#[from] crate::spaceweather::Error), + + #[error(transparent)] + SolarCycleForecast(#[from] crate::solar_cycle_forecast::Error), + + #[error(transparent)] + EarthOrientationParams(#[from] crate::earth_orientation_params::Error), + + #[error(transparent)] + JplEphem(#[from] crate::jplephem::Error), + + #[error(transparent)] + EarthGravity(#[from] crate::earthgravity::Error), + + #[error(transparent)] + LpEphem(#[from] crate::lpephem::Error), + + #[error(transparent)] + Datadir(#[from] crate::utils::datadir::Error), + + #[error(transparent)] + Download(#[from] crate::utils::download::Error), + + #[cfg(feature = "download")] + #[error(transparent)] + UpdateData(#[from] crate::utils::update_data::Error), +} + +/// Convenient type alias used throughout satkit. +pub type Result = std::result::Result; diff --git a/src/frametransform/error.rs b/src/frametransform/error.rs new file mode 100644 index 0000000..4f5182d --- /dev/null +++ b/src/frametransform/error.rs @@ -0,0 +1,52 @@ +//! Errors produced by the `frametransform` module. + +use std::num::{ParseFloatError, ParseIntError}; + +use thiserror::Error; + +use crate::Frame; + +/// Errors produced by the +/// [`frametransform`](crate::frametransform) module. +#[derive(Debug, Error)] +pub enum Error { + /// [`to_gcrf`](super::to_gcrf) and [`from_gcrf`](super::from_gcrf) + /// only build rotation matrices for satellite-local orbital frames + /// (`GCRF`, `LVLH`, `RTN`, `NTW`). Time-dependent inertial / + /// Earth-fixed frames must use the dedicated quaternion helpers + /// ([`qitrf2gcrf`](super::qitrf2gcrf), + /// [`qteme2gcrf`](super::qteme2gcrf), …). + #[error( + "to_gcrf: frame {frame} is not a satellite-local orbital frame; use the \ + time-based quaternion helpers (qitrf2gcrf, qteme2gcrf, etc.) instead" + )] + UnsupportedFrame { frame: Frame }, + + /// A `j = N` table-definition line in an IERS table file is + /// malformed. + #[error("Error parsing file {fname}, invalid table definition line")] + InvalidIersTableDef { fname: String }, + + /// Encountered a coefficient row in an IERS table file before any + /// table dimension was declared. + #[error("Error parsing file {fname}, table not initialized")] + IersTableNotInitialized { fname: String }, + + #[error(transparent)] + Io(#[from] std::io::Error), + + #[error(transparent)] + ParseInt(#[from] ParseIntError), + + #[error(transparent)] + ParseFloat(#[from] ParseFloatError), + + #[error(transparent)] + Datadir(#[from] crate::utils::datadir::Error), + + #[error(transparent)] + Download(#[from] crate::utils::download::Error), +} + +/// Convenient type alias used throughout the `frametransform` module. +pub type Result = std::result::Result; diff --git a/src/frametransform/ierstable.rs b/src/frametransform/ierstable.rs index 00c7417..b506ea8 100644 --- a/src/frametransform/ierstable.rs +++ b/src/frametransform/ierstable.rs @@ -1,6 +1,6 @@ use crate::utils::{self, download_if_not_exist}; -use anyhow::Result; +use super::{Error, Result}; use crate::mathtypes::*; @@ -54,17 +54,18 @@ impl IERSTable { let s: Vec<&str> = tline.split_whitespace().collect(); let tsize: usize = s[s.len() - 1].parse().unwrap_or(0); if !(0..=5).contains(&tnum) || tsize == 0 { - anyhow::bail!( - "Error parsing file {}, invalid table definition line", - fname - ); + return Err(Error::InvalidIersTableDef { + fname: fname.to_string(), + }); } table.data[tnum as usize] = DMatrix::::zeros(tsize, 17); rowcnt = 0; continue; } else if tnum >= 0 { if table.data[tnum as usize].ncols() < 17 { - anyhow::bail!("Error parsing file {}, table not initialized", fname); + return Err(Error::IersTableNotInitialized { + fname: fname.to_string(), + }); } let vals: Vec = tline.split_whitespace().map(|x| x.parse().unwrap()).collect(); for (c, &val) in vals.iter().enumerate() { diff --git a/src/frametransform/mod.rs b/src/frametransform/mod.rs index 5d5ad0f..3a2368a 100644 --- a/src/frametransform/mod.rs +++ b/src/frametransform/mod.rs @@ -25,9 +25,12 @@ //! - **CIRS** (Celestial Intermediate Reference System): IERS 2010 intermediate frame //! - **MOD** (Mean of Date): Precession-only frame +mod error; mod ierstable; mod qcirs2gcrs; +pub use error::{Error, Result}; + use crate::{TimeLike, TimeScale}; use std::f64::consts::PI; @@ -818,7 +821,7 @@ pub fn to_gcrf( frame: crate::Frame, pos_gcrf: &Vector3, vel_gcrf: &Vector3, -) -> anyhow::Result { +) -> Result { use crate::Frame; match frame { Frame::GCRF => Ok(Matrix3::eye()), @@ -830,11 +833,7 @@ pub fn to_gcrf( | Frame::CIRS | Frame::TEME | Frame::EME2000 - | Frame::ICRF => anyhow::bail!( - "to_gcrf: frame {} is not a satellite-local orbital frame; use the \ - time-based quaternion helpers (qitrf2gcrf, qteme2gcrf, etc.) instead", - frame - ), + | Frame::ICRF => Err(Error::UnsupportedFrame { frame }), } } @@ -847,7 +846,7 @@ pub fn from_gcrf( frame: crate::Frame, pos_gcrf: &Vector3, vel_gcrf: &Vector3, -) -> anyhow::Result { +) -> Result { Ok(to_gcrf(frame, pos_gcrf, vel_gcrf)?.transpose()) } diff --git a/src/jplephem.rs b/src/jplephem.rs index 0d98cfd..0e8b38d 100644 --- a/src/jplephem.rs +++ b/src/jplephem.rs @@ -74,16 +74,8 @@ pub enum Error { #[error(transparent)] Datadir(#[from] crate::utils::datadir::Error), - /// Wraps an [`anyhow::Error`] surfaced by the (still-anyhow) download - /// helpers in [`crate::utils::download`]. - #[error("Download failed: {0}")] - Download(anyhow::Error), -} - -impl From for Error { - fn from(e: anyhow::Error) -> Self { - Self::Download(e) - } + #[error(transparent)] + Download(#[from] crate::utils::download::Error), } /// Convenient type alias used throughout the `jplephem` module. diff --git a/src/lib.rs b/src/lib.rs index 5d7f2b3..5dfab4b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -216,7 +216,11 @@ pub mod omm; // Time and duration mod time; -pub use time::{Duration, Instant, TimeLike, TimeScale, Weekday}; +pub use time::{Duration, Instant, InstantError, TimeLike, TimeScale, Weekday}; + +// Top-level façade error type +mod error; +pub use error::{Error, Result}; // Core types available at crate level pub use frames::Frame; diff --git a/src/omm/mod.rs b/src/omm/mod.rs index ec93732..85182bb 100644 --- a/src/omm/mod.rs +++ b/src/omm/mod.rs @@ -335,7 +335,7 @@ impl OMM { /// let omms = OMM::from_json_string(json)?; /// assert_eq!(omms.len(), 1); /// assert_eq!(omms[0].object_id, "1998-067A"); - /// # Ok::<(), anyhow::Error>(()) + /// # Ok::<(), satkit::omm::Error>(()) /// ``` /// /// # Errors @@ -357,7 +357,7 @@ impl OMM { /// /// let omms = OMM::from_json_file("/path/to/omm.json")?; /// println!("Loaded {} OMM records", omms.len()); - /// # Ok::<(), anyhow::Error>(()) + /// # Ok::<(), satkit::omm::Error>(()) /// ``` /// /// # Errors @@ -423,24 +423,28 @@ impl SGP4Source for OMM { &mut self.satrec } - fn sgp4_init_args(&self) -> anyhow::Result { + fn sgp4_init_args(&self) -> crate::sgp4::Result { use std::f64::consts::PI; const TWOPI: f64 = PI * 2.0; if let Some(theory) = &self.mean_element_theory { if !theory.trim().eq_ignore_ascii_case("SGP4") { - return Err(Error::UnsupportedMeanElementTheory(theory.clone()).into()); + return Err(crate::sgp4::Error::source( + Error::UnsupportedMeanElementTheory(theory.clone()), + )); } } if let Some(ts) = &self.time_system { if !ts.trim().eq_ignore_ascii_case("UTC") { - return Err(Error::UnsupportedTimeSystem(ts.clone()).into()); + return Err(crate::sgp4::Error::source(Error::UnsupportedTimeSystem( + ts.clone(), + ))); } } - let epoch = self.epoch_instant()?; + let epoch = self.epoch_instant().map_err(crate::sgp4::Error::source)?; Ok(SGP4InitArgs { jdsatepoch: epoch.as_jd_with_scale(TimeScale::UTC), diff --git a/src/omm/xml.rs b/src/omm/xml.rs index 88f92b3..4273448 100644 --- a/src/omm/xml.rs +++ b/src/omm/xml.rs @@ -293,7 +293,7 @@ impl OMM { /// assert_eq!(omms.len(), 1); /// assert_eq!(omms[0].object_id, "1998-067A"); /// # } - /// # Ok::<(), anyhow::Error>(()) + /// # Ok::<(), satkit::omm::Error>(()) /// ``` /// /// # Errors diff --git a/src/orbitprop/error.rs b/src/orbitprop/error.rs index 8dade6f..d16ad42 100644 --- a/src/orbitprop/error.rs +++ b/src/orbitprop/error.rs @@ -49,12 +49,10 @@ pub enum Error { }, /// Wraps an error surfaced while building a - /// [`Precomputed`](crate::orbitprop::Precomputed) interp table. - /// Currently captures stringified errors from the still-anyhow - /// `jplephem` module; will become a typed variant when that module - /// is migrated in Phase 3. - #[error("Cannot compute precomputed interpolation data: {0}")] - Precompute(String), + /// [`Precomputed`](crate::orbitprop::Precomputed) interp table from + /// JPL ephemeris data. + #[error(transparent)] + Jplephem(#[from] crate::jplephem::Error), // -- satstate.rs ----------------------------------------------------- /// Returned by [`SatState::set_pos_uncertainty`](crate::orbitprop::SatState::set_pos_uncertainty), diff --git a/src/orbitprop/precomputed.rs b/src/orbitprop/precomputed.rs index 2b5db5e..ae9cf49 100644 --- a/src/orbitprop/precomputed.rs +++ b/src/orbitprop/precomputed.rs @@ -83,10 +83,8 @@ impl Precomputed { for idx in 0..nsteps { let t = pbegin + Duration::from_seconds((idx as f64) * step); let q = qgcrf2itrf_approx(&t); - let psun = jplephem::geocentric_pos(SolarSystem::Sun, &t) - .map_err(|e| Error::Precompute(e.to_string()))?; - let pmoon = jplephem::geocentric_pos(SolarSystem::Moon, &t) - .map_err(|e| Error::Precompute(e.to_string()))?; + let psun = jplephem::geocentric_pos(SolarSystem::Sun, &t)?; + let pmoon = jplephem::geocentric_pos(SolarSystem::Moon, &t)?; data.push((q, psun, pmoon)); } data diff --git a/src/sgp4/error.rs b/src/sgp4/error.rs new file mode 100644 index 0000000..fcbcc6d --- /dev/null +++ b/src/sgp4/error.rs @@ -0,0 +1,36 @@ +//! Errors produced by the `sgp4` module. + +use thiserror::Error; + +/// Errors that can occur while initialising or evaluating SGP4. +#[derive(Debug, Error)] +pub enum Error { + /// `sgp4init` returned a non-zero error code while constructing the + /// internal `SatRec`. The numeric code matches the legacy Vallado + /// convention (1 = eccentricity, 2 = mean motion, 3 = perturbed + /// eccentricity, 4 = semi-latus rectum, 6 = orbit decay). + #[error("SGP4 init error code {0}")] + SatRecInit(i32), + + /// Wraps an error surfaced by an [`SGP4Source`](super::SGP4Source) + /// implementation while building [`SGP4InitArgs`](super::SGP4InitArgs) + /// — for example an `OMM` with an unsupported mean-element theory or + /// a malformed epoch. + #[error(transparent)] + Source(Box), +} + +impl Error { + /// Wrap an arbitrary `std::error::Error` value as an + /// [`Error::Source`] without an explicit `Box::new` at the call + /// site. + pub fn source(e: E) -> Self + where + E: std::error::Error + Send + Sync + 'static, + { + Self::Source(Box::new(e)) + } +} + +/// Convenient type alias used throughout the `sgp4` module. +pub type Result = std::result::Result; diff --git a/src/sgp4/mod.rs b/src/sgp4/mod.rs index 5a2eccb..131c5a3 100644 --- a/src/sgp4/mod.rs +++ b/src/sgp4/mod.rs @@ -32,6 +32,7 @@ mod dpper; mod dscom; mod dsinit; mod dspace; +mod error; mod getgravconst; mod initl; pub mod satrec; @@ -39,6 +40,7 @@ mod sgp4_impl; mod sgp4_lowlevel; mod sgp4init; +pub use error::{Error, Result}; pub use sgp4_impl::sgp4; pub use sgp4_impl::sgp4_full; pub use sgp4_impl::SGP4Error; @@ -78,5 +80,5 @@ pub trait SGP4Source { fn satrec_mut(&mut self) -> &mut Option; /// Produce canonical SGP4 initialization arguments. - fn sgp4_init_args(&self) -> anyhow::Result; + fn sgp4_init_args(&self) -> Result; } diff --git a/src/sgp4/sgp4_impl.rs b/src/sgp4/sgp4_impl.rs index 89879ce..bad2781 100644 --- a/src/sgp4/sgp4_impl.rs +++ b/src/sgp4/sgp4_impl.rs @@ -129,7 +129,10 @@ use super::{GravConst, OpsMode, SGP4Source}; /// ``` /// #[inline] -pub fn sgp4(sgp4source: &mut impl SGP4Source, tm: &[T]) -> anyhow::Result { +pub fn sgp4( + sgp4source: &mut impl SGP4Source, + tm: &[T], +) -> super::Result { sgp4_full(sgp4source, tm, GravConst::WGS84, OpsMode::IMPROVED) } @@ -206,25 +209,28 @@ pub fn sgp4_full( tm: &[T], gravconst: GravConst, opsmode: OpsMode, -) -> anyhow::Result { +) -> super::Result { if sgp4source.satrec_mut().is_none() { let args = sgp4source.sgp4_init_args()?; - *sgp4source.satrec_mut() = Some(sgp4init( - gravconst, - opsmode, - "satno", - args.jdsatepoch - 2433281.5, - args.bstar, - args.ndot, - args.nddot, - args.ecco, - args.argpo, - args.inclo, - args.mo, - args.no, - args.nodeo, - ).map_err(|e| anyhow::anyhow!("SGP4 init error: {}", e))?); + *sgp4source.satrec_mut() = Some( + sgp4init( + gravconst, + opsmode, + "satno", + args.jdsatepoch - 2433281.5, + args.bstar, + args.ndot, + args.nddot, + args.ecco, + args.argpo, + args.inclo, + args.mo, + args.no, + args.nodeo, + ) + .map_err(super::Error::SatRecInit)?, + ); } let epoch = sgp4source.epoch(); @@ -345,8 +351,7 @@ mod tests { if tle.sat_num == 33334 { continue; } - return Err(e); - + return Err(e.into()); } }; if states.errcode[0] != SGP4Error::SGP4Success { diff --git a/src/solar_cycle_forecast.rs b/src/solar_cycle_forecast.rs index d98bce8..0af353f 100644 --- a/src/solar_cycle_forecast.rs +++ b/src/solar_cycle_forecast.rs @@ -58,22 +58,14 @@ pub enum Error { #[error(transparent)] ParseInt(#[from] ParseIntError), - /// Wraps an [`anyhow::Error`] surfaced by the (still-anyhow) download - /// helpers in [`crate::utils::download`]. - #[error("Download failed: {0}")] - Download(anyhow::Error), + #[error(transparent)] + Download(#[from] crate::utils::download::Error), /// The crate was built without the `download` feature enabled. #[error("satkit was built without the `download` feature")] DownloadFeatureDisabled, } -impl From for Error { - fn from(e: anyhow::Error) -> Self { - Self::Download(e) - } -} - /// Convenient type alias used throughout the `solar_cycle_forecast` module. pub type Result = std::result::Result; diff --git a/src/spaceweather.rs b/src/spaceweather.rs index 08fde8e..4de0ac8 100644 --- a/src/spaceweather.rs +++ b/src/spaceweather.rs @@ -42,16 +42,8 @@ pub enum Error { #[error(transparent)] Datadir(#[from] crate::utils::datadir::Error), - /// Wraps an [`anyhow::Error`] surfaced by the (still-anyhow) download - /// helpers in [`crate::utils::download`]. - #[error("Download failed: {0}")] - Download(anyhow::Error), -} - -impl From for Error { - fn from(e: anyhow::Error) -> Self { - Self::Download(e) - } + #[error(transparent)] + Download(#[from] crate::utils::download::Error), } /// Convenient type alias used throughout the `spaceweather` module. diff --git a/src/tle/error.rs b/src/tle/error.rs index 5d51daa..332cf09 100644 --- a/src/tle/error.rs +++ b/src/tle/error.rs @@ -38,8 +38,8 @@ pub enum Error { /// Wraps an error from constructing an [`Instant`](crate::time::Instant) /// while assembling a TLE epoch. - #[error("Invalid TLE epoch: {0}")] - InvalidEpoch(String), + #[error(transparent)] + InvalidEpoch(#[from] crate::time::InstantError), #[error("States and times must have the same length")] StatesTimesLengthMismatch, @@ -50,8 +50,8 @@ pub enum Error { #[error("Epoch is out of range. Must be between {min} and {max}")] EpochOutOfRange { min: String, max: String }, - #[error("Could not convert state to Keplerian elements: {0}")] - KeplerConversion(String), + #[error(transparent)] + Kepler(#[from] crate::kepler::Error), #[error("SGP4 evaluation failed: {0}")] Sgp4(String), diff --git a/src/tle/fitting.rs b/src/tle/fitting.rs index 7524d5c..423f683 100644 --- a/src/tle/fitting.rs +++ b/src/tle/fitting.rs @@ -257,8 +257,7 @@ impl TLE { let mut kepler = crate::kepler::Kepler::from_pv( numeris::vector![closest_state[0], closest_state[1], closest_state[2]], numeris::vector![closest_state[3], closest_state[4], closest_state[5]], - ) - .map_err(|e| Error::KeplerConversion(e.to_string()))?; + )?; // Move Kepler state to epoch if (epoch - closest_time).as_microseconds().abs() > 10 { diff --git a/src/tle/mod.rs b/src/tle/mod.rs index 1218f91..fd1de37 100644 --- a/src/tle/mod.rs +++ b/src/tle/mod.rs @@ -125,7 +125,7 @@ impl SGP4Source for TLE { &mut self.satrec } - fn sgp4_init_args(&self) -> anyhow::Result { + fn sgp4_init_args(&self) -> crate::sgp4::Result { use std::f64::consts::PI; const TWOPI: f64 = PI * 2.0; @@ -407,9 +407,7 @@ impl TLE { // Note: day_of_year starts from 1, not zero, // also, go from Jan 2 to avoid leap-second // issues, hence the "-2" at end - let epoch = Instant::from_date(year as i32, 1, 2) - .map_err(|e| Error::InvalidEpoch(format!("Invalid year, month, or day: {e}")))? - .add_utc_days(day_of_year - 2.0); + let epoch = Instant::from_date(year as i32, 1, 2)?.add_utc_days(day_of_year - 2.0); Ok(Self { name: "none".to_string(), diff --git a/src/utils/download.rs b/src/utils/download.rs index a6e5b81..2401351 100644 --- a/src/utils/download.rs +++ b/src/utils/download.rs @@ -1,5 +1,30 @@ -use anyhow::Result; use std::path::Path; +use thiserror::Error; + +/// Errors produced by the [`utils::download`](crate::utils::download) helpers. +#[derive(Debug, Error)] +pub enum Error { + /// Returned by all download helpers when satkit was built without the + /// `download` Cargo feature. + #[error("satkit was built without the `download` feature")] + FeatureDisabled, + + /// Returned by [`download_if_not_exist`] when the requested file is + /// missing on disk and satkit was built without the `download` feature + /// to fetch it. + #[error("File {path} not found and satkit was built without the `download` feature")] + FileNotFoundNoDownload { path: String }, + + #[error(transparent)] + Io(#[from] std::io::Error), + + #[cfg(feature = "download")] + #[error(transparent)] + Http(#[from] ureq::Error), +} + +/// Convenient type alias used throughout the `download` module. +pub type Result = std::result::Result; #[cfg(feature = "download")] pub fn download_if_not_exist(fname: &Path, seturl: Option<&str>) -> Result<()> { @@ -27,10 +52,9 @@ pub fn download_if_not_exist(fname: &Path, _seturl: Option<&str>) -> Result<()> if fname.is_file() { Ok(()) } else { - anyhow::bail!( - "File {} not found and satkit was built without the `download` feature", - fname.display() - ) + Err(Error::FileNotFoundNoDownload { + path: fname.display().to_string(), + }) } } @@ -57,7 +81,7 @@ pub fn download_file(url: &str, downloaddir: &Path, overwrite_if_exists: bool) - #[cfg(not(feature = "download"))] pub fn download_file(_url: &str, _downloaddir: &Path, _overwrite_if_exists: bool) -> Result { - anyhow::bail!("satkit was built without the `download` feature") + Err(Error::FeatureDisabled) } #[cfg(feature = "download")] @@ -78,7 +102,7 @@ pub fn download_file_async( _downloaddir: &Path, _overwrite_if_exists: bool, ) -> std::thread::JoinHandle> { - std::thread::spawn(|| anyhow::bail!("satkit was built without the `download` feature")) + std::thread::spawn(|| Err(Error::FeatureDisabled)) } #[cfg(feature = "download")] @@ -91,5 +115,5 @@ pub fn download_to_string(url: &str) -> Result { #[cfg(not(feature = "download"))] pub fn download_to_string(_url: &str) -> Result { - anyhow::bail!("satkit was built without the `download` feature") + Err(Error::FeatureDisabled) } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 90a1582..34b5f1a 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -7,11 +7,11 @@ pub use datadir::set_datadir; pub mod test; #[cfg(feature = "download")] -mod update_data; +pub mod update_data; #[cfg(feature = "download")] pub use update_data::update_datafiles; -mod download; +pub mod download; pub use download::download_file; pub use download::download_file_async; pub use download::download_if_not_exist; diff --git a/src/utils/update_data.rs b/src/utils/update_data.rs index d537bd4..41c9e7c 100644 --- a/src/utils/update_data.rs +++ b/src/utils/update_data.rs @@ -1,30 +1,74 @@ -use super::download_file_async; -use super::download_to_string; +use super::download::{self, download_file_async, download_to_string}; use crate::utils::datadir; use serde_json::Value; use std::path::PathBuf; use std::thread::JoinHandle; +use thiserror::Error; -use anyhow::{bail, Result}; +/// Errors produced by [`update_datafiles`] and the underlying download +/// orchestration helpers. +#[derive(Debug, Error)] +pub enum Error { + /// The downloaded JSON file does not parse as the expected array of + /// file URLs. + #[error("Expected JSON array of file URLs")] + NotJsonArray, + + /// An entry inside the JSON array is not a string URL. + #[error("Expected string URL")] + NotJsonString, + + /// Encountered an unexpected JSON node while traversing the + /// recursive directory manifest. + #[error("Invalid JSON manifest entry")] + InvalidManifestEntry, + + /// One or more entries in the JSON manifest could not be parsed. + #[error("Could not parse manifest entries")] + ManifestParseFailed, + + /// The configured data directory is read-only and cannot receive + /// new or refreshed files. + #[error( + "Data directory is read-only. Try setting SATKIT_DATA environment variable \ + to a writeable directory and re-starting" + )] + DataDirReadOnly, + + /// A worker thread launched by [`download_file_async`] panicked. + #[error("Background download thread panicked")] + ThreadPanic, + + #[error(transparent)] + Json(#[from] serde_json::Error), + + #[error(transparent)] + Io(#[from] std::io::Error), + + #[error(transparent)] + Datadir(#[from] crate::utils::datadir::Error), + + #[error(transparent)] + Download(#[from] download::Error), +} + +/// Convenient type alias used throughout the `update_data` module. +pub type Result = std::result::Result; /// Download a list of files from a JSON file fn download_from_url_json(json_url: String, basedir: &std::path::Path) -> Result<()> { let json_base: Value = serde_json::from_str(download_to_string(json_url.as_str())?.as_str())?; - let arr = json_base - .as_array() - .ok_or_else(|| anyhow::anyhow!("Expected JSON array of file URLs"))?; - let vresult: Vec>> = arr + let arr = json_base.as_array().ok_or(Error::NotJsonArray)?; + let vresult: Vec>> = arr .iter() - .map(|url| -> Result>> { - let url_str = url - .as_str() - .ok_or_else(|| anyhow::anyhow!("Expected string URL"))?; + .map(|url| -> Result>> { + let url_str = url.as_str().ok_or(Error::NotJsonString)?; Ok(download_file_async(url_str.to_string(), basedir, true)) }) .collect::>>()?; - // Wait for all the threads to funish + // Wait for all the threads to finish for jh in vresult { - jh.join().unwrap()?; + jh.join().map_err(|_| Error::ThreadPanic)??; } Ok(()) @@ -36,7 +80,7 @@ fn download_from_json( basedir: std::path::PathBuf, baseurl: String, overwrite: &bool, - thandles: &mut Vec>>, + thandles: &mut Vec>>, ) -> Result<()> { if let Some(obj) = v.as_object() { let r1: Vec> = obj @@ -54,7 +98,7 @@ fn download_from_json( .filter(|res| res.is_err()) .collect(); if !r1.is_empty() { - bail!("Could not parse entries"); + return Err(Error::ManifestParseFailed); } } else if let Some(arr) = v.as_array() { let r2: Vec> = arr @@ -66,14 +110,14 @@ fn download_from_json( .filter(|res| res.is_err()) .collect(); if !r2.is_empty() { - bail!("could not parse array entries"); + return Err(Error::ManifestParseFailed); } } else if let Some(s) = v.as_str() { let mut newurl = baseurl; newurl.push_str(format!("/{s}").as_str()); thandles.push(download_file_async(newurl, &basedir, *overwrite)); } else { - bail!("invalid json for downloading files??!!"); + return Err(Error::InvalidManifestEntry); } Ok(()) @@ -88,11 +132,11 @@ fn download_datadir(basedir: PathBuf, baseurl: String, overwrite: &bool) -> Resu fileurl.push_str("/files.json"); let json_base: Value = serde_json::from_str(download_to_string(fileurl.as_str())?.as_str())?; - let mut thandles: Vec>> = Vec::new(); + let mut thandles: Vec>> = Vec::new(); download_from_json(&json_base, basedir, baseurl, overwrite, &mut thandles)?; - // Wait for all the threads to funish + // Wait for all the threads to finish for jh in thandles { - jh.join().unwrap()?; + jh.join().map_err(|_| Error::ThreadPanic)??; } Ok(()) } @@ -122,13 +166,7 @@ pub fn update_datafiles(dir: Option, overwrite_if_exists: bool) -> Resu None => datadir()?, }; if downloaddir.metadata()?.permissions().readonly() { - bail!( - r#" - Data directory is read-only. - Try setting SATKIT_DATA environment - variable to a writeable directory and re-starting - "# - ); + return Err(Error::DataDirReadOnly); } println!(