ot_tools_io/
traits.rs

1/*
2SPDX-License-Identifier: GPL-3.0-or-later
3Copyright © 2024 Mike Robeson [dijksterhuis]
4*/
5
6use crate::RBoxErr;
7use serde::{Deserialize, Serialize};
8use serde_big_array::Array;
9use std::array::from_fn;
10use std::fmt::Debug;
11use std::path::Path;
12
13/// Convenience trait for types which directly correspond to Elektron Octatrack binary data files.
14/// Associated functions and methods etc. for File I/O, plus a `repr` method for debugging.
15pub trait OctatrackFileIO: Decode + Encode + Serialize + for<'a> Deserialize<'a> {
16    /// Read type from an Octatrack data file at path
17    /// ```rust
18    /// # use std::path::PathBuf;
19    /// # let path = PathBuf::from("test-data").join("blank-project").join("bank01.work");
20    /// use ot_tools_io::{BankFile, OctatrackFileIO};
21    /// assert!(BankFile::from_data_file(&path).unwrap().repr(None).is_ok());
22    /// assert!(BankFile::from_data_file(&path).unwrap().repr(Some(false)).is_ok());
23    /// assert!(BankFile::from_data_file(&path).unwrap().repr(Some(true)).is_ok());
24    /// ```
25    fn repr(&self, newlines: Option<bool>) -> RBoxErr<()>
26    where
27        Self: Debug,
28    {
29        if newlines.unwrap_or(true) {
30            println!("{self:#?}")
31        } else {
32            println!("{self:?}")
33        };
34        Ok(())
35    }
36    // BYTES
37    /// Read type from an Octatrack data file at path
38    /// ```rust
39    /// # use std::path::PathBuf;
40    /// # let path = PathBuf::from("test-data").join("blank-project").join("bank01.work");
41    /// use ot_tools_io::{BankFile, OctatrackFileIO};
42    /// let bank = BankFile::from_data_file(&path).unwrap();
43    /// assert_eq!(bank.datatype_version, 23);
44    /// ```
45    fn from_data_file(path: &Path) -> RBoxErr<Self> {
46        let bytes = crate::read_bin_file(path)?;
47        let data = Self::from_bytes(&bytes)?;
48        Ok(data)
49    }
50    /// Read type from bytes
51    /// ```rust
52    /// // need to create everything from scratch in this example as many bytes otherwise
53    /// use serde::{Deserialize, Serialize};
54    /// use ot_tools_io::{Decode, Encode, OctatrackFileIO};
55    ///
56    /// #[derive(Serialize, Deserialize, Debug, PartialEq)]
57    /// struct Something {
58    ///     value: u8,
59    /// }
60    ///
61    /// impl Decode for Something {}
62    /// impl Encode for Something {}
63    /// impl OctatrackFileIO for Something {}
64    ///
65    /// let x = [10];
66    ///
67    /// let r = Something::from_bytes(&x);
68    /// assert!(r.is_ok());
69    /// assert_eq!(r.unwrap(), Something { value: 10});
70    /// ```
71    fn from_bytes(bytes: &[u8]) -> RBoxErr<Self> {
72        let x = Self::decode(bytes)?;
73        Ok(x)
74    }
75    /// Write type to an Octatrack data file at path
76    /// ```rust
77    /// # use std::{path::PathBuf, fs::create_dir_all, env::temp_dir};
78    /// # let path = temp_dir()
79    /// #    .join("ot-tools-io")
80    /// #    .join("doctest")
81    /// #    .join("to_bytes_file.bank.work");
82    /// # create_dir_all(&path.parent().unwrap()).unwrap();
83    /// #
84    /// use ot_tools_io::{OctatrackFileIO, BankFile};
85    /// let r = BankFile::default().to_data_file(&path);
86    /// assert!(r.is_ok());
87    /// assert!(path.exists());
88    /// ```
89    fn to_data_file(&self, path: &Path) -> RBoxErr<()> {
90        let bytes = Self::to_bytes(self)?;
91        crate::write_bin_file(&bytes, path)?;
92        Ok(())
93    }
94    /// Create bytes from type
95    /// ```rust
96    /// // need to create everything from scratch in this example as many bytes otherwise
97    /// use serde::{Deserialize, Serialize};
98    /// use ot_tools_io::{Decode, Encode, OctatrackFileIO};
99    ///
100    /// #[derive(Serialize, Deserialize, Debug, PartialEq)]
101    /// struct Something {
102    ///     value: u8,
103    /// }
104    ///
105    /// impl Decode for Something {}
106    /// impl Encode for Something {}
107    /// impl OctatrackFileIO for Something {}
108    ///
109    /// let x = Something { value: 10 };
110    /// let r = x.to_bytes();
111    /// assert!(r.is_ok());
112    /// assert_eq!(r.unwrap(), [10]);
113    /// ```
114    fn to_bytes(&self) -> RBoxErr<Vec<u8>> {
115        self.encode()
116    }
117    // YAML
118    /// Read type from a YAML file at path
119    /// ```rust
120    /// # use std::path::PathBuf;
121    /// # let path = PathBuf::from("test-data").join("projects").join("default.yaml");
122    /// use ot_tools_io::{ProjectFile, OctatrackFileIO};
123    /// let project = ProjectFile::from_yaml_file(&path).unwrap();
124    /// assert_eq!(project.metadata.project_version, 19);
125    /// ```
126    fn from_yaml_file(path: &Path) -> RBoxErr<Self> {
127        let string = crate::read_str_file(path)?;
128        let data = Self::from_yaml_str(&string)?;
129        Ok(data)
130    }
131    /// Read type from YAML string
132    /// ```rust
133    /// // need to create everything from scratch in this example
134    /// use serde::{Deserialize, Serialize};
135    /// use ot_tools_io::{Decode, Encode, OctatrackFileIO};
136    ///
137    /// #[derive(Serialize, Deserialize, Debug, PartialEq)]
138    /// struct Something {
139    ///     value: u8,
140    /// }
141    ///
142    /// impl Decode for Something {}
143    /// impl Encode for Something {}
144    /// impl OctatrackFileIO for Something {}
145    ///
146    /// let r = Something::from_yaml_str("value: 10\n");
147    /// assert!(r.is_ok());
148    /// assert_eq!(r.unwrap(), Something { value: 10 });
149    /// ```
150    fn from_yaml_str(yaml: &str) -> RBoxErr<Self> {
151        let x = serde_yml::from_str(yaml)?;
152        Ok(x)
153    }
154    /// Write type to a YAML file at path
155    /// ```rust
156    /// # use std::{path::PathBuf, fs::create_dir_all, env::temp_dir};
157    /// # let path = temp_dir()
158    /// #    .join("ot-tools-io")
159    /// #    .join("doctest")
160    /// #    .join("to_bytes_file.bank.yaml");
161    /// # create_dir_all(&path.parent().unwrap()).unwrap();
162    /// #
163    /// use ot_tools_io::{OctatrackFileIO, BankFile};
164    /// let r = BankFile::default().to_yaml_file(&path);
165    /// assert!(r.is_ok());
166    /// assert!(path.exists());
167    /// ```
168    fn to_yaml_file(&self, path: &Path) -> RBoxErr<()> {
169        let yaml = Self::to_yaml_string(self)?;
170        crate::write_str_file(&yaml, path)?;
171        Ok(())
172    }
173    /// Create YAML string from type
174    /// ```rust
175    /// use ot_tools_io::{BankFile, OctatrackFileIO};
176    /// let bank = BankFile::default().to_yaml_string().unwrap();
177    /// assert_eq!(bank.len(), 12578424);
178    /// assert_eq!(bank[0..15], "header:\n- 70\n- ".to_string());
179    /// ```
180    fn to_yaml_string(&self) -> RBoxErr<String> {
181        Ok(serde_yml::to_string(self)?)
182    }
183    // JSON
184    /// Read type from a JSON file at path
185    fn from_json_file(path: &Path) -> RBoxErr<Self> {
186        let string = crate::read_str_file(path)?;
187        let data = Self::from_yaml_str(&string)?;
188        Ok(data)
189    }
190    /// Create type from JSON string
191    /// ```rust
192    /// // need to create everything from scratch in this example
193    /// use serde::{Deserialize, Serialize};
194    /// use ot_tools_io::{Decode, Encode, OctatrackFileIO};
195    ///
196    /// #[derive(Serialize, Deserialize, Debug, PartialEq)]
197    /// struct Something {
198    ///     value: u8,
199    /// }
200    ///
201    /// impl Decode for Something {}
202    /// impl Encode for Something {}
203    /// impl OctatrackFileIO for Something {}
204    ///
205    /// let r = Something::from_json_str("{\"value\":10}");
206    /// assert!(r.is_ok());
207    /// assert_eq!(r.unwrap(), Something { value: 10 });
208    /// ```
209    fn from_json_str(json: &str) -> RBoxErr<Self> {
210        Ok(serde_json::from_str::<Self>(json)?)
211    }
212    /// Write type to a JSON file at path
213    /// ```rust
214    /// # use std::{path::PathBuf, fs::create_dir_all, env::temp_dir};
215    /// # let path = temp_dir()
216    /// #    .join("ot-tools-io")
217    /// #    .join("doctest")
218    /// #    .join("to_bytes_file.bank.json");
219    /// # create_dir_all(&path.parent().unwrap()).unwrap();
220    /// #
221    /// use ot_tools_io::{OctatrackFileIO, BankFile};
222    /// let r = BankFile::default().to_json_file(&path);
223    /// assert!(r.is_ok());
224    /// assert!(path.exists());
225    /// ```
226    fn to_json_file(&self, path: &Path) -> RBoxErr<()> {
227        let yaml = Self::to_json_string(self)?;
228        crate::write_str_file(&yaml, path)?;
229        Ok(())
230    }
231    /// Create JSON string from type
232    /// ```rust
233    /// use ot_tools_io::{BankFile, OctatrackFileIO};
234    /// let json = BankFile::default().to_json_string().unwrap();
235    /// assert_eq!(json.len(), 7807763);
236    /// assert_eq!(json[0..15], "{\"header\":[70,7".to_string());
237    /// ```
238    fn to_json_string(&self) -> RBoxErr<String> {
239        Ok(serde_json::to_string(&self)?)
240    }
241}
242
243/// Trait to convert between Enum option instances and their corresponding value.
244/// ```
245/// use std::error::Error;
246/// use ot_tools_io::OptionEnumValueConvert;
247///
248/// #[derive(std::fmt::Debug, PartialEq, Copy, Clone)]
249/// enum SomeThing {
250///     X = 0,
251///     Y = 19473295,
252/// }
253///
254/// impl OptionEnumValueConvert<u32> for SomeThing {
255///
256///     fn from_value(v: &u32) -> Result<Self, Box<dyn Error>> {
257///         match v {
258///             0 => Ok(Self::X),
259///             19473295 => Ok(Self::Y),
260///             _ => Err(ot_tools_io::OtToolsIoErrors::NoMatchingOptionEnumValue.into())
261///         }
262///     }
263///     fn value(&self) -> Result<u32, Box<dyn Error>> {
264///         Ok(*self as u32)
265///     }
266/// }
267///
268/// assert_eq!(SomeThing::X.value().unwrap(), 0);
269/// assert_eq!(SomeThing::Y.value().unwrap(), 19473295);
270/// assert_eq!(SomeThing::from_value(&0).unwrap(), SomeThing::X);
271/// assert_eq!(SomeThing::from_value(&19473295).unwrap(), SomeThing::Y);
272/// assert!(SomeThing::from_value(&100).is_err());
273/// assert!(SomeThing::from_value(&200).is_err());
274/// ```
275pub trait OptionEnumValueConvert<V> {
276    /// Get an Enum instance from some value.
277    fn from_value(v: &V) -> RBoxErr<Self>
278    where
279        Self: Sized;
280
281    /// Get a numeric value for an Enum instance.
282    fn value(&self) -> RBoxErr<V>;
283}
284
285/// Adds deserialisation via [`bincode`] and [`serde`] to a type.
286/// Must be present on all major file types.
287/// ```
288/// use std::error::Error;
289/// use ot_tools_io::Decode;
290///
291/// #[derive(serde::Deserialize, std::fmt::Debug, PartialEq)]
292/// struct SomeType {
293///     x: u8,
294///     y: u32,
295/// }
296///
297/// // default implementation
298/// impl Decode for SomeType {}
299///
300/// let bytes: Vec<u8> = vec![8, 127, 95, 245, 1];
301/// let decoded = SomeType::decode(&bytes).unwrap();
302///
303/// assert_eq!(
304///     SomeType { x : 8, y: 32857983 },
305///     decoded,
306/// );
307/// ```
308pub trait Decode {
309    fn decode(bytes: &[u8]) -> RBoxErr<Self>
310    where
311        Self: Sized,
312        Self: for<'a> Deserialize<'a>,
313    {
314        let x: Self = bincode::deserialize(bytes)?;
315        Ok(x)
316    }
317}
318
319/// Adds serialization via [`bincode`] and [`serde`] to a type. Must be present on all major file
320/// types.
321///
322/// ```
323/// use std::error::Error;
324/// use ot_tools_io::Encode;
325///
326/// #[derive(serde::Serialize, std::fmt::Debug)]
327/// struct SomeType {
328///     x: u8,
329///     y: u32,
330/// }
331///
332/// // default implementation
333/// impl Encode for SomeType {}
334///
335/// let x = SomeType { x : 8, y: 32857983 };
336/// let encoded = x.encode().unwrap();
337/// assert_eq!(
338///     vec![8, 127, 95, 245, 1],
339///     encoded,
340/// );
341/// ```
342pub trait Encode {
343    fn encode(&self) -> RBoxErr<Vec<u8>>
344    where
345        Self: Serialize,
346    {
347        Ok(bincode::serialize(&self)?)
348    }
349}
350
351/// Trait for adding a method which swaps the bytes on all fields of a struct.
352///
353/// Useful for handling file writes to a different endianness.
354/// ```
355/// use std::error::Error;
356/// use ot_tools_io::SwapBytes;
357///
358/// #[derive(std::fmt::Debug, PartialEq)]
359/// struct SomeType {
360///     x: u8,
361///     y: u32,
362/// }
363///
364/// impl SwapBytes for SomeType {
365///     fn swap_bytes(self) -> Result<Self, Box<dyn Error>> {
366///         Ok(Self {
367///             x: self.x.swap_bytes(),
368///             y: self.y.swap_bytes(),
369///         })
370///     }
371/// }
372///
373/// let x = SomeType { x : 8, y: 32857983 };
374/// let swapped = x.swap_bytes().unwrap();
375/// assert_eq!(
376///     SomeType { x: 8_u8.swap_bytes(), y: 32857983_u32.swap_bytes()},
377///     swapped,
378/// );
379/// ```
380pub trait SwapBytes {
381    fn swap_bytes(self) -> RBoxErr<Self>
382    where
383        Self: Sized;
384}
385
386/// Used when we need a collection of default type instances
387/// e.g. when creating a default bank we need 16 default patterns.
388/// ```
389/// use ot_tools_io::DefaultsArray;
390///
391/// struct SomeType {
392///     x: u8,
393/// }
394///
395/// impl Default for SomeType {
396///     fn default() -> Self {
397///         Self { x: 0 }
398///     }
399/// }
400///
401/// impl DefaultsArray for SomeType {}
402///
403/// let xs: [SomeType; 20] = SomeType::defaults();
404/// assert_eq!(xs.len(), 20);
405///
406/// let xs = SomeType::defaults::<25>();
407/// assert_eq!(xs.len(), 25);
408/// ```
409pub trait DefaultsArray {
410    /// Create an Array containing `N` default instances of `Self`.
411    fn defaults<const N: usize>() -> [Self; N]
412    where
413        Self: Default,
414    {
415        from_fn(|_| Self::default())
416    }
417}
418
419/// Same as [`DefaultsArray`], but using a boxed [`Array`]
420/// container type
421/// ```
422/// use serde_big_array::Array;
423/// use ot_tools_io::DefaultsArrayBoxed;
424///
425/// struct SomeType {
426///     x: u8,
427/// }
428///
429/// impl Default for SomeType {
430///     fn default() -> Self {
431///         Self { x: 0 }
432///     }
433/// }
434///
435/// impl DefaultsArrayBoxed for SomeType {}
436///
437/// let xs: Box<Array<SomeType, 20>> = SomeType::defaults();
438/// assert_eq!(xs.len(), 20);
439///
440/// let xs = SomeType::defaults::<25>();
441/// assert_eq!(xs.len(), 25);
442/// ```
443pub trait DefaultsArrayBoxed {
444    /// Create a Boxed [Array] containing `N` default instances of `Self`.
445    fn defaults<const N: usize>() -> Box<Array<Self, N>>
446    where
447        Self: Default,
448    {
449        Box::new(Array(from_fn(|_| Self::default())))
450    }
451}
452
453/// Adds a method to check the current data structure matches the default for the type
454/// ```rust
455/// use ot_tools_io::{IsDefault, BankFile};
456///
457/// let mut bank = BankFile::default();
458/// assert_eq!(bank.is_default(), true);
459///
460/// bank.datatype_version = 190;
461/// assert_eq!(bank.is_default(), false);
462/// ```
463pub trait IsDefault {
464    fn is_default(&self) -> bool;
465}
466
467/// Method for calculating the checksum value for types that have a checksum field
468/// ```rust
469/// # use std::path::PathBuf;
470/// # let path = PathBuf::from("test-data")
471/// #     .join("blank-project")
472/// #     .join("bank01.work");
473/// use ot_tools_io::{CalculateChecksum, OctatrackFileIO, BankFile};
474/// let bank = BankFile::from_data_file(&path).unwrap();
475/// assert_eq!(bank.checksum, bank.calculate_checksum().unwrap())
476/// ```
477pub trait CalculateChecksum {
478    fn calculate_checksum(&self) -> RBoxErr<u16>;
479}
480
481/// Adds a method to verify if header(s) are valid in some data.
482/// [See this thread](https://www.elektronauts.com/t/bank-unavailable-octatrack/190647/27).
483/// ```rust
484/// # use std::path::PathBuf;
485/// # let path = PathBuf::from("test-data")
486/// #     .join("blank-project")
487/// #     .join("bank01.work");
488/// use ot_tools_io::{CheckHeader, OctatrackFileIO, BankFile};
489/// assert!(BankFile::from_data_file(&path).unwrap().check_header()) // true for valid header values
490/// ```
491// NOTE: ot-tools-io does not validate headers on file read, which means it is
492// possible to perform checks like this when a data file has been read.
493// otherwise we'd have to do a complicated check to verify headers on every
494// file we read, then throw out an error and probably do some complicated
495// error handling to explain to the end user exactly why we couldn't load the
496// file (bad header, which patterns, which track within patterns etc.).
497pub trait CheckHeader {
498    fn check_header(&self) -> bool;
499}
500
501/// Adds a method to verify if checksum is valid in some data type.
502/// [See this thread](https://www.elektronauts.com/t/bank-unavailable-octatrack/190647/27).
503/// ```rust
504/// # use std::path::PathBuf;
505/// # let path = PathBuf::from("test-data")
506/// #     .join("blank-project")
507/// #     .join("bank01.work");
508/// use ot_tools_io::{CheckChecksum, OctatrackFileIO, BankFile};
509/// // true for valid checksum values
510/// assert!(BankFile::from_data_file(&path).unwrap().check_checksum().unwrap())
511/// ```
512pub trait CheckChecksum {
513    fn check_checksum(&self) -> RBoxErr<bool>;
514}
515
516/// Adds a method to verify if the data file version field is valid for the given type.
517/// ```rust
518/// # use std::path::PathBuf;
519/// # let path = PathBuf::from("test-data")
520/// #     .join("blank-project")
521/// #     .join("bank01.work");
522/// use ot_tools_io::{CheckFileVersion, OctatrackFileIO, BankFile};
523/// // true for valid version values
524/// assert!(BankFile::from_data_file(&path).unwrap().check_file_version().unwrap())
525/// ```
526// NOTE: ot-tools-io does not validate headers on file read, which means it is
527// possible to perform checks like this when a data file has been read.
528// otherwise we'd have to do a complicated check to verify headers on every
529// file we read, then throw out an error and probably do some complicated
530// error handling to explain to the end user exactly why we couldn't load the
531// file (bad header, which patterns, which track within patterns etc.).
532pub trait CheckFileVersion {
533    fn check_file_version(&self) -> RBoxErr<bool>;
534}
535
536/// Adds a single method using the [`CheckHeader::check_header`],
537/// [`CheckChecksum::check_checksum`] and [`CheckFileVersion::check_file_version`]
538/// methods to run a full integrity check.
539/// ```rust
540/// # use std::path::PathBuf;
541/// # let path = PathBuf::from("test-data")
542/// #     .join("blank-project")
543/// #     .join("bank01.work");
544/// use ot_tools_io::{CheckIntegrity, OctatrackFileIO, BankFile};
545/// // true for valid checksum+header values
546/// assert!(BankFile::from_data_file(&path).unwrap().check_integrity().unwrap())
547/// ```
548pub trait CheckIntegrity: CheckHeader + CheckChecksum + CheckFileVersion {
549    fn check_integrity(&self) -> RBoxErr<bool> {
550        Ok(self.check_header() && self.check_checksum()? && self.check_file_version()?)
551    }
552}