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