Skip to main content

chordsketch_convert/
error.rs

1//! Error and warning types shared by every conversion direction.
2
3/// Reason a conversion can fail.
4///
5/// The `NotImplemented` variant is the placeholder every
6/// pre-implementation function returns. Subsequent issues
7/// (#2053 iReal→ChordPro, #2061 ChordPro→iReal) replace those
8/// returns with real implementations; new variants are added at
9/// the bottom of the enum to preserve compatibility with code that
10/// matches on the existing variants.
11///
12/// Marked `#[non_exhaustive]` so adding a new variant in a follow-up
13/// PR is non-breaking for downstream `match` expressions, matching
14/// the additive-evolution contract documented at the crate root.
15///
16/// # Note for future implementers
17///
18/// Once #2053 / #2061 land, [`Self::InvalidSource`] and
19/// [`Self::UnrepresentableTarget`] will carry parser-derived,
20/// potentially attacker-controlled text. Implementations SHOULD
21/// truncate or sanitise their messages before constructing these
22/// variants; downstream `Display` consumers and log forwarders
23/// MUST NOT assume bounded length until that bound is enforced
24/// upstream. (Same pattern as `chordsketch_ireal::json::truncate_for_message`.)
25#[derive(Debug, Clone, PartialEq, Eq)]
26#[non_exhaustive]
27pub enum ConversionError {
28    /// The conversion direction is recognised but not yet
29    /// implemented in this crate version. The contained `&'static
30    /// str` is the URL of the issue tracking the implementation
31    /// so callers can give a useful diagnostic.
32    NotImplemented(&'static str),
33    /// The source value was structurally invalid and could not be
34    /// converted at all (distinct from a lossy-but-successful
35    /// conversion, which returns `Ok` with warnings).
36    InvalidSource(String),
37    /// The conversion would produce a target value that is not
38    /// representable in the target format (e.g. an out-of-range
39    /// time signature on the iReal side).
40    UnrepresentableTarget(String),
41}
42
43impl std::fmt::Display for ConversionError {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        match self {
46            Self::NotImplemented(tracking_url) => {
47                write!(
48                    f,
49                    "conversion not yet implemented (tracked at {tracking_url})"
50                )
51            }
52            Self::InvalidSource(msg) => write!(f, "invalid source: {msg}"),
53            Self::UnrepresentableTarget(msg) => {
54                write!(f, "unrepresentable target: {msg}")
55            }
56        }
57    }
58}
59
60impl std::error::Error for ConversionError {}
61
62/// A non-fatal information loss recorded during conversion.
63///
64/// Conversions return `Ok(ConversionOutput { warnings, .. })` for
65/// lossy-but-successful runs; callers decide whether to fail on a
66/// non-empty warning list. This keeps the strictness policy in the
67/// caller's hands rather than baking it into the converter.
68///
69/// Marked `#[non_exhaustive]` so additional diagnostic fields can be
70/// appended in a follow-up PR without breaking downstream code that
71/// currently constructs `ConversionWarning` values only via
72/// [`ConversionWarning::new`].
73#[derive(Debug, Clone, PartialEq, Eq)]
74#[non_exhaustive]
75pub struct ConversionWarning {
76    /// Class of information loss.
77    pub kind: WarningKind,
78    /// Human-readable description of what was dropped or
79    /// approximated.
80    pub message: String,
81}
82
83impl ConversionWarning {
84    /// Constructs a warning with the given kind and message.
85    #[must_use]
86    pub fn new(kind: WarningKind, message: impl Into<String>) -> Self {
87        Self {
88            kind,
89            message: message.into(),
90        }
91    }
92}
93
94/// Class of information loss in a [`ConversionWarning`].
95///
96/// New variants are appended; existing variants are stable across
97/// minor versions. Marked `#[non_exhaustive]` so adding a variant
98/// is non-breaking for downstream `match` arms.
99///
100/// `Copy` is intentional here because every variant is fieldless,
101/// matching the per-warning struct (`ConversionWarning`) which
102/// owns the attached message and is therefore not `Copy`.
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104#[non_exhaustive]
105pub enum WarningKind {
106    /// A feature in the source format has no equivalent in the
107    /// target format and was dropped (e.g. lyrics on a
108    /// ChordPro→iReal conversion — iReal has no lyrics surface).
109    LossyDrop,
110    /// A feature in the source format was approximated to the
111    /// nearest equivalent in the target format (e.g. an unusual
112    /// section label mapped to the closest iReal letter).
113    Approximated,
114    /// A feature in the source format is not yet supported by the
115    /// converter, even though the target format could in principle
116    /// represent it. Distinct from `LossyDrop` because resolving
117    /// it is a future converter-side change rather than an inherent
118    /// format limitation.
119    Unsupported,
120}