1use serde::Serialize;
12use tsify_next::Tsify;
13
14#[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#[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#[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 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 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 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}