panproto_project/
config.rs1use std::path::{Path, PathBuf};
4
5use globset::{Glob, GlobSet, GlobSetBuilder};
6use serde::{Deserialize, Serialize};
7
8use crate::detect;
9use crate::error::ProjectError;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ProjectConfig {
14 pub workspace: WorkspaceConfig,
16 #[serde(default)]
18 pub package: Vec<PackageConfig>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct WorkspaceConfig {
24 pub name: String,
26 #[serde(default)]
28 pub exclude: Vec<String>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct PackageConfig {
34 pub name: String,
36 pub path: PathBuf,
38 #[serde(default)]
41 pub protocol: Option<String>,
42}
43
44pub fn load_config(dir: &Path) -> Result<Option<ProjectConfig>, ProjectError> {
52 let manifest_path = dir.join("panproto.toml");
53 if !manifest_path.exists() {
54 return Ok(None);
55 }
56 let content = std::fs::read_to_string(&manifest_path)?;
57 let config: ProjectConfig =
58 toml::from_str(&content).map_err(|e| ProjectError::InvalidManifest {
59 path: manifest_path.display().to_string(),
60 reason: e.to_string(),
61 })?;
62 Ok(Some(config))
63}
64
65pub fn compile_excludes(base: &Path, patterns: &[String]) -> Result<GlobSet, ProjectError> {
74 let mut builder = GlobSetBuilder::new();
75 for pattern in patterns {
76 let full_pattern = base.join(pattern).display().to_string();
77 let glob = Glob::new(&full_pattern).map_err(|e| ProjectError::InvalidPattern {
78 pattern: pattern.clone(),
79 reason: e.to_string(),
80 })?;
81 builder.add(glob);
82 }
83 builder.build().map_err(|e| ProjectError::InvalidPattern {
84 pattern: "<composite>".to_owned(),
85 reason: e.to_string(),
86 })
87}
88
89pub fn generate_config(dir: &Path, name: &str) -> Result<ProjectConfig, ProjectError> {
95 let packages = detect::scan_packages(dir)?;
96 let package_configs: Vec<PackageConfig> = packages
97 .into_iter()
98 .map(|pkg| {
99 let relative_path = pkg
100 .path
101 .strip_prefix(dir)
102 .unwrap_or(&pkg.path)
103 .to_path_buf();
104 PackageConfig {
105 name: pkg.name,
106 path: relative_path,
107 protocol: Some(pkg.protocol),
108 }
109 })
110 .collect();
111
112 Ok(ProjectConfig {
113 workspace: WorkspaceConfig {
114 name: name.to_owned(),
115 exclude: vec![
116 "target".to_owned(),
117 "node_modules".to_owned(),
118 "__pycache__".to_owned(),
119 "build".to_owned(),
120 "dist".to_owned(),
121 ".git".to_owned(),
122 ],
123 },
124 package: package_configs,
125 })
126}
127
128pub fn serialize_config(config: &ProjectConfig) -> Result<String, ProjectError> {
134 toml::to_string_pretty(config).map_err(|e| ProjectError::InvalidManifest {
135 path: "panproto.toml".to_owned(),
136 reason: e.to_string(),
137 })
138}
139
140#[cfg(test)]
141#[allow(clippy::unwrap_used)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn round_trip_config() {
147 let config = ProjectConfig {
148 workspace: WorkspaceConfig {
149 name: "test-project".to_owned(),
150 exclude: vec!["target".to_owned(), "node_modules".to_owned()],
151 },
152 package: vec![
153 PackageConfig {
154 name: "core".to_owned(),
155 path: PathBuf::from("crates/core"),
156 protocol: Some("rust".to_owned()),
157 },
158 PackageConfig {
159 name: "sdk".to_owned(),
160 path: PathBuf::from("sdk/typescript"),
161 protocol: Some("typescript".to_owned()),
162 },
163 ],
164 };
165
166 let toml_str = serialize_config(&config).unwrap();
167 let parsed: ProjectConfig = toml::from_str(&toml_str).unwrap();
168 assert_eq!(parsed.workspace.name, "test-project");
169 assert_eq!(parsed.package.len(), 2);
170 assert_eq!(parsed.package[0].name, "core");
171 assert_eq!(parsed.package[1].protocol.as_deref(), Some("typescript"));
172 }
173
174 #[test]
175 fn compile_excludes_builds_globset() {
176 let base = Path::new("/tmp/project");
177 let patterns = vec!["target".to_owned(), "**/*.log".to_owned()];
178 let globset = compile_excludes(base, &patterns).unwrap();
179 assert!(globset.is_match("/tmp/project/target"));
180 assert!(globset.is_match("/tmp/project/logs/debug.log"));
181 assert!(!globset.is_match("/tmp/project/src/main.rs"));
182 }
183
184 #[test]
185 fn load_config_missing_file() {
186 let result = load_config(Path::new("/nonexistent/path")).unwrap();
187 assert!(result.is_none());
188 }
189}