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}