Skip to main content

guitar_tab_generator/
error.rs

1//! Error types crossing the WASM boundary.
2//!
3//! `ParseError` is used both internally by the parser and as a leaf of `TabError::Parse`.
4//! `TabError` is the tagged enum the WASM boundary throws on failure.
5//!
6//! `Display` and `Error` are hand-rolled rather than derived via `thiserror`. The wire
7//! format is owned by `tsify-next` (which JS code matches on by `kind`), so the Rust
8//! `Display` form is a developer-facing fallback only and doesn't justify an extra
9//! transitive dependency.
10
11use serde::Serialize;
12use tsify_next::Tsify;
13
14/// One unparseable substring in the input, with its 1-indexed line number.
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Tsify)]
16#[tsify(into_wasm_abi)]
17#[serde(rename_all = "camelCase")]
18pub struct ParseError {
19    pub line: u32,
20    pub text: String,
21}
22
23impl std::fmt::Display for ParseError {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        write!(
26            f,
27            "Input '{}' on line {} could not be parsed into a pitch.",
28            self.text, self.line
29        )
30    }
31}
32
33/// A pitch that could not be played on the configured guitar, with its 1-indexed line number.
34///
35/// Public payload of [`TabError::UnplayablePitches`]. The structured `{ value, line }`
36/// record replaced the free-form prose string used before 2.0.0.
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Tsify)]
38#[tsify(into_wasm_abi)]
39#[serde(rename_all = "camelCase")]
40pub struct UnplayablePitch {
41    pub value: String,
42    pub line: u32,
43}
44
45impl std::fmt::Display for UnplayablePitch {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        write!(
48            f,
49            "Pitch {} on line {} cannot be played on any strings of the configured guitar.",
50            self.value, self.line
51        )
52    }
53}
54
55/// Top-level error variant for the WASM boundary.
56///
57/// Additional variants may be added in a non-breaking release; the `#[non_exhaustive]`
58/// attribute requires external matches to include a wildcard arm. JS consumers should keep a
59/// `default` arm in any `switch (err.kind)`.
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Tsify)]
61#[tsify(into_wasm_abi)]
62#[serde(
63    tag = "kind",
64    rename_all = "camelCase",
65    rename_all_fields = "camelCase"
66)]
67#[non_exhaustive]
68pub enum TabError {
69    Parse {
70        errors: Vec<ParseError>,
71    },
72    /// The input has more lines than the pathfinding graph can index. `max` is the inclusive
73    /// line limit (`u16::MAX`). Distinct from [`TabError::Parse`] because no single line is at
74    /// fault, so there is no `ParseError` line or text to report.
75    InputTooManyLines {
76        max: u32,
77    },
78    NumFretsTooHigh {
79        num_frets: u8,
80        max: u8,
81    },
82    CapoTooHigh {
83        capo: u8,
84        max: u8,
85    },
86    CapoExceedsFrets {
87        capo: u8,
88        num_frets: u8,
89    },
90    StringNumberOutOfRange {
91        value: u8,
92        max: u8,
93    },
94    /// `semitones` is `i16` (not `u8`) to mirror the offset arithmetic in [`crate::pitch::Pitch::plus_offset`]
95    /// and to leave room for negative tuning offsets without a future breaking change. The
96    /// 2.x emit site populates `0..=Guitar::MAX_CAPO` only.
97    OpenPitchOutOfRange {
98        string: u8,
99        semitones: i16,
100    },
101    FretRangeExceedsPitchRange {
102        open_pitch: String,
103        playable_frets: u8,
104    },
105    UnplayablePitches {
106        pitches: Vec<UnplayablePitch>,
107    },
108    NoArrangementsFound,
109    NumArrangementsOutOfRange {
110        value: u8,
111        max: u8,
112    },
113    TuningNameUnknown {
114        value: String,
115    },
116    IndexOutOfBounds {
117        index: usize,
118        len: usize,
119    },
120    /// The requested render `width` is below the minimum needed to lay out one beat at the
121    /// given `padding`. `ArrangementSet::render` rejects widths below `min_render_width(padding)`.
122    RenderWidthTooSmall {
123        width: u16,
124        min: u16,
125    },
126}
127
128impl std::fmt::Display for TabError {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        match self {
131            TabError::Parse { errors } => {
132                if errors.is_empty() {
133                    return write!(f, "Input could not be parsed.");
134                }
135                let joined = errors
136                    .iter()
137                    .map(|e| e.to_string())
138                    .collect::<Vec<_>>()
139                    .join("\n");
140                write!(f, "{joined}")
141            }
142            TabError::InputTooManyLines { max } => {
143                write!(f, "The input is too large. The maximum is {max} lines.")
144            }
145            TabError::NumFretsTooHigh { num_frets, max } => {
146                write!(f, "Too many frets ({num_frets}). The maximum is {max}.")
147            }
148            TabError::CapoTooHigh { capo, max } => {
149                write!(
150                    f,
151                    "The capo fret ({capo}) is too high. The maximum is {max}."
152                )
153            }
154            TabError::CapoExceedsFrets { capo, num_frets } => {
155                write!(
156                    f,
157                    "The capo fret ({capo}) cannot exceed the number of frets ({num_frets})."
158                )
159            }
160            TabError::StringNumberOutOfRange { value, max } => {
161                if *value == 0 {
162                    write!(
163                        f,
164                        "A guitar cannot have a string number of zero (0). Guitar string numbering commences at one (1)."
165                    )
166                } else {
167                    write!(
168                        f,
169                        "The string number ({value}) is too high. The maximum is {max}."
170                    )
171                }
172            }
173            TabError::OpenPitchOutOfRange { string, semitones } => {
174                write!(
175                    f,
176                    "Capo offset of {semitones} semitones on string {string} would push the open pitch out of the supported range."
177                )
178            }
179            TabError::FretRangeExceedsPitchRange {
180                open_pitch,
181                playable_frets,
182            } => {
183                write!(
184                    f,
185                    "Too many frets ({playable_frets}) for string starting at pitch {open_pitch}. The highest playable pitch is B9."
186                )
187            }
188            TabError::UnplayablePitches { pitches } => {
189                if pitches.is_empty() {
190                    return write!(
191                        f,
192                        "Some pitches could not be played on the configured guitar."
193                    );
194                }
195                let joined = pitches
196                    .iter()
197                    .map(|p| p.to_string())
198                    .collect::<Vec<_>>()
199                    .join("\n");
200                write!(f, "{joined}")
201            }
202            TabError::NoArrangementsFound => {
203                write!(f, "No arrangements could be calculated.")
204            }
205            TabError::NumArrangementsOutOfRange { value, max } => {
206                write!(
207                    f,
208                    "The number of arrangements ({value}) must be between 1 and {max}."
209                )
210            }
211            TabError::TuningNameUnknown { value } => {
212                write!(
213                    f,
214                    "The tuning name ({value:?}) is not recognized. Use \"standard\" or another supported tuning name."
215                )
216            }
217            TabError::IndexOutOfBounds { index, len } => {
218                write!(f, "index {index} is out of bounds for set of length {len}")
219            }
220            TabError::RenderWidthTooSmall { width, min } => {
221                write!(
222                    f,
223                    "The render width ({width}) is too small. The minimum is {min}."
224                )
225            }
226        }
227    }
228}
229
230impl std::error::Error for TabError {}
231
232#[cfg(test)]
233mod test_parse_error_display {
234    use super::*;
235
236    #[test]
237    fn reproduces_legacy_message_format() {
238        let err = ParseError {
239            line: 4,
240            text: "BB.2".to_owned(),
241        };
242        assert_eq!(
243            err.to_string(),
244            "Input 'BB.2' on line 4 could not be parsed into a pitch."
245        );
246    }
247}
248
249#[cfg(test)]
250mod test_tab_error_display {
251    use super::*;
252
253    #[test]
254    fn parse_variant_joins_errors_with_newlines() {
255        let err = TabError::Parse {
256            errors: vec![
257                ParseError {
258                    line: 1,
259                    text: "xyz".to_owned(),
260                },
261                ParseError {
262                    line: 4,
263                    text: "BB.2".to_owned(),
264                },
265            ],
266        };
267        assert_eq!(
268            err.to_string(),
269            "Input 'xyz' on line 1 could not be parsed into a pitch.\nInput 'BB.2' on line 4 could not be parsed into a pitch."
270        );
271    }
272
273    #[test]
274    fn parse_variant_with_empty_errors_falls_back_to_a_message() {
275        let err = TabError::Parse { errors: vec![] };
276        assert_eq!(err.to_string(), "Input could not be parsed.");
277    }
278
279    #[test]
280    fn unplayable_pitches_with_empty_vec_falls_back_to_a_message() {
281        let err = TabError::UnplayablePitches { pitches: vec![] };
282        assert_eq!(
283            err.to_string(),
284            "Some pitches could not be played on the configured guitar."
285        );
286    }
287}
288
289#[cfg(test)]
290mod test_new_variant_display {
291    use super::*;
292
293    #[test]
294    fn input_too_many_lines() {
295        let err = TabError::InputTooManyLines { max: 65535 };
296        assert_eq!(
297            err.to_string(),
298            "The input is too large. The maximum is 65535 lines."
299        );
300    }
301
302    #[test]
303    fn num_frets_too_high() {
304        let err = TabError::NumFretsTooHigh {
305            num_frets: 31,
306            max: 30,
307        };
308        assert_eq!(err.to_string(), "Too many frets (31). The maximum is 30.");
309    }
310
311    #[test]
312    fn capo_too_high() {
313        let err = TabError::CapoTooHigh { capo: 9, max: 8 };
314        assert_eq!(
315            err.to_string(),
316            "The capo fret (9) is too high. The maximum is 8."
317        );
318    }
319
320    #[test]
321    fn capo_exceeds_frets() {
322        let err = TabError::CapoExceedsFrets {
323            capo: 8,
324            num_frets: 2,
325        };
326        assert_eq!(
327            err.to_string(),
328            "The capo fret (8) cannot exceed the number of frets (2)."
329        );
330    }
331
332    #[test]
333    fn string_number_out_of_range_zero() {
334        let err = TabError::StringNumberOutOfRange { value: 0, max: 12 };
335        assert_eq!(
336            err.to_string(),
337            "A guitar cannot have a string number of zero (0). Guitar string numbering commences at one (1)."
338        );
339    }
340
341    #[test]
342    fn string_number_out_of_range_above_max() {
343        let err = TabError::StringNumberOutOfRange { value: 13, max: 12 };
344        assert_eq!(
345            err.to_string(),
346            "The string number (13) is too high. The maximum is 12."
347        );
348    }
349
350    #[test]
351    fn open_pitch_out_of_range() {
352        let err = TabError::OpenPitchOutOfRange {
353            string: 1,
354            semitones: 8,
355        };
356        assert_eq!(
357            err.to_string(),
358            "Capo offset of 8 semitones on string 1 would push the open pitch out of the supported range."
359        );
360    }
361
362    #[test]
363    fn fret_range_exceeds_pitch_range() {
364        let err = TabError::FretRangeExceedsPitchRange {
365            open_pitch: "G9".to_owned(),
366            playable_frets: 5,
367        };
368        assert_eq!(
369            err.to_string(),
370            "Too many frets (5) for string starting at pitch G9. The highest playable pitch is B9."
371        );
372    }
373
374    #[test]
375    fn unplayable_pitches_joins_with_newlines() {
376        let err = TabError::UnplayablePitches {
377            pitches: vec![
378                UnplayablePitch {
379                    value: "A1".to_owned(),
380                    line: 1,
381                },
382                UnplayablePitch {
383                    value: "B1".to_owned(),
384                    line: 4,
385                },
386            ],
387        };
388        assert_eq!(
389            err.to_string(),
390            concat!(
391                "Pitch A1 on line 1 cannot be played on any strings of the configured guitar.",
392                "\n",
393                "Pitch B1 on line 4 cannot be played on any strings of the configured guitar.",
394            )
395        );
396    }
397
398    #[test]
399    fn unplayable_pitch_display_reproduces_legacy_message() {
400        let pitch = UnplayablePitch {
401            value: "A1".to_owned(),
402            line: 3,
403        };
404        assert_eq!(
405            pitch.to_string(),
406            "Pitch A1 on line 3 cannot be played on any strings of the configured guitar."
407        );
408    }
409
410    #[test]
411    fn no_arrangements_found() {
412        let err = TabError::NoArrangementsFound;
413        assert_eq!(err.to_string(), "No arrangements could be calculated.");
414    }
415
416    #[test]
417    fn num_arrangements_out_of_range() {
418        let err = TabError::NumArrangementsOutOfRange { value: 21, max: 20 };
419        assert_eq!(
420            err.to_string(),
421            "The number of arrangements (21) must be between 1 and 20."
422        );
423    }
424
425    #[test]
426    fn tuning_name_unknown() {
427        let err = TabError::TuningNameUnknown {
428            value: "openZ".to_owned(),
429        };
430        assert_eq!(
431            err.to_string(),
432            "The tuning name (\"openZ\") is not recognized. Use \"standard\" or another supported tuning name."
433        );
434    }
435
436    #[test]
437    fn index_out_of_bounds() {
438        let err = TabError::IndexOutOfBounds { index: 99, len: 3 };
439        assert_eq!(
440            err.to_string(),
441            "index 99 is out of bounds for set of length 3"
442        );
443    }
444
445    #[test]
446    fn render_width_too_small() {
447        let err = TabError::RenderWidthTooSmall { width: 3, min: 4 };
448        assert_eq!(
449            err.to_string(),
450            "The render width (3) is too small. The minimum is 4."
451        );
452    }
453}