Skip to main content

use_diagnostic_message/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5
6/// A human-facing diagnostic message.
7#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
8pub struct DiagnosticMessage(String);
9
10impl DiagnosticMessage {
11    /// Creates a diagnostic message from non-empty plain text after trimming surrounding whitespace.
12    ///
13    /// # Errors
14    ///
15    /// Returns [`DiagnosticTextError::Empty`] when the trimmed value is empty.
16    pub fn new(value: impl AsRef<str>) -> Result<Self, DiagnosticTextError> {
17        Ok(Self(normalize_text(value.as_ref())?))
18    }
19
20    /// Returns the message text.
21    #[must_use]
22    pub fn as_str(&self) -> &str {
23        &self.0
24    }
25
26    /// Consumes the message and returns the owned string.
27    #[must_use]
28    pub fn into_string(self) -> String {
29        self.0
30    }
31}
32
33impl AsRef<str> for DiagnosticMessage {
34    fn as_ref(&self) -> &str {
35        self.as_str()
36    }
37}
38
39impl fmt::Display for DiagnosticMessage {
40    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41        formatter.write_str(self.as_str())
42    }
43}
44
45impl FromStr for DiagnosticMessage {
46    type Err = DiagnosticTextError;
47
48    fn from_str(value: &str) -> Result<Self, Self::Err> {
49        Self::new(value)
50    }
51}
52
53/// Additional plain-text context attached to a diagnostic.
54#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
55pub struct DiagnosticNote(String);
56
57impl DiagnosticNote {
58    /// Creates a diagnostic note from non-empty plain text after trimming surrounding whitespace.
59    ///
60    /// # Errors
61    ///
62    /// Returns [`DiagnosticTextError::Empty`] when the trimmed value is empty.
63    pub fn new(value: impl AsRef<str>) -> Result<Self, DiagnosticTextError> {
64        Ok(Self(normalize_text(value.as_ref())?))
65    }
66
67    /// Returns the note text.
68    #[must_use]
69    pub fn as_str(&self) -> &str {
70        &self.0
71    }
72
73    /// Consumes the note and returns the owned string.
74    #[must_use]
75    pub fn into_string(self) -> String {
76        self.0
77    }
78}
79
80impl AsRef<str> for DiagnosticNote {
81    fn as_ref(&self) -> &str {
82        self.as_str()
83    }
84}
85
86impl fmt::Display for DiagnosticNote {
87    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
88        formatter.write_str(self.as_str())
89    }
90}
91
92impl FromStr for DiagnosticNote {
93    type Err = DiagnosticTextError;
94
95    fn from_str(value: &str) -> Result<Self, Self::Err> {
96        Self::new(value)
97    }
98}
99
100/// Errors returned while constructing diagnostic text primitives.
101#[derive(Clone, Copy, Debug, Eq, PartialEq)]
102pub enum DiagnosticTextError {
103    /// The text was empty after trimming surrounding whitespace.
104    Empty,
105}
106
107impl fmt::Display for DiagnosticTextError {
108    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
109        match self {
110            Self::Empty => formatter.write_str("diagnostic text cannot be empty"),
111        }
112    }
113}
114
115impl std::error::Error for DiagnosticTextError {}
116
117fn normalize_text(value: &str) -> Result<String, DiagnosticTextError> {
118    let trimmed = value.trim();
119
120    if trimmed.is_empty() {
121        Err(DiagnosticTextError::Empty)
122    } else {
123        Ok(trimmed.to_string())
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::{DiagnosticMessage, DiagnosticNote, DiagnosticTextError};
130
131    #[test]
132    fn accepts_valid_message() {
133        let message =
134            DiagnosticMessage::new("missing required field").expect("message should be valid");
135
136        assert_eq!(message.as_str(), "missing required field");
137    }
138
139    #[test]
140    fn rejects_empty_message() {
141        assert_eq!(
142            DiagnosticMessage::new(" \n\t "),
143            Err(DiagnosticTextError::Empty)
144        );
145    }
146
147    #[test]
148    fn trims_surrounding_message_whitespace() {
149        let message = DiagnosticMessage::new("  invalid configuration value  ")
150            .expect("message should be valid");
151
152        assert_eq!(message.as_str(), "invalid configuration value");
153    }
154
155    #[test]
156    fn display_returns_plain_message() {
157        let message = DiagnosticMessage::new("malformed input").expect("message should be valid");
158
159        assert_eq!(message.to_string(), "malformed input");
160    }
161
162    #[test]
163    fn constructs_notes() {
164        let note =
165            DiagnosticNote::new("field names are case-sensitive").expect("note should be valid");
166
167        assert_eq!(note.as_str(), "field names are case-sensitive");
168    }
169}