Skip to main content

muffy_validation/
error.rs

1use alloc::collections::{BTreeMap, BTreeSet};
2use core::fmt::{self, Display, Formatter};
3
4/// A markup error.
5#[derive(Clone, Debug, Eq, PartialEq)]
6pub enum MarkupError {
7    /// An unknown tag.
8    UnknownTag(String),
9    /// Invalid element.
10    InvalidElement {
11        /// Invalid attributes.
12        invalid_attributes: BTreeMap<String, BTreeSet<AttributeError>>,
13        /// Invalid children.
14        invalid_children: BTreeMap<String, BTreeSet<ChildError>>,
15        /// Missing required attributes.
16        missing_attributes: BTreeSet<String>,
17        /// Missing required children.
18        missing_children: BTreeSet<String>,
19    },
20}
21
22impl Display for MarkupError {
23    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
24        match self {
25            Self::UnknownTag(tag) => write!(formatter, "unknown tag \"{tag}\""),
26            Self::InvalidElement {
27                invalid_attributes,
28                invalid_children,
29                missing_attributes,
30                missing_children,
31            } => write!(
32                formatter,
33                "{}",
34                [
35                    format_errors("invalid attributes", invalid_attributes),
36                    format_errors("invalid children", invalid_children),
37                    format_names("missing attributes", missing_attributes),
38                    format_names("missing children", missing_children),
39                ]
40                .into_iter()
41                .flatten()
42                .collect::<Vec<_>>()
43                .join(", ")
44            ),
45        }
46    }
47}
48
49fn format_errors<E: Display>(
50    label: &str,
51    errors: &BTreeMap<String, BTreeSet<E>>,
52) -> Option<String> {
53    (!errors.is_empty()).then(|| {
54        format!(
55            "{label}: {}",
56            errors
57                .iter()
58                .map(|(name, errors)| format!(
59                    "{name} ({})",
60                    errors
61                        .iter()
62                        .map(ToString::to_string)
63                        .collect::<Vec<_>>()
64                        .join(", ")
65                ))
66                .collect::<Vec<_>>()
67                .join(", ")
68        )
69    })
70}
71
72fn format_names(label: &str, names: &BTreeSet<String>) -> Option<String> {
73    (!names.is_empty()).then(|| {
74        format!(
75            "{label}: {}",
76            names.iter().cloned().collect::<Vec<_>>().join(", ")
77        )
78    })
79}
80
81/// An attribute markup error.
82#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
83pub enum AttributeError {
84    /// Conflicting with other attributes.
85    Conflict,
86    /// Not allowed.
87    NotAllowed,
88}
89
90impl Display for AttributeError {
91    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
92        match self {
93            Self::Conflict => write!(formatter, "conflicting"),
94            Self::NotAllowed => write!(formatter, "not allowed"),
95        }
96    }
97}
98
99/// A child markup error.
100#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
101pub enum ChildError {
102    /// Misplaced.
103    Misplaced,
104    /// Not allowed.
105    NotAllowed,
106}
107
108impl Display for ChildError {
109    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
110        match self {
111            Self::Misplaced => write!(formatter, "misplaced"),
112            Self::NotAllowed => write!(formatter, "not allowed"),
113        }
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn display_unknown_tag() {
123        assert_eq!(
124            format!("{}", MarkupError::UnknownTag("foo".into())),
125            "unknown tag \"foo\""
126        );
127    }
128
129    #[test]
130    fn display_not_allowed_attributes() {
131        assert_eq!(
132            format!(
133                "{}",
134                MarkupError::InvalidElement {
135                    invalid_attributes: [("foo".into(), [AttributeError::NotAllowed].into())]
136                        .into(),
137                    invalid_children: Default::default(),
138                    missing_attributes: Default::default(),
139                    missing_children: Default::default(),
140                }
141            ),
142            "invalid attributes: foo (not allowed)"
143        );
144    }
145
146    #[test]
147    fn display_conflicting_attribute() {
148        assert_eq!(
149            format!(
150                "{}",
151                MarkupError::InvalidElement {
152                    invalid_attributes: [("foo".into(), [AttributeError::Conflict].into())].into(),
153                    invalid_children: Default::default(),
154                    missing_attributes: Default::default(),
155                    missing_children: Default::default(),
156                }
157            ),
158            "invalid attributes: foo (conflicting)"
159        );
160    }
161
162    #[test]
163    fn display_not_allowed_children() {
164        assert_eq!(
165            format!(
166                "{}",
167                MarkupError::InvalidElement {
168                    invalid_attributes: Default::default(),
169                    invalid_children: [("foo".into(), [ChildError::NotAllowed].into())].into(),
170                    missing_attributes: Default::default(),
171                    missing_children: Default::default(),
172                }
173            ),
174            "invalid children: foo (not allowed)"
175        );
176    }
177
178    #[test]
179    fn display_misplaced_child() {
180        assert_eq!(
181            format!(
182                "{}",
183                MarkupError::InvalidElement {
184                    invalid_attributes: Default::default(),
185                    invalid_children: [("foo".into(), [ChildError::Misplaced].into())].into(),
186                    missing_attributes: Default::default(),
187                    missing_children: Default::default(),
188                }
189            ),
190            "invalid children: foo (misplaced)"
191        );
192    }
193
194    #[test]
195    fn display_missing_attributes() {
196        assert_eq!(
197            format!(
198                "{}",
199                MarkupError::InvalidElement {
200                    invalid_attributes: Default::default(),
201                    invalid_children: Default::default(),
202                    missing_attributes: ["bar".into(), "foo".into()].into(),
203                    missing_children: Default::default(),
204                }
205            ),
206            "missing attributes: bar, foo"
207        );
208    }
209
210    #[test]
211    fn display_missing_children() {
212        assert_eq!(
213            format!(
214                "{}",
215                MarkupError::InvalidElement {
216                    invalid_attributes: Default::default(),
217                    invalid_children: Default::default(),
218                    missing_attributes: Default::default(),
219                    missing_children: ["title".into()].into(),
220                }
221            ),
222            "missing children: title"
223        );
224    }
225
226    #[test]
227    fn display_multiple_missing_children() {
228        assert_eq!(
229            format!(
230                "{}",
231                MarkupError::InvalidElement {
232                    invalid_attributes: Default::default(),
233                    invalid_children: Default::default(),
234                    missing_attributes: Default::default(),
235                    missing_children: ["body".into(), "head".into()].into(),
236                }
237            ),
238            "missing children: body, head"
239        );
240    }
241
242    #[test]
243    fn display_missing_attributes_and_children() {
244        assert_eq!(
245            format!(
246                "{}",
247                MarkupError::InvalidElement {
248                    invalid_attributes: Default::default(),
249                    invalid_children: Default::default(),
250                    missing_attributes: ["href".into(), "src".into()].into(),
251                    missing_children: ["img".into(), "source".into()].into(),
252                }
253            ),
254            "missing attributes: href, src, missing children: img, source"
255        );
256    }
257
258    #[test]
259    fn display_not_allowed_attributes_and_children() {
260        assert_eq!(
261            format!(
262                "{}",
263                MarkupError::InvalidElement {
264                    invalid_attributes: [("foo".into(), [AttributeError::NotAllowed].into())]
265                        .into(),
266                    invalid_children: [("bar".into(), [ChildError::NotAllowed].into())].into(),
267                    missing_attributes: Default::default(),
268                    missing_children: Default::default(),
269                }
270            ),
271            "invalid attributes: foo (not allowed), invalid children: bar (not allowed)"
272        );
273    }
274}