Skip to main content

nhs_number/
parse_error.rs

1//! The error type returned by [`NHSNumber::from_str`](crate::NHSNumber).
2//!
3//! `ParseError` is a zero-sized unit struct that signals "this string is
4//! not a syntactically valid NHS Number". It deliberately carries no
5//! detail — callers who need a richer taxonomy (wrong length, wrong
6//! separator, non-digit character, …) wrap or map it at the parse site.
7//!
8//! See [`spec/index.md`](https://github.com/joelparkerhenderson/nhs-number-using-rust/blob/main/spec/index.md)
9//! §12 for the design rationale.
10
11/// Error returned by `<NHSNumber as FromStr>::from_str` for any string
12/// that is not one of the two accepted shapes (see
13/// [`spec/05-string-forms.md`] §5).
14///
15/// `ParseError` is a unit struct — every error value compares equal:
16///
17/// ```rust
18/// use nhs_number::NHSNumber;
19/// use nhs_number::parse_error::ParseError;
20/// use std::str::FromStr;
21///
22/// let a = NHSNumber::from_str("not even close").unwrap_err();
23/// let b = NHSNumber::from_str("wrong length").unwrap_err();
24/// assert_eq!(a, b);
25/// assert_eq!(a, ParseError);
26/// ```
27///
28/// To map it to your own richer error type:
29///
30/// ```rust
31/// use nhs_number::NHSNumber;
32/// use std::str::FromStr;
33///
34/// #[derive(Debug, PartialEq)]
35/// enum MyError {
36///     BadNhsNumber(String),
37/// }
38///
39/// let bad = "not a number";
40/// let result: Result<NHSNumber, MyError> =
41///     NHSNumber::from_str(bad).map_err(|_| MyError::BadNhsNumber(bad.into()));
42/// assert_eq!(result, Err(MyError::BadNhsNumber("not a number".into())));
43/// ```
44///
45/// `ParseError` also implements [`Display`](std::fmt::Display) and
46/// [`std::error::Error`], so it flows through `?` into boxed-error
47/// stacks without manual mapping:
48///
49/// ```rust
50/// use nhs_number::NHSNumber;
51/// use std::error::Error;
52/// use std::str::FromStr;
53///
54/// fn parse(s: &str) -> Result<NHSNumber, Box<dyn Error>> {
55///     Ok(NHSNumber::from_str(s)?)
56/// }
57///
58/// assert!(parse("999 100 0003").is_ok());
59/// assert!(parse("not a number").is_err());
60/// ```
61///
62/// [`spec/05-string-forms.md`]: https://github.com/joelparkerhenderson/nhs-number-using-rust/blob/main/spec/05-string-forms.md
63#[derive(Debug, PartialEq, Eq)]
64pub struct ParseError;
65
66/// A fixed message with **no input echo** — the rejected candidate
67/// string must never leak into error messages or logs
68/// (see `AGENTS/safety.md` §3).
69///
70/// ```rust
71/// use nhs_number::parse_error::ParseError;
72///
73/// assert_eq!(ParseError.to_string(), "invalid NHS Number string");
74/// ```
75impl std::fmt::Display for ParseError {
76    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
77        f.write_str("invalid NHS Number string")
78    }
79}
80
81impl std::error::Error for ParseError {}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn test_parse_error_is_zero_sized() {
89        assert_eq!(std::mem::size_of::<ParseError>(), 0);
90    }
91
92    #[test]
93    fn test_parse_error_equality() {
94        assert_eq!(ParseError, ParseError);
95    }
96
97    #[test]
98    fn test_parse_error_debug() {
99        let dbg = format!("{:?}", ParseError);
100        assert_eq!(dbg, "ParseError");
101    }
102
103    #[test]
104    fn test_parse_error_display_is_fixed_message() {
105        let actual = ParseError.to_string();
106        let expect = "invalid NHS Number string";
107        assert_eq!(actual, expect);
108    }
109
110    #[test]
111    fn test_parse_error_implements_std_error() {
112        fn assert_error<T: std::error::Error>() {}
113        assert_error::<ParseError>();
114        // And it boxes cleanly.
115        let boxed: Box<dyn std::error::Error> = Box::new(ParseError);
116        assert_eq!(boxed.to_string(), "invalid NHS Number string");
117    }
118
119    #[test]
120    fn test_parse_error_propagates_via_question_mark() {
121        use crate::NHSNumber;
122        use std::str::FromStr;
123
124        fn parse(s: &str) -> Result<NHSNumber, Box<dyn std::error::Error>> {
125            Ok(NHSNumber::from_str(s)?)
126        }
127        assert!(parse("999 100 0003").is_ok());
128        assert!(parse("bad").is_err());
129    }
130}