Skip to main content

ph_surfaces/
error.rs

1//! The outcome type reported when a coordinate leaves the declared domain.
2
3use core::fmt::{Display, Formatter, Result as FmtResult};
4
5/// A coordinate fell outside the declared domain on a side selecting
6/// [`Boundary::Error`](crate::Boundary::Error).
7///
8/// The four variants distinguish the four sides, so a caller never has to
9/// infer which axis rejected the input. Each carries the coordinate that was
10/// supplied and the bound that applied to it.
11///
12/// This enum is deliberately not `#[non_exhaustive]`. The v0.1 contract fixes
13/// exactly these four outcomes: shape mismatches are type errors, axis
14/// invariants are checked when the surface is defined, and the arithmetic
15/// cannot overflow for valid operands. Exhaustive matching without a wildcard
16/// arm is therefore both possible and intended.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum SurfaceError {
19    /// The X coordinate was below the first X knot.
20    XBelow {
21        /// The rejected X coordinate.
22        coordinate: u16,
23        /// The first X knot: the inclusive lower bound of the X domain.
24        bound: u16,
25    },
26    /// The X coordinate was above the last X knot.
27    XAbove {
28        /// The rejected X coordinate.
29        coordinate: u16,
30        /// The last X knot: the inclusive upper bound of the X domain.
31        bound: u16,
32    },
33    /// The Y coordinate was below the first Y knot.
34    YBelow {
35        /// The rejected Y coordinate.
36        coordinate: u16,
37        /// The first Y knot: the inclusive lower bound of the Y domain.
38        bound: u16,
39    },
40    /// The Y coordinate was above the last Y knot.
41    YAbove {
42        /// The rejected Y coordinate.
43        coordinate: u16,
44        /// The last Y knot: the inclusive upper bound of the Y domain.
45        bound: u16,
46    },
47}
48
49impl SurfaceError {
50    /// Returns the coordinate that was rejected, whichever side reported it.
51    ///
52    /// # Examples
53    ///
54    /// ```
55    /// use ph_surfaces::SurfaceError;
56    ///
57    /// let error = SurfaceError::YAbove {
58    ///     coordinate: 900,
59    ///     bound: 500,
60    /// };
61    ///
62    /// assert_eq!(error.coordinate(), 900);
63    /// assert_eq!(error.bound(), 500);
64    /// ```
65    #[must_use]
66    pub const fn coordinate(&self) -> u16 {
67        match *self {
68            Self::XBelow { coordinate, .. }
69            | Self::XAbove { coordinate, .. }
70            | Self::YBelow { coordinate, .. }
71            | Self::YAbove { coordinate, .. } => coordinate,
72        }
73    }
74
75    /// Returns the bound that applied, whichever side reported it.
76    ///
77    /// For the two below-domain variants this is the first knot of the axis;
78    /// for the two above-domain variants it is the last knot.
79    #[must_use]
80    pub const fn bound(&self) -> u16 {
81        match *self {
82            Self::XBelow { bound, .. }
83            | Self::XAbove { bound, .. }
84            | Self::YBelow { bound, .. }
85            | Self::YAbove { bound, .. } => bound,
86        }
87    }
88}
89
90impl Display for SurfaceError {
91    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
92        match *self {
93            Self::XBelow { coordinate, bound } => {
94                write!(
95                    f,
96                    "x coordinate {coordinate} is below the x axis minimum {bound}"
97                )
98            }
99            Self::XAbove { coordinate, bound } => {
100                write!(
101                    f,
102                    "x coordinate {coordinate} is above the x axis maximum {bound}"
103                )
104            }
105            Self::YBelow { coordinate, bound } => {
106                write!(
107                    f,
108                    "y coordinate {coordinate} is below the y axis minimum {bound}"
109                )
110            }
111            Self::YAbove { coordinate, bound } => {
112                write!(
113                    f,
114                    "y coordinate {coordinate} is above the y axis maximum {bound}"
115                )
116            }
117        }
118    }
119}
120
121impl core::error::Error for SurfaceError {}
122
123#[cfg(test)]
124mod tests {
125    use super::SurfaceError;
126    use core::fmt::Write;
127
128    /// A fixed-capacity `core::fmt::Write` sink.
129    ///
130    /// The crate denies `clippy::std_instead_of_core`, so tests render
131    /// `Display` output without `std::format!`.
132    struct Sink {
133        buffer: [u8; 96],
134        used: usize,
135    }
136
137    impl Sink {
138        const fn new() -> Self {
139            Self {
140                buffer: [0; 96],
141                used: 0,
142            }
143        }
144
145        fn as_str(&self) -> &str {
146            core::str::from_utf8(&self.buffer[..self.used]).expect("sink holds valid utf-8")
147        }
148    }
149
150    impl Write for Sink {
151        fn write_str(&mut self, s: &str) -> core::fmt::Result {
152            let bytes = s.as_bytes();
153            let end = self.used + bytes.len();
154            assert!(end <= self.buffer.len(), "sink overflowed");
155            self.buffer[self.used..end].copy_from_slice(bytes);
156            self.used = end;
157            Ok(())
158        }
159    }
160
161    fn rendered(error: SurfaceError) -> Sink {
162        let mut sink = Sink::new();
163        write!(sink, "{error}").expect("writing to the sink cannot fail");
164        sink
165    }
166
167    #[test]
168    fn each_side_reports_its_coordinate_and_bound() {
169        let cases = [
170            SurfaceError::XBelow {
171                coordinate: 3,
172                bound: 10,
173            },
174            SurfaceError::XAbove {
175                coordinate: 900,
176                bound: 500,
177            },
178            SurfaceError::YBelow {
179                coordinate: 0,
180                bound: 7,
181            },
182            SurfaceError::YAbove {
183                coordinate: u16::MAX,
184                bound: 1,
185            },
186        ];
187        let expected = [(3, 10), (900, 500), (0, 7), (u16::MAX, 1)];
188
189        for (error, (coordinate, bound)) in cases.into_iter().zip(expected) {
190            assert_eq!(error.coordinate(), coordinate);
191            assert_eq!(error.bound(), bound);
192        }
193    }
194
195    #[test]
196    fn sides_are_not_equal_to_each_other() {
197        let x_below = SurfaceError::XBelow {
198            coordinate: 4,
199            bound: 9,
200        };
201        let x_above = SurfaceError::XAbove {
202            coordinate: 4,
203            bound: 9,
204        };
205        let y_below = SurfaceError::YBelow {
206            coordinate: 4,
207            bound: 9,
208        };
209        let y_above = SurfaceError::YAbove {
210            coordinate: 4,
211            bound: 9,
212        };
213
214        assert_ne!(x_below, x_above);
215        assert_ne!(x_below, y_below);
216        assert_ne!(x_above, y_above);
217        assert_ne!(y_below, y_above);
218        assert_eq!(
219            x_below,
220            SurfaceError::XBelow {
221                coordinate: 4,
222                bound: 9,
223            }
224        );
225    }
226
227    #[test]
228    fn display_names_the_axis_the_direction_and_the_bound() {
229        let x_below = rendered(SurfaceError::XBelow {
230            coordinate: 3,
231            bound: 10,
232        });
233        assert_eq!(
234            x_below.as_str(),
235            "x coordinate 3 is below the x axis minimum 10"
236        );
237
238        let x_above = rendered(SurfaceError::XAbove {
239            coordinate: 900,
240            bound: 500,
241        });
242        assert_eq!(
243            x_above.as_str(),
244            "x coordinate 900 is above the x axis maximum 500"
245        );
246
247        let y_below = rendered(SurfaceError::YBelow {
248            coordinate: 3,
249            bound: 10,
250        });
251        assert_eq!(
252            y_below.as_str(),
253            "y coordinate 3 is below the y axis minimum 10"
254        );
255
256        let y_above = rendered(SurfaceError::YAbove {
257            coordinate: 900,
258            bound: 500,
259        });
260        assert_eq!(
261            y_above.as_str(),
262            "y coordinate 900 is above the y axis maximum 500"
263        );
264    }
265
266    #[test]
267    fn the_error_has_no_source() {
268        let error = SurfaceError::XBelow {
269            coordinate: 1,
270            bound: 2,
271        };
272
273        assert!(core::error::Error::source(&error).is_none());
274    }
275}