1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::path::{Path, PathBuf};
4
5#[derive(Debug, thiserror::Error)]
7pub enum ElfMagicError {
8 #[error("Failed to discover workspace: {0}")]
9 WorkspaceDiscovery(String),
10
11 #[error("Failed to build program {program}: {error}")]
12 ProgramBuild { program: String, error: String },
13
14 #[error("Failed to generate code: {0}")]
15 CodeGeneration(String),
16
17 #[error("IO error: {0}")]
18 Io(#[from] std::io::Error),
19
20 #[error("Metadata error: {0}")]
21 Metadata(String),
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct ManifestConfig {
30 #[serde(default)]
31 pub include: Vec<String>, #[serde(default)]
33 pub exclude: Vec<String>, }
35
36impl ManifestConfig {
37 pub fn allow_all() -> Self {
39 Self {
40 include: vec!["**/*".to_string()],
41 exclude: vec![],
42 }
43 }
44
45 pub fn allow_none() -> Self {
47 Self {
48 include: vec![],
49 exclude: vec!["**/*".to_string()],
50 }
51 }
52}
53
54#[derive(Debug, Clone)]
56pub struct ProgramFilter {
57 include_patterns: Vec<String>,
58 exclude_patterns: Vec<String>,
59}
60
61impl ProgramFilter {
62 pub fn new(include_patterns: Vec<String>, exclude_patterns: Vec<String>) -> Self {
63 Self {
64 include_patterns,
65 exclude_patterns,
66 }
67 }
68
69 pub fn allow_all() -> Self {
71 Self {
72 include_patterns: vec!["**/*".to_string()],
73 exclude_patterns: vec![],
74 }
75 }
76
77 pub fn should_include(&self, path: &Path) -> bool {
79 let path_str = path.to_string_lossy();
80
81 let included = if self.include_patterns.is_empty() {
83 true
84 } else {
85 self.include_patterns.iter().any(|pattern| {
88 let pattern_without_glob = pattern.replace("*", "");
90 path_str.contains(&pattern_without_glob)
91 })
92 };
93
94 let excluded = self.exclude_patterns.iter().any(|pattern| {
96 let pattern_without_glob = pattern.replace("*", "");
98 path_str.contains(&pattern_without_glob)
99 });
100
101 included && !excluded
102 }
103}
104
105impl Default for ProgramFilter {
106 fn default() -> Self {
107 Self::allow_all()
108 }
109}
110
111impl From<&ManifestConfig> for ProgramFilter {
112 fn from(config: &ManifestConfig) -> Self {
113 Self::new(config.include.clone(), config.exclude.clone())
114 }
115}
116
117#[derive(Debug, Clone)]
119pub struct Workspace {
120 pub packages: Vec<Package>,
121}
122
123#[derive(Clone, Debug, Deserialize)]
124pub struct Package {
125 pub name: String,
126 pub manifest_path: PathBuf,
127 pub targets: Vec<Target>,
128}
129
130#[derive(Clone, Debug, Deserialize)]
131pub struct Target {
132 pub name: String,
133 pub crate_types: Vec<String>,
134}
135
136#[derive(Clone)]
138pub struct SolanaProgram {
139 pub name: String,
140 pub path: PathBuf,
141 pub manifest_path: PathBuf,
142}
143
144impl fmt::Debug for SolanaProgram {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 f.debug_struct("SolanaProgram")
147 .field("name", &self.name)
148 .field("path", &self.path.display())
149 .field("env_var_name", &self.env_var_name())
150 .field("constant_name", &self.constant_name())
151 .finish()
152 }
153}
154
155impl fmt::Display for SolanaProgram {
156 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157 write!(f, "{} ({})", self.name, self.path.display())
158 }
159}
160
161pub struct GenerationResult {
163 pub programs: Vec<SolanaProgram>,
164}
165
166impl fmt::Debug for GenerationResult {
167 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168 f.debug_struct("GenerationResult")
169 .field("program_count", &self.programs.len())
170 .field("programs", &self.programs)
171 .finish()
172 }
173}
174
175impl fmt::Display for GenerationResult {
176 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177 if self.programs.is_empty() {
178 write!(f, "Generated lib.rs (no Solana programs found)")
179 } else {
180 writeln!(
181 f,
182 "Generated lib.rs with {} Solana programs:",
183 self.programs.len()
184 )?;
185 for program in &self.programs {
186 writeln!(f, " - {}", program)?;
187 }
188 Ok(())
189 }
190 }
191}
192
193impl GenerationResult {
194 pub fn new(programs: Vec<SolanaProgram>) -> Self {
195 Self { programs }
196 }
197}
198
199impl Workspace {
200 pub fn find_solana_programs(&self, filter: &ProgramFilter) -> Vec<SolanaProgram> {
202 self.packages
203 .iter()
204 .filter(|package| filter.should_include(&package.manifest_path))
205 .flat_map(|package| {
206 package
207 .targets
208 .iter()
209 .filter(|target| {
210 target
211 .crate_types
212 .iter()
213 .any(|crate_type| crate_type == "cdylib")
214 })
215 .map(|target| SolanaProgram {
216 name: target.name.clone(),
217 path: package.manifest_path.parent().unwrap().to_path_buf(),
218 manifest_path: package.manifest_path.clone(),
219 })
220 })
221 .collect()
222 }
223}
224
225impl SolanaProgram {
226 pub fn env_var_name(&self) -> String {
229 format!("PROGRAM_{}_ELF_MAGIC_PATH", self.name.to_uppercase())
230 }
231
232 pub fn constant_name(&self) -> String {
235 format!("{}_ELF", self.name.to_uppercase())
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use std::path::PathBuf;
243
244 fn create_test_member(name: &str, path: &str, crate_types: Vec<&str>) -> Package {
245 Package {
246 name: name.to_string(),
247 manifest_path: PathBuf::from(format!("{}/Cargo.toml", path)),
248 targets: vec![Target {
249 name: name.to_string(),
250 crate_types: crate_types.iter().map(|s| s.to_string()).collect(),
251 }],
252 }
253 }
254
255 #[test]
256 fn test_program_filter_simple() {
257 let filter = ProgramFilter::new(
258 vec!["programs/*".to_string()],
259 vec!["programs/deprecated-*".to_string()],
260 );
261
262 assert!(filter.should_include(Path::new("programs/token-manager")));
264 assert!(filter.should_include(Path::new("programs/governance")));
265
266 assert!(!filter.should_include(Path::new("programs/deprecated-old")));
268
269 assert!(!filter.should_include(Path::new("examples/demo")));
271 assert!(!filter.should_include(Path::new("src/lib.rs")));
272 }
273
274 #[test]
275 fn test_program_filter_empty_includes_all() {
276 let filter = ProgramFilter::new(vec![], vec!["deprecated-*".to_string()]);
277
278 assert!(filter.should_include(Path::new("programs/token-manager")));
280 assert!(filter.should_include(Path::new("examples/demo")));
281
282 assert!(!filter.should_include(Path::new("deprecated-old")));
284 }
285
286 #[test]
287 fn test_workspace_find_solana_programs() {
288 let config = ManifestConfig {
289 include: vec!["programs/*".to_string()],
290 exclude: vec!["programs/deprecated-*".to_string()],
291 };
292
293 let packages = vec![
294 create_test_member("token-manager", "programs/token-manager", vec!["cdylib"]),
295 create_test_member("governance", "programs/governance", vec!["cdylib"]),
296 create_test_member("deprecated", "programs/deprecated-old", vec!["cdylib"]),
297 create_test_member("test-utils", "test-utils", vec!["lib"]),
298 ];
299
300 let workspace = Workspace { packages };
301
302 let filter = ProgramFilter::from(&config);
303 let programs = workspace.find_solana_programs(&filter);
304
305 assert_eq!(programs.len(), 2);
307 assert!(programs.iter().any(|p| p.name == "token-manager"));
308 assert!(programs.iter().any(|p| p.name == "governance"));
309
310 assert!(!programs.iter().any(|p| p.name == "deprecated"));
312 assert!(!programs.iter().any(|p| p.name == "test-utils"));
313 }
314
315 #[test]
316 fn test_manifest_config_conversion() {
317 let config = ManifestConfig {
318 include: vec!["programs/*".to_string()],
319 exclude: vec!["deprecated-*".to_string()],
320 };
321
322 let filter = ProgramFilter::from(&config);
323
324 assert!(filter.should_include(Path::new("programs/good")));
325 assert!(!filter.should_include(Path::new("deprecated-bad")));
326 }
327
328 #[test]
329 fn test_solana_program_naming() {
330 let program = SolanaProgram {
331 name: "token_manager".to_string(),
332 path: PathBuf::from("programs/token-manager"),
333 manifest_path: PathBuf::from("programs/token-manager/Cargo.toml"),
334 };
335
336 assert_eq!(
337 program.env_var_name(),
338 "PROGRAM_TOKEN_MANAGER_ELF_MAGIC_PATH"
339 );
340 assert_eq!(program.constant_name(), "TOKEN_MANAGER_ELF");
341 }
342
343 #[test]
344 fn test_solana_program_debug_display() {
345 let program = SolanaProgram {
346 name: "token_manager".to_string(),
347 path: PathBuf::from("programs/token-manager"),
348 manifest_path: PathBuf::from("programs/token-manager/Cargo.toml"),
349 };
350
351 let debug_output = format!("{:?}", program);
353 println!("debug_output: {}", debug_output);
354
355 assert!(debug_output.contains("SolanaProgram"));
356 assert!(debug_output.contains("token-manager"));
357 assert!(debug_output.contains("programs/token-manager"));
358 assert!(debug_output.contains("env_var_name"));
359 assert!(debug_output.contains("PROGRAM_TOKEN_MANAGER_ELF_MAGIC_PATH"));
360 assert!(debug_output.contains("constant_name"));
361 assert!(debug_output.contains("TOKEN_MANAGER_ELF"));
362
363 let display_output = format!("{}", program);
365 assert_eq!(display_output, "token_manager (programs/token-manager)");
366 }
367
368 #[test]
369 fn test_generation_result_debug_display() {
370 let programs = vec![
371 SolanaProgram {
372 name: "token-manager".to_string(),
373 path: PathBuf::from("programs/token-manager"),
374 manifest_path: PathBuf::from("programs/token-manager/Cargo.toml"),
375 },
376 SolanaProgram {
377 name: "governance".to_string(),
378 path: PathBuf::from("programs/governance"),
379 manifest_path: PathBuf::from("programs/governance/Cargo.toml"),
380 },
381 ];
382
383 let result = GenerationResult::new(programs);
384
385 let debug_output = format!("{:?}", result);
387 assert!(debug_output.contains("GenerationResult"));
388 assert!(debug_output.contains("program_count: 2"));
389
390 let display_output = format!("{}", result);
392 assert!(display_output.contains("Generated lib.rs with 2 Solana programs:"));
393 assert!(display_output.contains("- token-manager (programs/token-manager)"));
394 assert!(display_output.contains("- governance (programs/governance)"));
395
396 let empty_result = GenerationResult::new(vec![]);
398 let empty_display = format!("{}", empty_result);
399 assert_eq!(empty_display, "Generated lib.rs (no Solana programs found)");
400 }
401}