Skip to main content

ot_tools_io/
traits.rs

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