use std::fmt;
use miette::{Diagnostic, Severity, SourceSpan};
use thiserror::Error;
use crate::description::DescriptionOwner;
use crate::legend::DashKey;
use crate::model::{EdgeId, MAX_MAGNITUDE, PlacementHint, Position, Shape};
use crate::path::ElementPath;
use crate::style::{AnyStyleKey, StyleValueKind};
#[derive(Debug, Error, Diagnostic)]
pub enum Problem {
#[error(transparent)]
#[diagnostic(transparent)]
MissingAncestor(MissingAncestor),
#[error(transparent)]
#[diagnostic(transparent)]
DanglingEdgeEndpoint(DanglingEdgeEndpoint),
#[error(transparent)]
#[diagnostic(transparent)]
DuplicateElement(DuplicateElement),
#[error(transparent)]
#[diagnostic(transparent)]
AncestorDescendantEdge(AncestorDescendantEdge),
#[error(transparent)]
#[diagnostic(transparent)]
UnknownStyleKey(UnknownStyleKey),
#[error(transparent)]
#[diagnostic(transparent)]
MismatchedStyleValue(MismatchedStyleValue),
#[error(transparent)]
#[diagnostic(transparent)]
EmptyGridHint(EmptyGridHint),
#[error(transparent)]
#[diagnostic(transparent)]
UncountablePackedTracks(UncountablePackedTracks),
#[error(transparent)]
#[diagnostic(transparent)]
ChildPlacementWithoutChildren(ChildPlacementWithoutChildren),
#[error(transparent)]
#[diagnostic(transparent)]
UnboundedPosition(UnboundedPosition),
#[error(transparent)]
#[diagnostic(transparent)]
UnresolvedLink(UnresolvedLink),
#[error(transparent)]
#[diagnostic(transparent)]
UnresolvedDescriptionTarget(UnresolvedDescriptionTarget),
#[error(transparent)]
#[diagnostic(transparent)]
IgnoredContainerShape(IgnoredContainerShape),
#[error(transparent)]
#[diagnostic(transparent)]
ImpossibleHeight(ImpossibleHeight),
#[error(transparent)]
#[diagnostic(transparent)]
IgnoredContainerHeight(IgnoredContainerHeight),
#[error(transparent)]
#[diagnostic(transparent)]
BadgeEdgeOnContainer(BadgeEdgeOnContainer),
#[error(transparent)]
#[diagnostic(transparent)]
MalformedLegendDash(MalformedLegendDash),
#[error(transparent)]
#[diagnostic(transparent)]
UndrawnLegendKey(UndrawnLegendKey),
}
#[derive(Debug, Error, Diagnostic)]
#[error("element `{element}` is inside `{ancestor}`, which is not an element")]
#[diagnostic(code(svugy::dangling_reference))]
pub struct MissingAncestor {
pub element: ElementPath,
pub ancestor: ElementPath,
#[help]
pub suggestion: Option<Suggestion<ElementPath>>,
#[label("declared here")]
pub span: Option<SourceSpan>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Suggestion<T>(pub T);
impl<T: fmt::Display> Suggestion<T> {
pub fn closest_to(unknown: &str, candidates: impl IntoIterator<Item = T>) -> Option<Self> {
let lowercase_unknown = unknown.to_lowercase();
let unknown_length = unknown.chars().count();
let max_distance = unknown_length / 3;
let distances = candidates.into_iter().map(|candidate| {
let candidate_text = candidate.to_string();
let lowercase_candidate = candidate_text.to_lowercase();
let distance = strsim::levenshtein(&lowercase_unknown, &lowercase_candidate);
(distance, candidate)
});
let (closest_distance, closest) =
distances.min_by_key(|(distance, _candidate)| *distance)?;
let is_too_far = closest_distance > max_distance;
if is_too_far {
return None;
}
let suggestion = Suggestion(closest);
Some(suggestion)
}
}
impl<T: fmt::Display> fmt::Display for Suggestion<T> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "did you mean `{}`?", self.0)
}
}
#[derive(Debug, Error, Diagnostic)]
#[error("edge `{edge}` points to `{endpoint}`, which is not an element")]
#[diagnostic(code(svugy::dangling_reference))]
pub struct DanglingEdgeEndpoint {
pub edge: EdgeId,
pub endpoint: ElementPath,
#[help]
pub suggestion: Option<Suggestion<ElementPath>>,
#[label("referenced here")]
pub span: Option<SourceSpan>,
}
#[derive(Debug, Error, Diagnostic)]
#[error("element `{element}` is declared more than once")]
#[diagnostic(code(svugy::duplicate_id))]
pub struct DuplicateElement {
pub element: ElementPath,
#[label("declared again here")]
pub span: Option<SourceSpan>,
}
#[derive(Debug, Error, Diagnostic)]
#[error("edge `{edge}` joins an element to its own ancestor, so layout leaves it out")]
#[diagnostic(code(svugy::ancestor_descendant_edge), severity(Warning))]
pub struct AncestorDescendantEdge {
pub edge: EdgeId,
#[label("declared here")]
pub span: Option<SourceSpan>,
}
#[derive(Debug, Error, Diagnostic)]
#[error("{owner} has style `{key}`, which is not a v1 style key")]
#[diagnostic(code(svugy::unknown_style))]
pub struct UnknownStyleKey {
pub owner: StyleOwner,
pub key: String,
#[help]
pub suggestion: Option<Suggestion<&'static str>>,
#[label("styled here")]
pub span: Option<SourceSpan>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StyleOwner {
Element(ElementPath),
Edge(EdgeId),
}
impl fmt::Display for StyleOwner {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Element(path) => write!(formatter, "element `{path}`"),
Self::Edge(id) => write!(formatter, "edge `{id}`"),
}
}
}
#[derive(Debug, Error, Diagnostic)]
#[error("{owner} sets style `{key}` to `{value}`, which is not {expected}")]
#[diagnostic(code(svugy::mismatched_style_value))]
pub struct MismatchedStyleValue {
pub owner: StyleOwner,
pub key: AnyStyleKey,
pub value: String,
pub expected: StyleValueKind,
#[label("styled here")]
pub span: Option<SourceSpan>,
}
#[derive(Debug, Error, Diagnostic)]
#[error("element `{element}` asks for a grid of 0 columns, which has no cell for any child")]
#[diagnostic(code(svugy::impossible_placement))]
pub struct EmptyGridHint {
pub element: ElementPath,
#[label("declared here")]
pub span: Option<SourceSpan>,
}
#[derive(Debug, Error, Diagnostic)]
#[error(
"element `{element}` packs its {child_count} children into {columns} columns and {rows} rows, but a packed level holds at most {max} of either",
max = PlacementHint::MAX_PACKED_TRACKS
)]
#[diagnostic(code(svugy::impossible_placement))]
pub struct UncountablePackedTracks {
pub element: ElementPath,
pub child_count: usize,
pub columns: usize,
pub rows: usize,
#[label("declared here")]
pub span: Option<SourceSpan>,
}
#[derive(Debug, Error, Diagnostic)]
#[error("element `{element}` has child placement `{child_placement:?}` but no children to place")]
#[diagnostic(code(svugy::impossible_placement), severity(Warning))]
pub struct ChildPlacementWithoutChildren {
pub element: ElementPath,
pub child_placement: PlacementHint,
#[label("declared here")]
pub span: Option<SourceSpan>,
}
#[derive(Debug, Error, Diagnostic)]
#[error(
"element `{element}` is pinned at ({}, {}), which is not within {MAX_MAGNITUDE} plan units of its level's origin",
.position.x,
.position.y
)]
#[diagnostic(code(svugy::impossible_placement))]
pub struct UnboundedPosition {
pub element: ElementPath,
pub position: Position,
#[label("declared here")]
pub span: Option<SourceSpan>,
}
#[derive(Debug, Error, Diagnostic)]
#[error(
"element `{element}` links to `{link}`, which is not an element; write `./{link}` if it is a relative URL"
)]
#[diagnostic(code(svugy::unresolved_link), severity(Warning))]
pub struct UnresolvedLink {
pub element: ElementPath,
pub link: String,
#[help]
pub suggestion: Option<Suggestion<ElementPath>>,
#[label("linked here")]
pub span: Option<SourceSpan>,
}
#[derive(Debug, Error, Diagnostic)]
#[error(
"the description of {owner} links to `#{target}`, which names no element `#PATH` or edge `#FROM->TO[n]`"
)]
#[diagnostic(code(svugy::unresolved_description_target), severity(Warning))]
pub struct UnresolvedDescriptionTarget {
pub owner: DescriptionOwner,
pub target: String,
#[help]
pub suggestion: Option<Suggestion<String>>,
#[label("described here")]
pub span: Option<SourceSpan>,
}
#[derive(Debug, Error, Diagnostic)]
#[error(
"element `{element}` is a container, so it is drawn as a slab and its shape `{shape:?}` is ignored in v1"
)]
#[diagnostic(code(svugy::ignored_container_shape), severity(Warning))]
pub struct IgnoredContainerShape {
pub element: ElementPath,
pub shape: Shape,
#[label("shaped here")]
pub span: Option<SourceSpan>,
}
#[derive(Debug, Error, Diagnostic)]
#[error(
"element `{element}` stands {height} plan units tall, which is not a positive number no greater than {MAX_MAGNITUDE}"
)]
#[diagnostic(code(svugy::impossible_height))]
pub struct ImpossibleHeight {
pub element: ElementPath,
pub height: f64,
#[label("declared here")]
pub span: Option<SourceSpan>,
}
#[derive(Debug, Error, Diagnostic)]
#[error(
"element `{element}` is a container, so it is drawn as a slab and its height {height} is ignored in v1"
)]
#[diagnostic(code(svugy::ignored_container_height), severity(Warning))]
pub struct IgnoredContainerHeight {
pub element: ElementPath,
pub height: f64,
#[label("declared here")]
pub span: Option<SourceSpan>,
}
#[derive(Debug, Error, Diagnostic)]
#[error("edge `{edge}` is a badge, which needs a leaf at both ends")]
#[diagnostic(code(svugy::badge_edge_on_container))]
pub struct BadgeEdgeOnContainer {
pub edge: EdgeId,
#[label("declared here")]
pub span: Option<SourceSpan>,
}
#[derive(Debug, Error, Diagnostic)]
#[error(
"the legend describes dash `{dash}`, which is not a dash length in its shortest spelling, e.g. `5` or `1.5`"
)]
#[diagnostic(code(svugy::malformed_legend))]
pub struct MalformedLegendDash {
pub dash: DashKey,
}
#[derive(Debug, Error, Diagnostic)]
#[error("the legend describes {key}, which the diagram never draws")]
#[diagnostic(code(svugy::undrawn_legend_key), severity(Warning))]
pub struct UndrawnLegendKey {
pub key: LegendKey,
#[help]
pub suggestion: Option<Suggestion<ElementPath>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LegendKey {
Dash(DashKey),
Badge(ElementPath),
}
impl fmt::Display for LegendKey {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Dash(dash) => write!(formatter, "the arcs dashed `{dash}`"),
Self::Badge(target) => write!(formatter, "the badges of `{target}`"),
}
}
}
pub trait ErrorSeverity {
fn is_error(&self) -> bool;
}
impl<D: Diagnostic + ?Sized> ErrorSeverity for D {
fn is_error(&self) -> bool {
let severity = self.severity();
let severity_or_error = severity.unwrap_or(Severity::Error);
severity_or_error == Severity::Error
}
}