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