Skip to main content

tinted_builder_rust/operations/build/
utils.rs

1use anyhow::{anyhow, Result};
2use serde::Deserialize;
3use std::fs::read_to_string;
4use std::path::{Path, PathBuf};
5use tinted_builder::{Scheme, SchemeSystem};
6
7#[derive(Debug, Clone)]
8pub enum SchemeFile {
9    Yaml(PathBuf),
10    Yml(PathBuf),
11}
12
13impl SchemeFile {
14    /// Creates a new [`SchemeFile`] from the given path.
15    ///
16    /// # Errors
17    ///
18    /// Returns an error if the provided file does **not** have a `.yaml` or `.yml` extension.
19    pub fn new(path: impl AsRef<Path>) -> Result<Self> {
20        let extension = path
21            .as_ref()
22            .extension()
23            .unwrap_or_default()
24            .to_str()
25            .unwrap_or_default();
26
27        match extension {
28            "yaml" => Ok(Self::Yaml(path.as_ref().to_path_buf())),
29            "yml" => Ok(Self::Yml(path.as_ref().to_path_buf())),
30            _ => Err(anyhow!("Invalid file extension: {extension}")),
31        }
32    }
33
34    /// Reads and parses the YAML scheme file into a [`Scheme`].
35    ///
36    /// # Errors
37    ///
38    /// Returns an error if:
39    /// - The file cannot be read from disk.
40    /// - The contents are not valid YAML.
41    /// - The YAML structure does not match the expected Base16/Base24 scheme format.
42    pub fn get_scheme(&self) -> Result<Scheme> {
43        match self {
44            Self::Yaml(path) | Self::Yml(path) => {
45                let scheme_str = read_to_string(path)?;
46                let scheme: serde_yaml::Value = serde_yaml::from_str(&scheme_str)?;
47
48                if let serde_yaml::Value::Mapping(map) = scheme {
49                    match map.get("system") {
50                        Some(serde_yaml::Value::String(system_str))
51                            if system_str == &SchemeSystem::Base24.to_string() =>
52                        {
53                            let scheme_inner =
54                                serde_yaml::from_value(serde_yaml::Value::Mapping(map))?;
55                            let scheme = Scheme::Base24(scheme_inner);
56
57                            Ok(scheme)
58                        }
59                        None | Some(_) => {
60                            let scheme_inner =
61                                serde_yaml::from_value(serde_yaml::Value::Mapping(map))?;
62                            let scheme = Scheme::Base16(scheme_inner);
63
64                            Ok(scheme)
65                        }
66                    }
67                } else {
68                    Err(anyhow!("Unable to get scheme from SchemeFile"))
69                }
70            }
71        }
72    }
73
74    #[must_use]
75    pub fn get_path(&self) -> PathBuf {
76        match self {
77            Self::Yaml(path) | Self::Yml(path) => path.clone(),
78        }
79    }
80}
81
82#[derive(Debug, Deserialize)]
83pub struct TemplateConfig {
84    pub filename: Option<String>,
85
86    #[serde(rename = "supported-systems")]
87    pub supported_systems: Option<Vec<SchemeSystem>>,
88
89    #[deprecated]
90    pub extension: Option<String>,
91
92    #[deprecated]
93    pub output: Option<String>,
94}
95
96#[derive(Debug)]
97pub struct ParsedFilename {
98    pub directory: PathBuf,
99    pub filestem: String,
100    pub file_extension: Option<String>,
101}
102
103impl ParsedFilename {
104    #[must_use]
105    pub fn get_path(&self) -> PathBuf {
106        let directory = &self.directory;
107        let filestem = &self.filestem;
108        let file_extension = &self
109            .file_extension
110            .as_ref()
111            .map(|ext| format!(".{ext}"))
112            .unwrap_or_default();
113
114        directory.join(format!("{filestem}{file_extension}"))
115    }
116}
117
118/// Recursively retrieves scheme file paths from a directory.
119///
120/// This function traverses the given directory recursively, gathering all valid scheme files.
121/// It skips hidden files and directories (those whose names start with a `.`).
122///
123/// # Arguments
124///
125/// * `dirpath` - A reference to a `Path` representing the directory to start the search from.
126///
127/// # Returns
128///
129/// Returns a `Result` containing a `Vec<SchemeFile>` if successful, where `SchemeFile`
130/// represents a valid scheme file. If any error occurs during directory traversal or file handling,
131/// an `Err` with the relevant error information is returned.
132///
133/// # Errors
134///
135/// This function can return an error in the following scenarios:
136///
137/// * If the directory cannot be read.
138/// * If there is an issue accessing the contents of the directory.
139/// * If there is an issue creating a `SchemeFile` from a file path.
140pub fn get_scheme_files(dirpath: impl AsRef<Path>, is_recursive: bool) -> Result<Vec<SchemeFile>> {
141    let mut scheme_paths: Vec<SchemeFile> = vec![];
142
143    for item in dirpath.as_ref().read_dir()? {
144        let file_path = item?.path();
145        let file_stem = file_path
146            .file_stem()
147            .unwrap_or_default()
148            .to_str()
149            .unwrap_or_default();
150
151        // Skip hidden files and directories
152        if file_stem.starts_with('.') {
153            continue;
154        }
155
156        if file_path.is_dir() && is_recursive {
157            let inner_scheme_paths_result = get_scheme_files(&file_path, true);
158
159            if let Ok(inner_scheme_paths) = inner_scheme_paths_result {
160                scheme_paths.extend(inner_scheme_paths);
161            }
162
163            continue;
164        }
165
166        let scheme_file_type_result = SchemeFile::new(&file_path);
167
168        if let Ok(scheme_file_type) = scheme_file_type_result {
169            scheme_paths.push(scheme_file_type);
170        }
171    }
172
173    scheme_paths.sort_by_key(SchemeFile::get_path);
174
175    Ok(scheme_paths)
176}
177
178/// Parses a given file path into its directory, filestem, and optional extension.
179///
180/// This function takes a `template_path` (which is used as the base path for relative directories)
181/// and a `filepath` (the path to parse). It returns a `ParsedFilename` struct, which contains:
182/// - `directory`: the directory of the file (relative to `template_path` or `.` if not present)
183/// - `filestem`: the filename without the extension
184/// - `file_extension`: the optional file extension
185pub fn parse_filename(template_path: impl AsRef<Path>, filepath: &str) -> ParsedFilename {
186    let p = Path::new(filepath);
187
188    let directory: PathBuf = p.parent().map_or_else(
189        || template_path.as_ref().to_path_buf(),
190        |dir| template_path.as_ref().join(dir),
191    );
192
193    // A filestem must exist and be non-empty.
194    let filestem = p
195        .file_stem()
196        .and_then(|s| s.to_str())
197        .filter(|s| !s.is_empty())
198        .map(String::from)
199        .unwrap_or_default();
200
201    let file_extension = p.extension().and_then(|e| e.to_str()).map(String::from);
202
203    ParsedFilename {
204        directory,
205        filestem,
206        file_extension,
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use std::path::Path;
214
215    #[test]
216    fn test_parse_filename_with_directory_and_extension() {
217        let template_path = Path::new("/home/user/templates");
218        let result = parse_filename(template_path, "some-directory/name/file.txt");
219
220        assert_eq!(result.directory, template_path.join("some-directory/name"));
221        assert_eq!(result.filestem, "file");
222        assert_eq!(result.file_extension, Some("txt".to_string()));
223    }
224
225    #[test]
226    fn test_parse_filename_with_filename_and_extension() {
227        let template_path = Path::new("/home/user/templates");
228        let result = parse_filename(template_path, "filename.ext");
229
230        assert_eq!(result.directory, template_path);
231        assert_eq!(result.filestem, "filename");
232        assert_eq!(result.file_extension, Some("ext".to_string()));
233    }
234
235    #[test]
236    fn test_parse_filename_with_only_filename() {
237        let template_path = Path::new("/home/user/templates");
238        let result = parse_filename(template_path, "file");
239
240        assert_eq!(result.directory, template_path);
241        assert_eq!(result.filestem, "file");
242        assert_eq!(result.file_extension, None);
243    }
244
245    #[test]
246    fn test_parse_filename_with_directory_and_no_extension() {
247        let template_path = Path::new("/home/user/templates");
248        let result = parse_filename(template_path, "some-directory/file");
249
250        assert_eq!(result.directory, template_path.join("some-directory"));
251        assert_eq!(result.filestem, "file");
252        assert_eq!(result.file_extension, None);
253    }
254}