use glob::Pattern;
use serde::{Deserialize, Serialize};
use std::env;
use std::path::{Path, PathBuf};
use chrono::NaiveDate;
#[derive(Debug, Deserialize, Serialize, Default)]
#[serde(default)]
pub struct RackConfig {
pub rack: RackInfoConfig,
pub artifact: ArtifactConfig,
pub project: ProjectConfig,
pub document: DocumentConfig,
}
#[derive(Debug, Deserialize, Serialize, Default)]
#[serde(default)]
pub struct RackInfoConfig {
pub name: String,
}
#[derive(Debug, Deserialize, Serialize, Default)]
#[serde(default)]
pub struct ArtifactConfig {
pub required_files: Vec<String>,
pub optional_files: Vec<String>,
}
impl ArtifactConfig {
pub fn is_required_file(&self, filename: &Path) -> bool {
self.required_files.iter().any(|pattern| {
let is_directory = pattern.ends_with('/') || pattern.ends_with('\\');
if is_directory {
let directory = pattern.trim_end_matches(&['/', '\\'][..]);
if directory.is_empty() {
return true;
}
let dir_path = Path::new(directory);
filename.starts_with(dir_path)
} else {
Pattern::new(pattern)
.map(|p| p.matches_path(filename))
.unwrap_or(false)
}
})
}
pub fn is_optional_file(&self, filename: &Path) -> bool {
self.optional_files.iter().any(|pattern| {
Pattern::new(pattern)
.map(|p| p.matches_path(filename))
.unwrap_or(false)
})
}
}
#[derive(Debug, Deserialize, Serialize, Default)]
#[serde(default)]
pub struct ProjectConfig {
pub project: Option<Vec<String>>,
pub version_yyyymmdd: Option<Vec<String>>,
pub version_x: Option<Vec<String>>,
pub version_x_y: Option<Vec<String>>,
pub version_x_y_z: Option<Vec<String>>,
}
impl ProjectConfig {
pub fn is_valid_version(&self, project_name: &str, version: &str) -> bool {
let version_elements: Vec<String> = version.split('.').map(|s| s.to_string()).collect();
if self.is_yyyymmdd_project(project_name) {
is_yyyymmdd_format(version)
} else if self.is_x_project(project_name) {
version.parse::<i64>().is_ok()
} else if self.is_x_y_project(project_name) {
version.matches('.').count() == 1
&& version_elements.len() == 2
&& version_elements[0].parse::<i64>().is_ok()
&& version_elements[1].parse::<i64>().is_ok()
} else if self.is_x_y_z_project(project_name) {
version.matches('.').count() == 2
&& version_elements.len() == 3
&& version_elements[0].parse::<i64>().is_ok()
&& version_elements[1].parse::<i64>().is_ok()
&& version_elements[2].parse::<i64>().is_ok()
} else {
true
}
}
fn is_yyyymmdd_project(&self, project_name: &str) -> bool {
self.version_yyyymmdd.is_some()
&& self
.version_yyyymmdd
.as_ref()
.unwrap()
.iter()
.any(|s| s == project_name)
}
fn is_x_project(&self, project_name: &str) -> bool {
self.version_x.is_some()
&& self
.version_x
.as_ref()
.unwrap()
.iter()
.any(|s| s == project_name)
}
fn is_x_y_project(&self, project_name: &str) -> bool {
self.version_x_y.is_some()
&& self
.version_x_y
.as_ref()
.unwrap()
.iter()
.any(|s| s == project_name)
}
fn is_x_y_z_project(&self, project_name: &str) -> bool {
self.version_x_y_z.is_some()
&& self
.version_x_y_z
.as_ref()
.unwrap()
.iter()
.any(|s| s == project_name)
}
}
#[derive(Debug, Deserialize, Serialize, Default)]
#[serde(default)]
pub struct DocumentConfig {
pub template_file: Option<String>,
pub result_file: Option<String>,
}
pub fn load_rack_config(path: &Path) -> (RackConfig, PathBuf) {
let relative_dir = if path.is_dir() {
path.to_path_buf()
} else {
path.parent().unwrap().to_path_buf()
};
let mut absolute_dir = if relative_dir.is_absolute() {
relative_dir
} else {
env::current_dir().unwrap().join(relative_dir)
};
loop {
let candidate = absolute_dir.join("config.toml");
if candidate.is_file() {
log::info!("Loading config from {:?}", candidate);
let config_content = std::fs::read_to_string(&candidate).unwrap();
return (toml::from_str(&config_content).unwrap(), absolute_dir);
}
match absolute_dir.parent() {
Some(parent) => absolute_dir = parent.to_path_buf(),
None => {
log::error!(
"config.toml not found; reached filesystem root at {:?}\n\
Please ensure that config.toml exists in the directory tree starting from the directory of the provided path.",
absolute_dir
);
panic!(
"config.toml not found in the directory tree starting from {:?}",
path
);
}
}
}
}
fn is_yyyymmdd_format(s: &str) -> bool {
if s.len() != 8 || !s.as_bytes().iter().all(|b| b.is_ascii_digit()) {
return false;
}
let y: i32 = match s[0..4].parse() {
Ok(v) => v,
Err(_) => return false,
};
let m: u32 = match s[4..6].parse() {
Ok(v) => v,
Err(_) => return false,
};
let d: u32 = match s[6..8].parse() {
Ok(v) => v,
Err(_) => return false,
};
NaiveDate::from_ymd_opt(y, m, d).is_some()
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
use toml::to_string_pretty;
#[test]
fn test_load_rack_config() {
let temp = TempDir::new().unwrap();
let root_directory = temp.path();
let toml_str = to_string_pretty(&RackConfig::default())
.expect("failed to serialize rack config to TOML");
std::fs::write(root_directory.join("config.toml"), toml_str)
.expect("failed to write config.toml");
let (config, config_dir) = load_rack_config(root_directory);
assert_eq!(config_dir, root_directory);
assert_eq!(config.rack.name, "");
}
}