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}