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}