Skip to main content

hpr/
error.rs

1//! The facade's error type.
2
3use hpr_aero::AeroError;
4use hpr_atmos::AtmosError;
5use hpr_core::CoreError;
6use hpr_design::checks::Severity;
7use hpr_design::{DesignError, Finding};
8use hpr_motor::MotorError;
9use hpr_sim::SimError;
10use thiserror::Error;
11
12/// Anything the builder API can refuse: its own checks, and every error of the crates it calls.
13#[derive(Debug, Error)]
14#[non_exhaustive]
15pub enum Error {
16    /// A number outside its domain, such as a negative length or a NaN, found as it was given.
17    #[error("{what} is {value}, outside its domain")]
18    Domain {
19        /// What the number is.
20        what: &'static str,
21        /// The number.
22        value: f64,
23    },
24    /// Parts added in an order the rocket can't be built in, such as fins before any body tube.
25    #[error(transparent)]
26    Order(Order),
27    /// A material id that isn't built in; [`hpr_design::materials`] lists them.
28    #[error("no built-in material `{0}` (`hpr_design::materials` lists them)")]
29    UnknownMaterial(String),
30    /// No motor with a thrust curve in the bundled catalog matches the name.
31    #[error("no motor with a bundled thrust curve matches `{0}`")]
32    NoSuchMotor(String),
33    /// Several different motors match the name. Each is listed by its designation, which finds
34    /// it alone, then its manufacturer: `I175WS (AeroTech)`.
35    #[error("`{name}` matches several motors: {}", candidates.join(", "))]
36    AmbiguousMotor {
37        /// The name asked for.
38        name: String,
39        /// The motors it matches.
40        candidates: Vec<String>,
41    },
42    /// A motor with an empty designation, which the design would take as a duplicate id.
43    #[error("a motor needs a designation")]
44    EmptyDesignation,
45    /// A motor file holds no motor, or more than one where one was expected.
46    #[error("the motor file holds {0} motors, not one")]
47    MotorCount(usize),
48    /// A flight of a rocket with no motor: give it one with [`crate::Rocket::set_motor`].
49    #[error("the rocket has no motor; give it one with `Rocket::set_motor`")]
50    NoMotor,
51    /// A configuration id the design doesn't hold.
52    #[error("the design has no configuration `{0}`")]
53    NoSuchConfiguration(String),
54    /// The design's checks ([`hpr_design::checks`]) found errors: every finding, errors and
55    /// warnings, in the checks' order.
56    #[error(
57        "the design's checks found {} finding(s), errors among them; the first error is {}",
58        .0.len(),
59        first_error(.0)
60    )]
61    DesignChecks(Vec<Finding>),
62    /// A catalog part the builder can't make into the part asked for.
63    #[error("catalog part {part}: {problem}")]
64    Catalog {
65        /// The part: its maker and part number, and its file.
66        part: String,
67        /// What is wrong with it.
68        problem: CatalogProblem,
69    },
70    /// From the design: its tree, parts and mass properties.
71    #[error(transparent)]
72    Design(#[from] DesignError),
73    /// From the motor models and motor files.
74    #[error(transparent)]
75    Motor(#[from] MotorError),
76    /// From the flight simulation.
77    #[error(transparent)]
78    Sim(#[from] SimError),
79    /// From the aerodynamic model.
80    #[error(transparent)]
81    Aero(#[from] AeroError),
82    /// From the atmosphere and wind models.
83    #[error(transparent)]
84    Atmos(#[from] AtmosError),
85    /// From the geodesy: a launch site that isn't on the Earth.
86    #[error(transparent)]
87    Core(#[from] CoreError),
88}
89
90/// What is out of order when a part can't go where it was added.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
92#[non_exhaustive]
93pub enum Order {
94    /// A nose added after another body part: the nose goes first.
95    #[error("the nose goes first, before any other body part")]
96    NoseNotFirst,
97    /// A transition with no body part before it to start from.
98    #[error("a transition needs a body part before it")]
99    NothingBeforeTransition,
100    /// Fins, a motor tube or a mass with no body tube to go on.
101    #[error("fins, a motor tube and masses go on a body tube: add one first")]
102    NoTube,
103    /// A second motor tube: a built rocket has one.
104    #[error("the rocket has a motor tube already")]
105    SecondMotorTube,
106    /// A motor with no motor tube to go in.
107    #[error("a motor needs a motor tube to go in")]
108    NoMotorTube,
109    /// A fitting holding a part that isn't one (one read from a file): a fitting is a coupler,
110    /// a centering ring, a bulkhead, a launch lug, a parachute or a streamer.
111    #[error("a fitting is a coupler, centering ring, bulkhead, launch lug, parachute or streamer")]
112    NotAFitting,
113    /// A part added to a rocket read from a design, which the builder doesn't change.
114    #[error("parts can't be added to a rocket read from a design")]
115    ReadFromDesign,
116}
117
118/// Why a catalog part ([`hpr_io::orc::Part`]) can't be made into a builder part.
119#[derive(Debug, Clone, PartialEq, Eq, Error)]
120#[non_exhaustive]
121pub enum CatalogProblem {
122    /// A part of a kind the builder's part isn't, such as a body tube given to
123    /// `Nose::from_catalog`.
124    #[error("it is a {found}, which `{builder}` doesn't make")]
125    Kind {
126        /// The part's kind.
127        found: &'static str,
128        /// The function it was given to.
129        builder: &'static str,
130    },
131    /// A material its file names but doesn't define, so it has no density.
132    #[error("its file doesn't define its material `{0}`")]
133    UndefinedMaterial(String),
134    /// A nose cone or transition neither filled nor given a wall thickness.
135    #[error("it is neither filled nor given a wall thickness")]
136    NoWall,
137    /// A nose cone's or transition's shape the builder doesn't know.
138    #[error("its shape is one the builder doesn't know")]
139    Shape,
140    /// A part that states its mass but whose sizes give it no volume to hold it, so no density
141    /// gives it that mass.
142    #[error("it states a mass, but its sizes give it no volume to hold it")]
143    NoVolume,
144}
145
146/// `value` if it is finite and positive, [`Error::Domain`] otherwise.
147pub(crate) fn positive(what: &'static str, value: f64) -> Result<f64, Error> {
148    if value.is_finite() && value > 0.0 {
149        Ok(value)
150    } else {
151        Err(Error::Domain { what, value })
152    }
153}
154
155/// `value` if it is finite and not negative, [`Error::Domain`] otherwise.
156pub(crate) fn non_negative(what: &'static str, value: f64) -> Result<f64, Error> {
157    if value.is_finite() && value >= 0.0 {
158        Ok(value)
159    } else {
160        Err(Error::Domain { what, value })
161    }
162}
163
164/// `value` if it is finite, [`Error::Domain`] otherwise.
165pub(crate) fn finite(what: &'static str, value: f64) -> Result<f64, Error> {
166    if value.is_finite() {
167        Ok(value)
168    } else {
169        Err(Error::Domain { what, value })
170    }
171}
172
173/// The first finding that is an error, for [`Error::DesignChecks`]'s message.
174fn first_error(findings: &[Finding]) -> String {
175    findings
176        .iter()
177        .find(|finding| finding.severity() == Severity::Error)
178        .map_or_else(|| "missing".to_owned(), |finding| format!("{finding:?}"))
179}