ot_tools_io/
projects.rs

1/*
2SPDX-License-Identifier: GPL-3.0-or-later
3Copyright © 2024 Mike Robeson [dijksterhuis]
4*/
5
6//! Types and ser/de of `project.*` data files.
7
8pub mod metadata;
9pub mod settings;
10pub mod slots;
11pub mod states;
12
13use crate::projects::{
14    metadata::OsMetadata, settings::Settings, slots::SlotsAttributes, states::State,
15};
16use crate::{Decode, Encode, IsDefault, OptionEnumValueConvert, OtToolsIoErrors, RBoxErr};
17use ot_tools_io_derive::OctatrackFile;
18use serde::{Deserialize, Serialize};
19use std::cmp::PartialEq;
20use std::{collections::HashMap, fmt::Debug, str::FromStr};
21
22/// Trait to use when a new struct can be created from some hashmap with all the necessary fields.
23trait FromHashMap {
24    /// Type for `HashMap` keys
25    type A;
26
27    /// Type for `HashMap` values
28    type B;
29
30    /// Type for `Self`
31    type T;
32
33    /// Crete a new struct from a `HashMap`.
34    fn from_hashmap(hmap: &HashMap<Self::A, Self::B>) -> RBoxErr<Self::T>;
35}
36
37/// Return the string value of a `HashMap<_, String>` parsed into specified type `T`
38fn parse_hashmap_string_value<T: FromStr>(
39    hmap: &HashMap<String, String>,
40    key: &str,
41    default_str: Option<&str>,
42) -> Result<T, <T as FromStr>::Err>
43where
44    <T as FromStr>::Err: Debug,
45{
46    match default_str {
47        Some(x) => hmap.get(key).unwrap_or(&x.to_string()).parse::<T>(),
48        None => hmap.get(key).unwrap().parse::<T>(),
49    }
50}
51
52/// Return the string value of a `HashMap<_, String>` parsed into a boolean value
53/// (any parsed value != 1 returns `false`)
54fn parse_hashmap_string_value_bool(
55    hmap: &HashMap<String, String>,
56    key: &str,
57    default_str: Option<&str>,
58) -> RBoxErr<bool> {
59    // NOTE: https://rust-lang.github.io/rust-clippy/master/index.html#match_like_matches_macro
60    Ok(matches!(
61        parse_hashmap_string_value::<u8>(hmap, key, default_str)?,
62        1
63    ))
64}
65
66/// Extract ASCII string project data for a specified section as a HashMap of k-v pairs.
67fn string_to_hashmap(
68    data: &str,
69    section: &ProjectRawFileSection,
70) -> RBoxErr<HashMap<String, String>> {
71    let start_idx: usize = data.find(&section.start_string()?).unwrap();
72    let start_idx_shifted: usize = start_idx + section.start_string()?.len();
73    let end_idx: usize = data.find(&section.end_string()?).unwrap();
74
75    let section: String = data[start_idx_shifted..end_idx].to_string();
76
77    let mut hmap: HashMap<String, String> = HashMap::new();
78    let mut trig_mode_midi_field_idx = 1;
79
80    for split_s in section.split("\r\n") {
81        // new line splits returns empty fields :/
82
83        if !split_s.is_empty() {
84            let key_pair_string = split_s.to_string();
85            let mut key_pair_split: Vec<&str> = key_pair_string.split('=').collect();
86
87            // there are 8x TRIG_MODE_MIDI key value pairs in project settings data
88            // but the keys do not have audio track number indicators. i assume they're
89            // stored in order of the midi track number, and each subsequent one we
90            // read is the next track.
91            let key_renamed: String = format!("trig_mode_midi_track_{}", &trig_mode_midi_field_idx);
92            if key_pair_split[0] == "TRIG_MODE_MIDI" {
93                key_pair_split[0] = key_renamed.as_str();
94                trig_mode_midi_field_idx += 1;
95            }
96
97            hmap.insert(
98                key_pair_split[0].to_string().to_ascii_lowercase(),
99                key_pair_split[1].to_string(),
100            );
101        }
102    }
103
104    Ok(hmap)
105}
106
107/// ASCII data section headings within an Octatrack `project.*` file
108#[derive(Debug, PartialEq)]
109enum ProjectRawFileSection {
110    Meta,
111    States,
112    Settings,
113    Samples,
114}
115
116impl OptionEnumValueConvert<String> for ProjectRawFileSection {
117    fn from_value(v: &String) -> RBoxErr<Self> {
118        match v.to_ascii_uppercase().as_str() {
119            "META" => Ok(Self::Meta),
120            "STATES" => Ok(Self::States),
121            "SETTINGS" => Ok(Self::Settings),
122            "SAMPLES" => Ok(Self::Samples),
123            _ => Err(OtToolsIoErrors::NoMatchingOptionEnumValue.into()),
124        }
125    }
126
127    // TODO: doesn't need a Result here as should never error
128    fn value(&self) -> RBoxErr<String> {
129        match self {
130            Self::Meta => Ok("META".to_string()),
131            Self::States => Ok("STATES".to_string()),
132            Self::Settings => Ok("SETTINGS".to_string()),
133            Self::Samples => Ok("SAMPLES".to_string()),
134        }
135    }
136}
137
138impl ProjectRawFileSection {
139    fn start_string(&self) -> RBoxErr<String> {
140        Ok(format!("[{}]", self.value()?))
141    }
142    fn end_string(&self) -> RBoxErr<String> {
143        Ok(format!("[/{}]", self.value()?))
144    }
145}
146
147/// A parsed representation of an Octatrack Project file (`project.work` or `project.strd`).
148///
149/// **Note**: We derive [`serde::Deserialize`] and [`serde::Serialize`] on this (and all the included subtypes).
150/// But project files are actually string data being parsed directly without
151/// any [`serde`]-ing or [`bincode`]-ing. This may change in the future to custom
152/// Serialize/Deserialize implementations for string source file data.
153/// But that will only happen if i can work out how to differentiate between
154/// deserializing a yaml/json string and a raw data file's string data
155/// (we use [`serde::de::Deserializer::is_human_readable`] for arrangements
156/// to differentiate between YAML/JSON and string data files, which is a problem here as
157/// we'll be testing for the difference between YAML/JSON (string data) and String (string data).)
158// TODO: Switch to custom Deserialize/Serialize parsing from string data,
159//       as per arrangements.
160#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, OctatrackFile)]
161pub struct ProjectFile {
162    /// Metadata key-value pairs from a Project file.
163    pub metadata: OsMetadata,
164
165    /// Settings key-value pairs from a Project file.
166    pub settings: Settings,
167
168    /// States key-value pairs from a Project file.
169    pub states: State,
170
171    /// Slots key-value pairs from a Project file.
172    pub slots: SlotsAttributes,
173}
174
175impl Default for ProjectFile {
176    fn default() -> Self {
177        let metadata = OsMetadata::default();
178        let states = State::default();
179        let settings = Settings::default();
180        let slots = SlotsAttributes::default();
181
182        ProjectFile {
183            metadata,
184            settings,
185            states,
186            slots,
187        }
188    }
189}
190
191impl IsDefault for ProjectFile {
192    fn is_default(&self) -> bool {
193        &ProjectFile::default() == self
194    }
195}
196
197impl std::fmt::Display for ProjectFile {
198    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
199        let states_header =
200            "############################\r\n# Project States\r\n############################"
201                .to_string();
202        let settings_header =
203            "############################\r\n# Project Settings\r\n############################"
204                .to_string();
205        let slots_header =
206            "############################\r\n# Samples\r\n############################".to_string();
207        let footer = "############################".to_string();
208
209        let metadata_string: String = self.metadata.to_string();
210        let states_string: String = self.states.to_string();
211        let settings_string: String = self.settings.to_string();
212
213        let sample_slots_string = self.slots.to_string();
214
215        let v: Vec<String> = vec![
216            // META and SETTINGS are subsections of 'Project Settings'...
217            // so have to do this joining in `ProjectFile`
218            settings_header,
219            metadata_string,
220            settings_string,
221            states_header,
222            states_string,
223            slots_header,
224            sample_slots_string,
225            footer,
226        ];
227        let mut project_string = v.join("\r\n\r\n");
228        project_string.push_str("\r\n\r\n");
229
230        write!(f, "{project_string}")
231    }
232}
233
234// For project data, need to read bytes as an utf string, then split the structs out from the string
235// data.
236impl Decode for ProjectFile {
237    fn decode(bytes: &[u8]) -> RBoxErr<Self> {
238        let s = std::str::from_utf8(bytes)?.to_string();
239
240        let metadata = OsMetadata::from_str(&s)?;
241        let states = State::from_str(&s)?;
242        let settings = Settings::from_str(&s)?;
243        let slots = SlotsAttributes::from_str(&s)?;
244
245        Ok(Self {
246            metadata,
247            settings,
248            states,
249            slots,
250        })
251    }
252}
253
254// For project data, need to convert to string values again then into bytes
255impl Encode for ProjectFile {
256    fn encode(&self) -> RBoxErr<Vec<u8>> {
257        let data = self.to_string();
258        let bytes: Vec<u8> = data.bytes().collect::<Vec<u8>>();
259        Ok(bytes)
260    }
261}
262
263#[cfg(test)]
264#[allow(unused_imports)]
265mod tests {
266    use super::*;
267
268    const DEFAULT_STR_FILE: &str = "############################\r\n# Project Settings\r\n############################\r\n\r\n[META]\r\nTYPE=OCTATRACK DPS-1 PROJECT\r\nVERSION=19\r\nOS_VERSION=R0177     1.40B\r\n[/META]\r\n\r\n[SETTINGS]\r\nWRITEPROTECTED=0\r\nTEMPOx24=2880\r\nPATTERN_TEMPO_ENABLED=0\r\nMIDI_CLOCK_SEND=0\r\nMIDI_CLOCK_RECEIVE=0\r\nMIDI_TRANSPORT_SEND=0\r\nMIDI_TRANSPORT_RECEIVE=0\r\nMIDI_PROGRAM_CHANGE_SEND=0\r\nMIDI_PROGRAM_CHANGE_SEND_CH=-1\r\nMIDI_PROGRAM_CHANGE_RECEIVE=0\r\nMIDI_PROGRAM_CHANGE_RECEIVE_CH=-1\r\nMIDI_TRIG_CH1=0\r\nMIDI_TRIG_CH2=1\r\nMIDI_TRIG_CH3=2\r\nMIDI_TRIG_CH4=3\r\nMIDI_TRIG_CH5=4\r\nMIDI_TRIG_CH6=5\r\nMIDI_TRIG_CH7=6\r\nMIDI_TRIG_CH8=7\r\nMIDI_AUTO_CHANNEL=10\r\nMIDI_SOFT_THRU=0\r\nMIDI_AUDIO_TRK_CC_IN=1\r\nMIDI_AUDIO_TRK_CC_OUT=3\r\nMIDI_AUDIO_TRK_NOTE_IN=1\r\nMIDI_AUDIO_TRK_NOTE_OUT=3\r\nMIDI_MIDI_TRK_CC_IN=1\r\nPATTERN_CHANGE_CHAIN_BEHAVIOR=0\r\nPATTERN_CHANGE_AUTO_SILENCE_TRACKS=0\r\nPATTERN_CHANGE_AUTO_TRIG_LFOS=0\r\nLOAD_24BIT_FLEX=0\r\nDYNAMIC_RECORDERS=0\r\nRECORD_24BIT=0\r\nRESERVED_RECORDER_COUNT=8\r\nRESERVED_RECORDER_LENGTH=16\r\nINPUT_DELAY_COMPENSATION=0\r\nGATE_AB=127\r\nGATE_CD=127\r\nGAIN_AB=64\r\nGAIN_CD=64\r\nDIR_AB=0\r\nDIR_CD=0\r\nPHONES_MIX=64\r\nMAIN_TO_CUE=0\r\nMASTER_TRACK=0\r\nCUE_STUDIO_MODE=0\r\nMAIN_LEVEL=64\r\nCUE_LEVEL=64\r\nMETRONOME_TIME_SIGNATURE=3\r\nMETRONOME_TIME_SIGNATURE_DENOMINATOR=2\r\nMETRONOME_PREROLL=0\r\nMETRONOME_CUE_VOLUME=32\r\nMETRONOME_MAIN_VOLUME=0\r\nMETRONOME_PITCH=12\r\nMETRONOME_TONAL=1\r\nMETRONOME_ENABLED=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\n[/SETTINGS]\r\n\r\n############################\r\n# Project States\r\n############################\r\n\r\n[STATES]\r\nBANK=0\r\nPATTERN=0\r\nARRANGEMENT=0\r\nARRANGEMENT_MODE=0\r\nPART=0\r\nTRACK=0\r\nTRACK_OTHERMODE=0\r\nSCENE_A_MUTE=0\r\nSCENE_B_MUTE=0\r\nTRACK_CUE_MASK=0\r\nTRACK_MUTE_MASK=0\r\nTRACK_SOLO_MASK=0\r\nMIDI_TRACK_MUTE_MASK=0\r\nMIDI_TRACK_SOLO_MASK=0\r\nMIDI_MODE=0\r\n[/STATES]\r\n\r\n############################\r\n# Samples\r\n############################\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=129\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=130\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=131\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=132\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=133\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=134\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=135\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=136\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n############################\r\n\r\n";
269    const DEFAULT_STR_META: &str = "[META]\r\nTYPE=OCTATRACK DPS-1 PROJECT\r\nVERSION=19\r\nOS_VERSION=R0177     1.40B\r\n[/META]";
270    const DEFAULT_STR_STATE: &str = "[STATES]\r\nBANK=0\r\nPATTERN=0\r\nARRANGEMENT=0\r\nARRANGEMENT_MODE=0\r\nPART=0\r\nTRACK=0\r\nTRACK_OTHERMODE=0\r\nSCENE_A_MUTE=0\r\nSCENE_B_MUTE=0\r\nTRACK_CUE_MASK=0\r\nTRACK_MUTE_MASK=0\r\nTRACK_SOLO_MASK=0\r\nMIDI_TRACK_MUTE_MASK=0\r\nMIDI_TRACK_SOLO_MASK=0\r\nMIDI_MODE=0\r\n[/STATES]";
271    const DEFAULT_STR_SETTINGS: &str = "[SETTINGS]\r\nWRITEPROTECTED=0\r\nTEMPOx24=2880\r\nPATTERN_TEMPO_ENABLED=0\r\nMIDI_CLOCK_SEND=0\r\nMIDI_CLOCK_RECEIVE=0\r\nMIDI_TRANSPORT_SEND=0\r\nMIDI_TRANSPORT_RECEIVE=0\r\nMIDI_PROGRAM_CHANGE_SEND=0\r\nMIDI_PROGRAM_CHANGE_SEND_CH=-1\r\nMIDI_PROGRAM_CHANGE_RECEIVE=0\r\nMIDI_PROGRAM_CHANGE_RECEIVE_CH=-1\r\nMIDI_TRIG_CH1=0\r\nMIDI_TRIG_CH2=1\r\nMIDI_TRIG_CH3=2\r\nMIDI_TRIG_CH4=3\r\nMIDI_TRIG_CH5=4\r\nMIDI_TRIG_CH6=5\r\nMIDI_TRIG_CH7=6\r\nMIDI_TRIG_CH8=7\r\nMIDI_AUTO_CHANNEL=10\r\nMIDI_SOFT_THRU=0\r\nMIDI_AUDIO_TRK_CC_IN=1\r\nMIDI_AUDIO_TRK_CC_OUT=3\r\nMIDI_AUDIO_TRK_NOTE_IN=1\r\nMIDI_AUDIO_TRK_NOTE_OUT=3\r\nMIDI_MIDI_TRK_CC_IN=1\r\nPATTERN_CHANGE_CHAIN_BEHAVIOR=0\r\nPATTERN_CHANGE_AUTO_SILENCE_TRACKS=0\r\nPATTERN_CHANGE_AUTO_TRIG_LFOS=0\r\nLOAD_24BIT_FLEX=0\r\nDYNAMIC_RECORDERS=0\r\nRECORD_24BIT=0\r\nRESERVED_RECORDER_COUNT=8\r\nRESERVED_RECORDER_LENGTH=16\r\nINPUT_DELAY_COMPENSATION=0\r\nGATE_AB=127\r\nGATE_CD=127\r\nGAIN_AB=64\r\nGAIN_CD=64\r\nDIR_AB=0\r\nDIR_CD=0\r\nPHONES_MIX=64\r\nMAIN_TO_CUE=0\r\nMASTER_TRACK=0\r\nCUE_STUDIO_MODE=0\r\nMAIN_LEVEL=64\r\nCUE_LEVEL=64\r\nMETRONOME_TIME_SIGNATURE=3\r\nMETRONOME_TIME_SIGNATURE_DENOMINATOR=2\r\nMETRONOME_PREROLL=0\r\nMETRONOME_CUE_VOLUME=32\r\nMETRONOME_MAIN_VOLUME=0\r\nMETRONOME_PITCH=12\r\nMETRONOME_TONAL=1\r\nMETRONOME_ENABLED=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\n[/SETTINGS]";
272    const DEFAULT_STR_SLOTS: &str = "[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=129\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=130\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=131\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=132\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=133\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=134\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=135\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=136\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]";
273
274    mod test_spec {
275        use super::*;
276
277        #[test]
278        fn section_heading_from_value_no_match_is_err() {
279            assert!(ProjectRawFileSection::from_value(&"skfsdkfjskdh".to_string()).is_err())
280        }
281
282        #[test]
283        fn section_heading_from_value_uppercase_meta() {
284            assert_eq!(
285                ProjectRawFileSection::from_value(&"META".to_string()).unwrap(),
286                ProjectRawFileSection::Meta,
287            )
288        }
289
290        #[test]
291        fn section_heading_from_value_uppercase_states() {
292            assert_eq!(
293                ProjectRawFileSection::from_value(&"STATES".to_string()).unwrap(),
294                ProjectRawFileSection::States,
295            )
296        }
297
298        #[test]
299        fn section_heading_from_value_uppercase_settings() {
300            assert_eq!(
301                ProjectRawFileSection::from_value(&"SETTINGS".to_string()).unwrap(),
302                ProjectRawFileSection::Settings,
303            )
304        }
305
306        #[test]
307        fn section_heading_from_value_uppercase_samples() {
308            assert_eq!(
309                ProjectRawFileSection::from_value(&"SAMPLES".to_string()).unwrap(),
310                ProjectRawFileSection::Samples,
311            )
312        }
313
314        #[test]
315        fn section_heading_from_value_lowercase_meta() {
316            assert_eq!(
317                ProjectRawFileSection::from_value(&"meta".to_string()).unwrap(),
318                ProjectRawFileSection::Meta,
319            )
320        }
321
322        #[test]
323        fn section_heading_from_value_lowercase_states() {
324            assert_eq!(
325                ProjectRawFileSection::from_value(&"states".to_string()).unwrap(),
326                ProjectRawFileSection::States,
327            )
328        }
329
330        #[test]
331        fn section_heading_from_value_lowercase_settings() {
332            assert_eq!(
333                ProjectRawFileSection::from_value(&"settings".to_string()).unwrap(),
334                ProjectRawFileSection::Settings,
335            )
336        }
337
338        #[test]
339        fn section_heading_from_value_lowercase_samples() {
340            assert_eq!(
341                ProjectRawFileSection::from_value(&"samples".to_string()).unwrap(),
342                ProjectRawFileSection::Samples,
343            )
344        }
345
346        #[test]
347        fn section_heading_value_meta() {
348            assert_eq!(
349                ProjectRawFileSection::Meta.value().unwrap(),
350                "META".to_string(),
351            )
352        }
353
354        #[test]
355        fn section_heading_value_states() {
356            assert_eq!(
357                ProjectRawFileSection::States.value().unwrap(),
358                "STATES".to_string(),
359            )
360        }
361
362        #[test]
363        fn section_heading_value_settings() {
364            assert_eq!(
365                ProjectRawFileSection::Settings.value().unwrap(),
366                "SETTINGS".to_string(),
367            )
368        }
369
370        #[test]
371        fn section_heading_value_samples() {
372            assert_eq!(
373                ProjectRawFileSection::Samples.value().unwrap(),
374                "SAMPLES".to_string(),
375            )
376        }
377    }
378
379    // make sure to_string() and display() for a project type displays
380    // the file format representation, not a debug struct
381    mod test_to_string_display {
382        use super::*;
383
384        #[test]
385        fn test_full_to_string() {
386            assert_eq!(ProjectFile::default().to_string(), DEFAULT_STR_FILE);
387            assert_eq!(format!["{:#}", ProjectFile::default()], DEFAULT_STR_FILE);
388        }
389
390        #[test]
391        fn test_metadata_to_string() {
392            assert_eq!(OsMetadata::default().to_string(), DEFAULT_STR_META);
393            assert_eq!(format!["{:#}", OsMetadata::default()], DEFAULT_STR_META);
394        }
395
396        #[test]
397        fn test_states_to_string() {
398            assert_eq!(State::default().to_string(), DEFAULT_STR_STATE);
399            assert_eq!(format!["{:#}", State::default()], DEFAULT_STR_STATE);
400        }
401
402        #[test]
403        fn test_settings_to_string() {
404            assert_eq!(Settings::default().to_string(), DEFAULT_STR_SETTINGS);
405            assert_eq!(format!["{:#}", Settings::default()], DEFAULT_STR_SETTINGS);
406        }
407
408        #[test]
409        fn test_sample_slots_to_string() {
410            assert_eq!(SlotsAttributes::default().to_string(), DEFAULT_STR_SLOTS);
411            assert_eq!(
412                format!["{:#}", SlotsAttributes::default()],
413                DEFAULT_STR_SLOTS
414            );
415        }
416    }
417
418    // make sure debug formatting is not the same as display formatting
419    mod test_to_string_debug {
420        use super::*;
421
422        #[test]
423        fn test_full_to_string() {
424            assert_ne!(
425                format!["{:#?}", ProjectFile::default()],
426                DEFAULT_STR_FILE,
427                "debug formatting should not be 'file' representation",
428            );
429        }
430
431        #[test]
432        fn test_metadata_to_string() {
433            assert_ne!(
434                format!["{:#?}", OsMetadata::default().to_string()],
435                DEFAULT_STR_META,
436                "debug formatting should not be 'file' representation",
437            );
438        }
439
440        #[test]
441        fn test_states_to_string() {
442            assert_ne!(
443                format!["{:#?}", State::default().to_string()],
444                DEFAULT_STR_STATE,
445                "debug formatting should not be 'file' representation",
446            );
447        }
448
449        #[test]
450        fn test_settings_to_string() {
451            assert_ne!(
452                format!["{:#?}", Settings::default().to_string()],
453                DEFAULT_STR_SETTINGS,
454                "debug formatting should not be 'file' representation",
455            );
456        }
457
458        #[test]
459        fn test_sample_slots_to_string() {
460            assert_ne!(
461                format!["{:#?}", SlotsAttributes::default().to_string()],
462                DEFAULT_STR_SLOTS,
463                "debug formatting should not be 'file' representation",
464            );
465        }
466    }
467}