ot_tools_io/
traits.rs

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