1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5
6#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
8pub struct DiagnosticPosition {
9 line: usize,
10 column: usize,
11}
12
13impl DiagnosticPosition {
14 pub const fn new(line: usize, column: usize) -> Result<Self, DiagnosticPositionError> {
21 if line == 0 {
22 return Err(DiagnosticPositionError::ZeroLine);
23 }
24
25 if column == 0 {
26 return Err(DiagnosticPositionError::ZeroColumn);
27 }
28
29 Ok(Self { line, column })
30 }
31
32 #[must_use]
34 pub const fn line(self) -> usize {
35 self.line
36 }
37
38 #[must_use]
40 pub const fn column(self) -> usize {
41 self.column
42 }
43}
44
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub enum DiagnosticPositionError {
48 ZeroLine,
50 ZeroColumn,
52}
53
54impl fmt::Display for DiagnosticPositionError {
55 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
56 match self {
57 Self::ZeroLine => formatter.write_str("diagnostic position line must be at least 1"),
58 Self::ZeroColumn => {
59 formatter.write_str("diagnostic position column must be at least 1")
60 },
61 }
62 }
63}
64
65impl std::error::Error for DiagnosticPositionError {}
66
67#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
69pub struct DiagnosticSourceId(String);
70
71impl DiagnosticSourceId {
72 pub fn new(value: impl AsRef<str>) -> Result<Self, DiagnosticSourceIdError> {
78 let trimmed = value.as_ref().trim();
79
80 if trimmed.is_empty() {
81 return Err(DiagnosticSourceIdError::Empty);
82 }
83
84 Ok(Self(trimmed.to_string()))
85 }
86
87 #[must_use]
89 pub fn as_str(&self) -> &str {
90 &self.0
91 }
92
93 #[must_use]
95 pub fn into_string(self) -> String {
96 self.0
97 }
98}
99
100impl AsRef<str> for DiagnosticSourceId {
101 fn as_ref(&self) -> &str {
102 self.as_str()
103 }
104}
105
106impl fmt::Display for DiagnosticSourceId {
107 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
108 formatter.write_str(self.as_str())
109 }
110}
111
112impl FromStr for DiagnosticSourceId {
113 type Err = DiagnosticSourceIdError;
114
115 fn from_str(value: &str) -> Result<Self, Self::Err> {
116 Self::new(value)
117 }
118}
119
120#[derive(Clone, Copy, Debug, Eq, PartialEq)]
122pub enum DiagnosticSourceIdError {
123 Empty,
125}
126
127impl fmt::Display for DiagnosticSourceIdError {
128 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
129 match self {
130 Self::Empty => formatter.write_str("diagnostic source ID cannot be empty"),
131 }
132 }
133}
134
135impl std::error::Error for DiagnosticSourceIdError {}
136
137#[derive(Clone, Debug, Eq, Hash, PartialEq)]
139pub struct DiagnosticSpan {
140 source: Option<DiagnosticSourceId>,
141 start: DiagnosticPosition,
142 end: DiagnosticPosition,
143}
144
145impl DiagnosticSpan {
146 pub fn new(
152 source: Option<DiagnosticSourceId>,
153 start: DiagnosticPosition,
154 end: DiagnosticPosition,
155 ) -> Result<Self, DiagnosticSpanError> {
156 if end < start {
157 return Err(DiagnosticSpanError::Reversed);
158 }
159
160 Ok(Self { source, start, end })
161 }
162
163 pub fn with_source(
169 source: DiagnosticSourceId,
170 start: DiagnosticPosition,
171 end: DiagnosticPosition,
172 ) -> Result<Self, DiagnosticSpanError> {
173 Self::new(Some(source), start, end)
174 }
175
176 pub fn without_source(
182 start: DiagnosticPosition,
183 end: DiagnosticPosition,
184 ) -> Result<Self, DiagnosticSpanError> {
185 Self::new(None, start, end)
186 }
187
188 #[must_use]
190 pub const fn source(&self) -> Option<&DiagnosticSourceId> {
191 self.source.as_ref()
192 }
193
194 #[must_use]
196 pub const fn start(&self) -> DiagnosticPosition {
197 self.start
198 }
199
200 #[must_use]
202 pub const fn end(&self) -> DiagnosticPosition {
203 self.end
204 }
205}
206
207#[derive(Clone, Copy, Debug, Eq, PartialEq)]
209pub enum DiagnosticSpanError {
210 Reversed,
212}
213
214impl fmt::Display for DiagnosticSpanError {
215 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
216 match self {
217 Self::Reversed => formatter.write_str("diagnostic span end cannot be before start"),
218 }
219 }
220}
221
222impl std::error::Error for DiagnosticSpanError {}
223
224#[cfg(test)]
225mod tests {
226 use super::{
227 DiagnosticPosition, DiagnosticPositionError, DiagnosticSourceId, DiagnosticSpan,
228 DiagnosticSpanError,
229 };
230
231 #[test]
232 fn accepts_valid_position() {
233 let position = DiagnosticPosition::new(1, 1).expect("position should be valid");
234
235 assert_eq!(position.line(), 1);
236 assert_eq!(position.column(), 1);
237 }
238
239 #[test]
240 fn rejects_zero_line_or_column() {
241 assert_eq!(
242 DiagnosticPosition::new(0, 1),
243 Err(DiagnosticPositionError::ZeroLine)
244 );
245 assert_eq!(
246 DiagnosticPosition::new(1, 0),
247 Err(DiagnosticPositionError::ZeroColumn)
248 );
249 }
250
251 #[test]
252 fn accepts_valid_span() {
253 let start = DiagnosticPosition::new(2, 4).expect("position should be valid");
254 let end = DiagnosticPosition::new(2, 9).expect("position should be valid");
255 let span = DiagnosticSpan::without_source(start, end).expect("span should be valid");
256
257 assert_eq!(span.start(), start);
258 assert_eq!(span.end(), end);
259 }
260
261 #[test]
262 fn rejects_reversed_span() {
263 let start = DiagnosticPosition::new(3, 10).expect("position should be valid");
264 let end = DiagnosticPosition::new(3, 5).expect("position should be valid");
265
266 assert_eq!(
267 DiagnosticSpan::without_source(start, end),
268 Err(DiagnosticSpanError::Reversed)
269 );
270 }
271
272 #[test]
273 fn creates_span_with_source_id() {
274 let source = DiagnosticSourceId::new(" config.toml ").expect("source should be valid");
275 let start = DiagnosticPosition::new(4, 1).expect("position should be valid");
276 let end = DiagnosticPosition::new(4, 3).expect("position should be valid");
277 let span = DiagnosticSpan::with_source(source, start, end).expect("span should be valid");
278
279 assert_eq!(
280 span.source().map(DiagnosticSourceId::as_str),
281 Some("config.toml")
282 );
283 }
284
285 #[test]
286 fn creates_span_without_source_id() {
287 let start = DiagnosticPosition::new(1, 1).expect("position should be valid");
288 let end = DiagnosticPosition::new(1, 1).expect("position should be valid");
289 let span = DiagnosticSpan::without_source(start, end).expect("span should be valid");
290
291 assert!(span.source().is_none());
292 }
293}