Skip to main content

svugy_model/
problem.rs

1//! Describe each problem validation reports, as a diagnostic with its severity, labels and did-you-mean help.
2
3use 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/// Report one problem: something the author must fix (an error) or should look at (a warning).
15#[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/// Report an element whose ancestor is missing: its enclosing container was never declared.
74#[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/// Suggest the known name the author most likely meant.
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct Suggestion<T>(pub T);
89
90impl<T: fmt::Display> Suggestion<T> {
91    /// Suggest c* = argmin over c of levenshtein(lower(unknown), lower(c)), kept iff that distance ≤ ⌊len(unknown) / 3⌋.
92    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/// Report a dangling edge endpoint: a path that is not an element.
120#[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/// Report a duplicate element: one declared twice at the same path.
133#[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/// Report an edge joining an element to its own ancestor or descendant, which layout leaves out.
143#[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/// Report an unknown style key: one outside the v1 set.
153#[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/// Identify the owner of a style entry: an element or an edge.
166#[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/// Report a mismatched style value: a v1 style key set to a value of the wrong kind.
182#[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/// Report an empty grid hint: zero columns, so no cell to place a child in.
195#[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/// Report a packing hint laying children in more tracks, columns or rows, than a packed level can count.
205#[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/// Report a child placement hint on an element without children, which has nothing to place.
221#[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/// Report a pinned position with a coordinate that is not a finite number within `MAX_MAGNITUDE` of its level's origin.
232#[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/// Report an unresolved link: one shaped like an element path that names no element, the likely typo of a drill-down target.
247#[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/// Report a description link whose `#` target names no element or edge, so the runtime shows its text unlinked.
262#[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/// Report a container's authored shape, which v1 ignores: a container is a slab, whose rectangular top holds its interior.
277#[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/// Report a height that is not a positive number within `MAX_MAGNITUDE` plan units, so no solid can stand that tall.
290#[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/// Report a container's authored height, which v1 ignores: a container is a slab, whose tag hangs from its low front face onto the ground.
303#[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/// Report a badge edge with a container at either end, since a badge is drawn from a leaf's top face as a mini of a leaf.
316#[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/// Report a legend dash key that is not a dash length spelled in its shortest form, so no arc's dash can match it.
326#[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/// Report a key row the legend describes but the diagram never draws, so the description is never shown.
336#[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/// Name a key row of the legend: the arcs of one dash length, or the badges of one target.
346#[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
361/// Classify a diagnostic as an error, one that blocks a build or a load, when it says so or states no severity at all.
362pub trait ErrorSeverity {
363    /// Tell whether the diagnostic is an error.
364    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}