1use std::fmt;
4
5use miette::{Diagnostic, Severity, SourceSpan};
6use thiserror::Error;
7
8use crate::description::DescriptionOwner;
9use crate::legend::DashKey;
10use crate::model::{EdgeId, MAX_MAGNITUDE, PlacementHint, Position, Shape};
11use crate::path::ElementPath;
12use crate::style::{AnyStyleKey, StyleValueKind};
13
14#[derive(Debug, Error, Diagnostic)]
16pub enum Problem {
17 #[error(transparent)]
18 #[diagnostic(transparent)]
19 MissingAncestor(MissingAncestor),
20 #[error(transparent)]
21 #[diagnostic(transparent)]
22 DanglingEdgeEndpoint(DanglingEdgeEndpoint),
23 #[error(transparent)]
24 #[diagnostic(transparent)]
25 DuplicateElement(DuplicateElement),
26 #[error(transparent)]
27 #[diagnostic(transparent)]
28 AncestorDescendantEdge(AncestorDescendantEdge),
29 #[error(transparent)]
30 #[diagnostic(transparent)]
31 UnknownStyleKey(UnknownStyleKey),
32 #[error(transparent)]
33 #[diagnostic(transparent)]
34 MismatchedStyleValue(MismatchedStyleValue),
35 #[error(transparent)]
36 #[diagnostic(transparent)]
37 EmptyGridHint(EmptyGridHint),
38 #[error(transparent)]
39 #[diagnostic(transparent)]
40 UncountablePackedTracks(UncountablePackedTracks),
41 #[error(transparent)]
42 #[diagnostic(transparent)]
43 ChildPlacementWithoutChildren(ChildPlacementWithoutChildren),
44 #[error(transparent)]
45 #[diagnostic(transparent)]
46 UnboundedPosition(UnboundedPosition),
47 #[error(transparent)]
48 #[diagnostic(transparent)]
49 UnresolvedLink(UnresolvedLink),
50 #[error(transparent)]
51 #[diagnostic(transparent)]
52 UnresolvedDescriptionTarget(UnresolvedDescriptionTarget),
53 #[error(transparent)]
54 #[diagnostic(transparent)]
55 IgnoredContainerShape(IgnoredContainerShape),
56 #[error(transparent)]
57 #[diagnostic(transparent)]
58 ImpossibleHeight(ImpossibleHeight),
59 #[error(transparent)]
60 #[diagnostic(transparent)]
61 IgnoredContainerHeight(IgnoredContainerHeight),
62 #[error(transparent)]
63 #[diagnostic(transparent)]
64 BadgeEdgeOnContainer(BadgeEdgeOnContainer),
65 #[error(transparent)]
66 #[diagnostic(transparent)]
67 MalformedLegendDash(MalformedLegendDash),
68 #[error(transparent)]
69 #[diagnostic(transparent)]
70 UndrawnLegendKey(UndrawnLegendKey),
71}
72
73#[derive(Debug, Error, Diagnostic)]
75#[error("element `{element}` is inside `{ancestor}`, which is not an element")]
76#[diagnostic(code(svugy::dangling_reference))]
77pub struct MissingAncestor {
78 pub element: ElementPath,
79 pub ancestor: ElementPath,
80 #[help]
81 pub suggestion: Option<Suggestion<ElementPath>>,
82 #[label("declared here")]
83 pub span: Option<SourceSpan>,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct Suggestion<T>(pub T);
89
90impl<T: fmt::Display> Suggestion<T> {
91 pub fn closest_to(unknown: &str, candidates: impl IntoIterator<Item = T>) -> Option<Self> {
93 let lowercase_unknown = unknown.to_lowercase();
94 let unknown_length = unknown.chars().count();
95 let max_distance = unknown_length / 3;
96 let distances = candidates.into_iter().map(|candidate| {
97 let candidate_text = candidate.to_string();
98 let lowercase_candidate = candidate_text.to_lowercase();
99 let distance = strsim::levenshtein(&lowercase_unknown, &lowercase_candidate);
100 (distance, candidate)
101 });
102 let (closest_distance, closest) =
103 distances.min_by_key(|(distance, _candidate)| *distance)?;
104 let is_too_far = closest_distance > max_distance;
105 if is_too_far {
106 return None;
107 }
108 let suggestion = Suggestion(closest);
109 Some(suggestion)
110 }
111}
112
113impl<T: fmt::Display> fmt::Display for Suggestion<T> {
114 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
115 write!(formatter, "did you mean `{}`?", self.0)
116 }
117}
118
119#[derive(Debug, Error, Diagnostic)]
121#[error("edge `{edge}` points to `{endpoint}`, which is not an element")]
122#[diagnostic(code(svugy::dangling_reference))]
123pub struct DanglingEdgeEndpoint {
124 pub edge: EdgeId,
125 pub endpoint: ElementPath,
126 #[help]
127 pub suggestion: Option<Suggestion<ElementPath>>,
128 #[label("referenced here")]
129 pub span: Option<SourceSpan>,
130}
131
132#[derive(Debug, Error, Diagnostic)]
134#[error("element `{element}` is declared more than once")]
135#[diagnostic(code(svugy::duplicate_id))]
136pub struct DuplicateElement {
137 pub element: ElementPath,
138 #[label("declared again here")]
139 pub span: Option<SourceSpan>,
140}
141
142#[derive(Debug, Error, Diagnostic)]
144#[error("edge `{edge}` joins an element to its own ancestor, so layout leaves it out")]
145#[diagnostic(code(svugy::ancestor_descendant_edge), severity(Warning))]
146pub struct AncestorDescendantEdge {
147 pub edge: EdgeId,
148 #[label("declared here")]
149 pub span: Option<SourceSpan>,
150}
151
152#[derive(Debug, Error, Diagnostic)]
154#[error("{owner} has style `{key}`, which is not a v1 style key")]
155#[diagnostic(code(svugy::unknown_style))]
156pub struct UnknownStyleKey {
157 pub owner: StyleOwner,
158 pub key: String,
159 #[help]
160 pub suggestion: Option<Suggestion<&'static str>>,
161 #[label("styled here")]
162 pub span: Option<SourceSpan>,
163}
164
165#[derive(Debug, Clone, PartialEq, Eq)]
167pub enum StyleOwner {
168 Element(ElementPath),
169 Edge(EdgeId),
170}
171
172impl fmt::Display for StyleOwner {
173 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
174 match self {
175 Self::Element(path) => write!(formatter, "element `{path}`"),
176 Self::Edge(id) => write!(formatter, "edge `{id}`"),
177 }
178 }
179}
180
181#[derive(Debug, Error, Diagnostic)]
183#[error("{owner} sets style `{key}` to `{value}`, which is not {expected}")]
184#[diagnostic(code(svugy::mismatched_style_value))]
185pub struct MismatchedStyleValue {
186 pub owner: StyleOwner,
187 pub key: AnyStyleKey,
188 pub value: String,
189 pub expected: StyleValueKind,
190 #[label("styled here")]
191 pub span: Option<SourceSpan>,
192}
193
194#[derive(Debug, Error, Diagnostic)]
196#[error("element `{element}` asks for a grid of 0 columns, which has no cell for any child")]
197#[diagnostic(code(svugy::impossible_placement))]
198pub struct EmptyGridHint {
199 pub element: ElementPath,
200 #[label("declared here")]
201 pub span: Option<SourceSpan>,
202}
203
204#[derive(Debug, Error, Diagnostic)]
206#[error(
207 "element `{element}` packs its {child_count} children into {columns} columns and {rows} rows, but a packed level holds at most {max} of either",
208 max = PlacementHint::MAX_PACKED_TRACKS
209)]
210#[diagnostic(code(svugy::impossible_placement))]
211pub struct UncountablePackedTracks {
212 pub element: ElementPath,
213 pub child_count: usize,
214 pub columns: usize,
215 pub rows: usize,
216 #[label("declared here")]
217 pub span: Option<SourceSpan>,
218}
219
220#[derive(Debug, Error, Diagnostic)]
222#[error("element `{element}` has child placement `{child_placement:?}` but no children to place")]
223#[diagnostic(code(svugy::impossible_placement), severity(Warning))]
224pub struct ChildPlacementWithoutChildren {
225 pub element: ElementPath,
226 pub child_placement: PlacementHint,
227 #[label("declared here")]
228 pub span: Option<SourceSpan>,
229}
230
231#[derive(Debug, Error, Diagnostic)]
233#[error(
234 "element `{element}` is pinned at ({}, {}), which is not within {MAX_MAGNITUDE} plan units of its level's origin",
235 .position.x,
236 .position.y
237)]
238#[diagnostic(code(svugy::impossible_placement))]
239pub struct UnboundedPosition {
240 pub element: ElementPath,
241 pub position: Position,
242 #[label("declared here")]
243 pub span: Option<SourceSpan>,
244}
245
246#[derive(Debug, Error, Diagnostic)]
248#[error(
249 "element `{element}` links to `{link}`, which is not an element; write `./{link}` if it is a relative URL"
250)]
251#[diagnostic(code(svugy::unresolved_link), severity(Warning))]
252pub struct UnresolvedLink {
253 pub element: ElementPath,
254 pub link: String,
255 #[help]
256 pub suggestion: Option<Suggestion<ElementPath>>,
257 #[label("linked here")]
258 pub span: Option<SourceSpan>,
259}
260
261#[derive(Debug, Error, Diagnostic)]
263#[error(
264 "the description of {owner} links to `#{target}`, which names no element `#PATH` or edge `#FROM->TO[n]`"
265)]
266#[diagnostic(code(svugy::unresolved_description_target), severity(Warning))]
267pub struct UnresolvedDescriptionTarget {
268 pub owner: DescriptionOwner,
269 pub target: String,
270 #[help]
271 pub suggestion: Option<Suggestion<String>>,
272 #[label("described here")]
273 pub span: Option<SourceSpan>,
274}
275
276#[derive(Debug, Error, Diagnostic)]
278#[error(
279 "element `{element}` is a container, so it is drawn as a slab and its shape `{shape:?}` is ignored in v1"
280)]
281#[diagnostic(code(svugy::ignored_container_shape), severity(Warning))]
282pub struct IgnoredContainerShape {
283 pub element: ElementPath,
284 pub shape: Shape,
285 #[label("shaped here")]
286 pub span: Option<SourceSpan>,
287}
288
289#[derive(Debug, Error, Diagnostic)]
291#[error(
292 "element `{element}` stands {height} plan units tall, which is not a positive number no greater than {MAX_MAGNITUDE}"
293)]
294#[diagnostic(code(svugy::impossible_height))]
295pub struct ImpossibleHeight {
296 pub element: ElementPath,
297 pub height: f64,
298 #[label("declared here")]
299 pub span: Option<SourceSpan>,
300}
301
302#[derive(Debug, Error, Diagnostic)]
304#[error(
305 "element `{element}` is a container, so it is drawn as a slab and its height {height} is ignored in v1"
306)]
307#[diagnostic(code(svugy::ignored_container_height), severity(Warning))]
308pub struct IgnoredContainerHeight {
309 pub element: ElementPath,
310 pub height: f64,
311 #[label("declared here")]
312 pub span: Option<SourceSpan>,
313}
314
315#[derive(Debug, Error, Diagnostic)]
317#[error("edge `{edge}` is a badge, which needs a leaf at both ends")]
318#[diagnostic(code(svugy::badge_edge_on_container))]
319pub struct BadgeEdgeOnContainer {
320 pub edge: EdgeId,
321 #[label("declared here")]
322 pub span: Option<SourceSpan>,
323}
324
325#[derive(Debug, Error, Diagnostic)]
327#[error(
328 "the legend describes dash `{dash}`, which is not a dash length in its shortest spelling, e.g. `5` or `1.5`"
329)]
330#[diagnostic(code(svugy::malformed_legend))]
331pub struct MalformedLegendDash {
332 pub dash: DashKey,
333}
334
335#[derive(Debug, Error, Diagnostic)]
337#[error("the legend describes {key}, which the diagram never draws")]
338#[diagnostic(code(svugy::undrawn_legend_key), severity(Warning))]
339pub struct UndrawnLegendKey {
340 pub key: LegendKey,
341 #[help]
342 pub suggestion: Option<Suggestion<ElementPath>>,
343}
344
345#[derive(Debug, Clone, PartialEq, Eq)]
347pub enum LegendKey {
348 Dash(DashKey),
349 Badge(ElementPath),
350}
351
352impl fmt::Display for LegendKey {
353 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
354 match self {
355 Self::Dash(dash) => write!(formatter, "the arcs dashed `{dash}`"),
356 Self::Badge(target) => write!(formatter, "the badges of `{target}`"),
357 }
358 }
359}
360
361pub trait ErrorSeverity {
363 fn is_error(&self) -> bool;
365}
366
367impl<D: Diagnostic + ?Sized> ErrorSeverity for D {
368 fn is_error(&self) -> bool {
369 let severity = self.severity();
370 let severity_or_error = severity.unwrap_or(Severity::Error);
371 severity_or_error == Severity::Error
372 }
373}