elf_magic/
domain.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::path::{Path, PathBuf};
4
5/// Main error type for elf-magic operations
6#[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/// Configuration for program discovery from package.metadata.elf-magic
25///
26/// This config specifies include/exclude glob patterns to determine
27/// which workspace members should be built as Solana programs.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct ManifestConfig {
30    #[serde(default)]
31    pub include: Vec<String>, // Glob patterns like "programs/*"
32    #[serde(default)]
33    pub exclude: Vec<String>, // Glob patterns like "programs/deprecated-*"
34}
35
36impl ManifestConfig {
37    /// Create a config that includes everything (for testing/fallback only)
38    pub fn allow_all() -> Self {
39        Self {
40            include: vec!["**/*".to_string()],
41            exclude: vec![],
42        }
43    }
44
45    /// Create a config that includes nothing (safe default)
46    pub fn allow_none() -> Self {
47        Self {
48            include: vec![],
49            exclude: vec!["**/*".to_string()],
50        }
51    }
52}
53
54/// Program filter using glob patterns - core domain logic for which programs to build
55#[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    /// Create filter that includes everything
70    pub fn allow_all() -> Self {
71        Self {
72            include_patterns: vec!["**/*".to_string()],
73            exclude_patterns: vec![],
74        }
75    }
76
77    /// Test if a program path should be included based on glob patterns
78    pub fn should_include(&self, path: &Path) -> bool {
79        let path_str = path.to_string_lossy();
80
81        // If no include patterns, include everything by default
82        let included = if self.include_patterns.is_empty() {
83            true
84        } else {
85            // TODO: Implement glob pattern matching
86            // For now, just check if path contains any include pattern (simplified)
87            self.include_patterns.iter().any(|pattern| {
88                // Simplified matching - replace with proper glob later
89                let pattern_without_glob = pattern.replace("*", "");
90                path_str.contains(&pattern_without_glob)
91            })
92        };
93
94        // Check exclude patterns
95        let excluded = self.exclude_patterns.iter().any(|pattern| {
96            // Simplified matching - replace with proper glob later
97            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/// A Cargo workspace containing potential Solana programs
118#[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/// A confirmed Solana program (has crate-type = ["cdylib"])
137#[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
161/// Result of the entire generation process
162pub 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    /// Find all Solana programs in this workspace using configured filters
201    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    /// Generate environment variable name for this program
227    /// e.g., "token_manager" → "PROGRAM_TOKEN_MANAGER_ELF_MAGIC_PATH"
228    pub fn env_var_name(&self) -> String {
229        format!("PROGRAM_{}_ELF_MAGIC_PATH", self.name.to_uppercase())
230    }
231
232    /// Generate constant name for this program
233    /// e.g., "token_manager" → "TOKEN_MANAGER_ELF"
234    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        // Should include programs that match pattern
263        assert!(filter.should_include(Path::new("programs/token-manager")));
264        assert!(filter.should_include(Path::new("programs/governance")));
265
266        // Should exclude deprecated programs
267        assert!(!filter.should_include(Path::new("programs/deprecated-old")));
268
269        // Should exclude non-program paths
270        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        // Empty include patterns should include everything
279        assert!(filter.should_include(Path::new("programs/token-manager")));
280        assert!(filter.should_include(Path::new("examples/demo")));
281
282        // But still respect exclude patterns
283        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        // Should find 2 programs (excluding deprecated and non-cdylib)
306        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        // Should not include deprecated or non-cdylib crates
311        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        // Test Debug - should be clean and structured with computed fields
352        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        // Test Display - should be user-friendly
364        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        // Test Debug - should show count and details
386        let debug_output = format!("{:?}", result);
387        assert!(debug_output.contains("GenerationResult"));
388        assert!(debug_output.contains("program_count: 2"));
389
390        // Test Display - should be user-friendly summary
391        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        // Test empty case
397        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}