tinted_builder_rust/operations/build/
utils.rs

1use anyhow::{anyhow, Result};
2use regex::Regex;
3use serde::Deserialize;
4use std::fs::read_to_string;
5use std::path::{Path, PathBuf};
6use tinted_builder::{Scheme, SchemeSystem};
7
8#[derive(Debug, Clone)]
9pub enum SchemeFile {
10    Yaml(PathBuf),
11    Yml(PathBuf),
12}
13
14impl SchemeFile {
15    pub fn new(path: impl AsRef<Path>) -> Result<Self> {
16        let extension = path
17            .as_ref()
18            .extension()
19            .unwrap_or_default()
20            .to_str()
21            .unwrap_or_default();
22
23        match extension {
24            "yaml" => Ok(Self::Yaml(path.as_ref().to_path_buf())),
25            "yml" => Ok(Self::Yml(path.as_ref().to_path_buf())),
26            _ => Err(anyhow!("Invalid file extension: {}", extension.to_string())),
27        }
28    }
29
30    pub fn get_scheme(&self) -> Result<Scheme> {
31        match self {
32            Self::Yaml(path) | Self::Yml(path) => {
33                let scheme_str = read_to_string(path)?;
34                let scheme: serde_yaml::Value = serde_yaml::from_str(&scheme_str)?;
35
36                if let serde_yaml::Value::Mapping(map) = scheme {
37                    match map.get("system") {
38                        Some(serde_yaml::Value::String(system_str))
39                            if system_str == &SchemeSystem::Base24.to_string() =>
40                        {
41                            let scheme_inner =
42                                serde_yaml::from_value(serde_yaml::Value::Mapping(map))?;
43                            let scheme = Scheme::Base24(scheme_inner);
44
45                            Ok(scheme)
46                        }
47                        None | Some(_) => {
48                            let scheme_inner =
49                                serde_yaml::from_value(serde_yaml::Value::Mapping(map))?;
50                            let scheme = Scheme::Base16(scheme_inner);
51
52                            Ok(scheme)
53                        }
54                    }
55                } else {
56                    Err(anyhow!("Unable to get scheme from SchemeFile"))
57                }
58            }
59        }
60    }
61
62    pub fn get_path(&self) -> Option<PathBuf> {
63        match self {
64            Self::Yaml(path) | Self::Yml(path) => Some(path.to_path_buf()),
65        }
66    }
67}
68
69#[derive(Debug, Deserialize)]
70pub(crate) struct TemplateConfig {
71    pub filename: Option<String>,
72
73    #[serde(rename = "supported-systems")]
74    pub supported_systems: Option<Vec<SchemeSystem>>,
75
76    #[deprecated]
77    pub extension: Option<String>,
78
79    #[deprecated]
80    pub output: Option<String>,
81}
82
83#[derive(Debug)]
84pub(crate) struct ParsedFilename {
85    pub directory: PathBuf,
86    pub filestem: String,
87    pub file_extension: Option<String>,
88}
89
90impl ParsedFilename {
91    pub fn get_path(&self) -> PathBuf {
92        let directory = &self.directory;
93        let filestem = &self.filestem;
94        let file_extension = &self
95            .file_extension
96            .as_ref()
97            .map(|ext| format!(".{}", ext))
98            .unwrap_or_default();
99
100        directory.join(format!("{}{}", filestem, file_extension))
101    }
102}
103
104/// Recursively retrieves scheme file paths from a directory.
105///
106/// This function traverses the given directory recursively, gathering all valid scheme files.
107/// It skips hidden files and directories (those whose names start with a `.`).
108///
109/// # Arguments
110///
111/// * `dirpath` - A reference to a `Path` representing the directory to start the search from.
112///
113/// # Returns
114///
115/// Returns a `Result` containing a `Vec<SchemeFile>` if successful, where `SchemeFile`
116/// represents a valid scheme file. If any error occurs during directory traversal or file handling,
117/// an `Err` with the relevant error information is returned.
118///
119/// # Errors
120///
121/// This function can return an error in the following scenarios:
122///
123/// * If the directory cannot be read.
124/// * If there is an issue accessing the contents of the directory.
125/// * If there is an issue creating a `SchemeFile` from a file path.
126pub fn get_scheme_files(dirpath: impl AsRef<Path>, is_recursive: bool) -> Result<Vec<SchemeFile>> {
127    let mut scheme_paths: Vec<SchemeFile> = vec![];
128
129    for item in dirpath.as_ref().read_dir()? {
130        let file_path = item?.path();
131        let file_stem = file_path
132            .file_stem()
133            .unwrap_or_default()
134            .to_str()
135            .unwrap_or_default();
136
137        // Skip hidden files and directories
138        if file_stem.starts_with('.') {
139            continue;
140        }
141
142        if file_path.is_dir() && is_recursive {
143            let inner_scheme_paths_result = get_scheme_files(&file_path, true);
144
145            if let Ok(inner_scheme_paths) = inner_scheme_paths_result {
146                scheme_paths.extend(inner_scheme_paths);
147            }
148
149            continue;
150        }
151
152        let scheme_file_type_result = SchemeFile::new(&file_path);
153
154        match scheme_file_type_result {
155            Ok(scheme_file_type) => {
156                scheme_paths.push(scheme_file_type);
157            }
158            Err(_) => continue,
159        }
160    }
161
162    scheme_paths.sort_by_key(|k| k.get_path());
163
164    Ok(scheme_paths)
165}
166
167/// Parses a given file path into its directory, filestem, and optional extension.
168///
169/// This function takes a `template_path` (which is used as the base path for relative directories)
170/// and a `filepath` (the path to parse). It returns a `ParsedFilename` struct, which contains:
171/// - `directory`: the directory of the file (relative to `template_path` or `.` if not present)
172/// - `filestem`: the filename without the extension
173/// - `file_extension`: the optional file extension
174pub(crate) fn parse_filename(
175    template_path: impl AsRef<Path>,
176    filepath: &str,
177) -> Result<ParsedFilename> {
178    let re = Regex::new(r"^(?P<directory>.*/)?(?P<filestem>[^/\.]+)(?:\.(?P<extension>[^/]+))?$")
179        .unwrap();
180
181    if let Some(captures) = re.captures(filepath) {
182        // Extract the directory (if present), or use "." if there's no directory
183        let directory = captures
184            .name("directory")
185            .map(|d| template_path.as_ref().join(d.as_str()))
186            .unwrap_or_else(|| template_path.as_ref().to_path_buf());
187        let filestem = captures.name("filestem").unwrap().as_str().to_string();
188        let file_extension = captures
189            .name("extension")
190            .map(|ext| ext.as_str().to_string());
191
192        if filestem.is_empty() {
193            Err(anyhow!(
194                "Config property \"filename\" requires a filestem: {}",
195                &filepath
196            ))
197        } else {
198            // Return the parsed path
199            Ok(ParsedFilename {
200                directory,
201                filestem,
202                file_extension,
203            })
204        }
205    } else {
206        Err(anyhow!("Unable to parse template: {}", &filepath))
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").unwrap();
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").unwrap();
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").unwrap();
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").unwrap();
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
255    #[test]
256    fn test_parse_filename_invalid_filestem() {
257        let template_path = Path::new("/home/user/templates");
258        let filename = "/invalid/path/";
259        let err_message = parse_filename(template_path, filename)
260            .unwrap_err()
261            .to_string();
262
263        assert!(
264            err_message.contains(format!("Unable to parse template: {}", &filename).as_str()),
265            "Unexpected error message: {}",
266            err_message
267        );
268    }
269}