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}