Skip to main content

use_diagnostic_code/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5
6/// A stable string identifier for a diagnostic.
7#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
8pub struct DiagnosticCode(String);
9
10impl DiagnosticCode {
11    /// Creates a diagnostic code from a non-empty string after trimming surrounding whitespace.
12    ///
13    /// # Errors
14    ///
15    /// Returns [`DiagnosticCodeError::Empty`] when the trimmed value is empty.
16    pub fn new(value: impl AsRef<str>) -> Result<Self, DiagnosticCodeError> {
17        let trimmed = value.as_ref().trim();
18
19        if trimmed.is_empty() {
20            return Err(DiagnosticCodeError::Empty);
21        }
22
23        Ok(Self(trimmed.to_string()))
24    }
25
26    /// Returns the diagnostic code text.
27    #[must_use]
28    pub fn as_str(&self) -> &str {
29        &self.0
30    }
31
32    /// Consumes the code and returns the owned string.
33    #[must_use]
34    pub fn into_string(self) -> String {
35        self.0
36    }
37}
38
39impl AsRef<str> for DiagnosticCode {
40    fn as_ref(&self) -> &str {
41        self.as_str()
42    }
43}
44
45impl fmt::Display for DiagnosticCode {
46    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
47        formatter.write_str(self.as_str())
48    }
49}
50
51impl FromStr for DiagnosticCode {
52    type Err = DiagnosticCodeError;
53
54    fn from_str(value: &str) -> Result<Self, Self::Err> {
55        Self::new(value)
56    }
57}
58
59/// Errors returned while constructing a [`DiagnosticCode`].
60#[derive(Clone, Copy, Debug, Eq, PartialEq)]
61pub enum DiagnosticCodeError {
62    /// The code was empty after trimming surrounding whitespace.
63    Empty,
64}
65
66impl fmt::Display for DiagnosticCodeError {
67    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
68        match self {
69            Self::Empty => formatter.write_str("diagnostic code cannot be empty"),
70        }
71    }
72}
73
74impl std::error::Error for DiagnosticCodeError {}
75
76#[cfg(test)]
77mod tests {
78    use super::{DiagnosticCode, DiagnosticCodeError};
79
80    #[test]
81    fn accepts_valid_code() {
82        let code = DiagnosticCode::new("CONFIG001").expect("code should be valid");
83
84        assert_eq!(code.as_str(), "CONFIG001");
85    }
86
87    #[test]
88    fn rejects_empty_code() {
89        assert_eq!(DiagnosticCode::new("   "), Err(DiagnosticCodeError::Empty));
90    }
91
92    #[test]
93    fn trims_surrounding_whitespace() {
94        let code = DiagnosticCode::new("  DATA.INVALID_SHAPE  ").expect("code should be valid");
95
96        assert_eq!(code.as_str(), "DATA.INVALID_SHAPE");
97    }
98
99    #[test]
100    fn display_round_trips_through_parse() {
101        let code = DiagnosticCode::new("VALIDATE_MISSING_FIELD").expect("code should be valid");
102        let parsed: DiagnosticCode = code
103            .to_string()
104            .parse()
105            .expect("displayed code should parse");
106
107        assert_eq!(parsed, code);
108    }
109
110    #[test]
111    fn ordering_is_deterministic() {
112        let mut codes = [
113            DiagnosticCode::new("DATA.INVALID_SHAPE").expect("code should be valid"),
114            DiagnosticCode::new("CONFIG001").expect("code should be valid"),
115            DiagnosticCode::new("VALIDATE_MISSING_FIELD").expect("code should be valid"),
116        ];
117
118        codes.sort();
119
120        assert_eq!(codes[0].as_str(), "CONFIG001");
121        assert_eq!(codes[1].as_str(), "DATA.INVALID_SHAPE");
122        assert_eq!(codes[2].as_str(), "VALIDATE_MISSING_FIELD");
123    }
124}