use std::path::{Path, PathBuf};
use globset::{Glob, GlobSet, GlobSetBuilder};
use serde::{Deserialize, Serialize};
use crate::detect;
use crate::error::ProjectError;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectConfig {
pub workspace: WorkspaceConfig,
#[serde(default)]
pub package: Vec<PackageConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspaceConfig {
pub name: String,
#[serde(default)]
pub exclude: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageConfig {
pub name: String,
pub path: PathBuf,
#[serde(default)]
pub protocol: Option<String>,
}
pub fn load_config(dir: &Path) -> Result<Option<ProjectConfig>, ProjectError> {
let manifest_path = dir.join("panproto.toml");
if !manifest_path.exists() {
return Ok(None);
}
let content = std::fs::read_to_string(&manifest_path)?;
let config: ProjectConfig =
toml::from_str(&content).map_err(|e| ProjectError::InvalidManifest {
path: manifest_path.display().to_string(),
reason: e.to_string(),
})?;
Ok(Some(config))
}
pub fn compile_excludes(base: &Path, patterns: &[String]) -> Result<GlobSet, ProjectError> {
let mut builder = GlobSetBuilder::new();
for pattern in patterns {
let full_pattern = base.join(pattern).display().to_string();
let glob = Glob::new(&full_pattern).map_err(|e| ProjectError::InvalidPattern {
pattern: pattern.clone(),
reason: e.to_string(),
})?;
builder.add(glob);
}
builder.build().map_err(|e| ProjectError::InvalidPattern {
pattern: "<composite>".to_owned(),
reason: e.to_string(),
})
}
pub fn generate_config(dir: &Path, name: &str) -> Result<ProjectConfig, ProjectError> {
let packages = detect::scan_packages(dir)?;
let package_configs: Vec<PackageConfig> = packages
.into_iter()
.map(|pkg| {
let relative_path = pkg
.path
.strip_prefix(dir)
.unwrap_or(&pkg.path)
.to_path_buf();
PackageConfig {
name: pkg.name,
path: relative_path,
protocol: Some(pkg.protocol),
}
})
.collect();
Ok(ProjectConfig {
workspace: WorkspaceConfig {
name: name.to_owned(),
exclude: vec![
"target".to_owned(),
"node_modules".to_owned(),
"__pycache__".to_owned(),
"build".to_owned(),
"dist".to_owned(),
".git".to_owned(),
],
},
package: package_configs,
})
}
pub fn serialize_config(config: &ProjectConfig) -> Result<String, ProjectError> {
toml::to_string_pretty(config).map_err(|e| ProjectError::InvalidManifest {
path: "panproto.toml".to_owned(),
reason: e.to_string(),
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn round_trip_config() {
let config = ProjectConfig {
workspace: WorkspaceConfig {
name: "test-project".to_owned(),
exclude: vec!["target".to_owned(), "node_modules".to_owned()],
},
package: vec![
PackageConfig {
name: "core".to_owned(),
path: PathBuf::from("crates/core"),
protocol: Some("rust".to_owned()),
},
PackageConfig {
name: "sdk".to_owned(),
path: PathBuf::from("sdk/typescript"),
protocol: Some("typescript".to_owned()),
},
],
};
let toml_str = serialize_config(&config).unwrap();
let parsed: ProjectConfig = toml::from_str(&toml_str).unwrap();
assert_eq!(parsed.workspace.name, "test-project");
assert_eq!(parsed.package.len(), 2);
assert_eq!(parsed.package[0].name, "core");
assert_eq!(parsed.package[1].protocol.as_deref(), Some("typescript"));
}
#[test]
fn compile_excludes_builds_globset() {
let base = Path::new("/tmp/project");
let patterns = vec!["target".to_owned(), "**/*.log".to_owned()];
let globset = compile_excludes(base, &patterns).unwrap();
assert!(globset.is_match("/tmp/project/target"));
assert!(globset.is_match("/tmp/project/logs/debug.log"));
assert!(!globset.is_match("/tmp/project/src/main.rs"));
}
#[test]
fn load_config_missing_file() {
let result = load_config(Path::new("/nonexistent/path")).unwrap();
assert!(result.is_none());
}
}