1use alloc::collections::{BTreeMap, BTreeSet};
2use core::fmt::{self, Display, Formatter};
3
4#[derive(Clone, Debug, Eq, PartialEq)]
6pub enum MarkupError {
7 UnknownTag(String),
9 InvalidElement {
11 invalid_attributes: BTreeMap<String, BTreeSet<AttributeError>>,
13 invalid_children: BTreeMap<String, BTreeSet<ChildError>>,
15 missing_attributes: BTreeSet<String>,
17 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#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
83pub enum AttributeError {
84 Conflict,
86 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#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
101pub enum ChildError {
102 Misplaced,
104 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}