use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::{Path, PathBuf};
#[derive(Debug, thiserror::Error)]
pub enum ElfMagicError {
#[error("Failed to discover workspace: {0}")]
WorkspaceDiscovery(String),
#[error("Failed to build program {program}: {error}")]
ProgramBuild { program: String, error: String },
#[error("Failed to generate code: {0}")]
CodeGeneration(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Metadata error: {0}")]
Metadata(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManifestConfig {
#[serde(default)]
pub include: Vec<String>, #[serde(default)]
pub exclude: Vec<String>, }
impl ManifestConfig {
pub fn allow_all() -> Self {
Self {
include: vec!["**/*".to_string()],
exclude: vec![],
}
}
pub fn allow_none() -> Self {
Self {
include: vec![],
exclude: vec!["**/*".to_string()],
}
}
}
#[derive(Debug, Clone)]
pub struct ProgramFilter {
include_patterns: Vec<String>,
exclude_patterns: Vec<String>,
}
impl ProgramFilter {
pub fn new(include_patterns: Vec<String>, exclude_patterns: Vec<String>) -> Self {
Self {
include_patterns,
exclude_patterns,
}
}
pub fn allow_all() -> Self {
Self {
include_patterns: vec!["**/*".to_string()],
exclude_patterns: vec![],
}
}
pub fn should_include(&self, path: &Path) -> bool {
let path_str = path.to_string_lossy();
let included = if self.include_patterns.is_empty() {
true
} else {
self.include_patterns.iter().any(|pattern| {
let pattern_without_glob = pattern.replace("*", "");
path_str.contains(&pattern_without_glob)
})
};
let excluded = self.exclude_patterns.iter().any(|pattern| {
let pattern_without_glob = pattern.replace("*", "");
path_str.contains(&pattern_without_glob)
});
included && !excluded
}
}
impl Default for ProgramFilter {
fn default() -> Self {
Self::allow_all()
}
}
impl From<&ManifestConfig> for ProgramFilter {
fn from(config: &ManifestConfig) -> Self {
Self::new(config.include.clone(), config.exclude.clone())
}
}
#[derive(Debug, Clone)]
pub struct Workspace {
pub packages: Vec<Package>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct Package {
pub name: String,
pub manifest_path: PathBuf,
pub targets: Vec<Target>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct Target {
pub name: String,
pub crate_types: Vec<String>,
}
#[derive(Clone)]
pub struct SolanaProgram {
pub name: String,
pub path: PathBuf,
pub manifest_path: PathBuf,
}
impl fmt::Debug for SolanaProgram {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SolanaProgram")
.field("name", &self.name)
.field("path", &self.path.display())
.field("env_var_name", &self.env_var_name())
.field("constant_name", &self.constant_name())
.finish()
}
}
impl fmt::Display for SolanaProgram {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ({})", self.name, self.path.display())
}
}
pub struct GenerationResult {
pub programs: Vec<SolanaProgram>,
}
impl fmt::Debug for GenerationResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("GenerationResult")
.field("program_count", &self.programs.len())
.field("programs", &self.programs)
.finish()
}
}
impl fmt::Display for GenerationResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.programs.is_empty() {
write!(f, "Generated lib.rs (no Solana programs found)")
} else {
writeln!(
f,
"Generated lib.rs with {} Solana programs:",
self.programs.len()
)?;
for program in &self.programs {
writeln!(f, " - {}", program)?;
}
Ok(())
}
}
}
impl GenerationResult {
pub fn new(programs: Vec<SolanaProgram>) -> Self {
Self { programs }
}
}
impl Workspace {
pub fn find_solana_programs(&self, filter: &ProgramFilter) -> Vec<SolanaProgram> {
self.packages
.iter()
.filter(|package| filter.should_include(&package.manifest_path))
.flat_map(|package| {
package
.targets
.iter()
.filter(|target| {
target
.crate_types
.iter()
.any(|crate_type| crate_type == "cdylib")
})
.map(|target| SolanaProgram {
name: target.name.clone(),
path: package.manifest_path.parent().unwrap().to_path_buf(),
manifest_path: package.manifest_path.clone(),
})
})
.collect()
}
}
impl SolanaProgram {
pub fn env_var_name(&self) -> String {
format!("PROGRAM_{}_ELF_MAGIC_PATH", self.name.to_uppercase())
}
pub fn constant_name(&self) -> String {
format!("{}_ELF", self.name.to_uppercase())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn create_test_member(name: &str, path: &str, crate_types: Vec<&str>) -> Package {
Package {
name: name.to_string(),
manifest_path: PathBuf::from(format!("{}/Cargo.toml", path)),
targets: vec![Target {
name: name.to_string(),
crate_types: crate_types.iter().map(|s| s.to_string()).collect(),
}],
}
}
#[test]
fn test_program_filter_simple() {
let filter = ProgramFilter::new(
vec!["programs/*".to_string()],
vec!["programs/deprecated-*".to_string()],
);
assert!(filter.should_include(Path::new("programs/token-manager")));
assert!(filter.should_include(Path::new("programs/governance")));
assert!(!filter.should_include(Path::new("programs/deprecated-old")));
assert!(!filter.should_include(Path::new("examples/demo")));
assert!(!filter.should_include(Path::new("src/lib.rs")));
}
#[test]
fn test_program_filter_empty_includes_all() {
let filter = ProgramFilter::new(vec![], vec!["deprecated-*".to_string()]);
assert!(filter.should_include(Path::new("programs/token-manager")));
assert!(filter.should_include(Path::new("examples/demo")));
assert!(!filter.should_include(Path::new("deprecated-old")));
}
#[test]
fn test_workspace_find_solana_programs() {
let config = ManifestConfig {
include: vec!["programs/*".to_string()],
exclude: vec!["programs/deprecated-*".to_string()],
};
let packages = vec![
create_test_member("token-manager", "programs/token-manager", vec!["cdylib"]),
create_test_member("governance", "programs/governance", vec!["cdylib"]),
create_test_member("deprecated", "programs/deprecated-old", vec!["cdylib"]),
create_test_member("test-utils", "test-utils", vec!["lib"]),
];
let workspace = Workspace { packages };
let filter = ProgramFilter::from(&config);
let programs = workspace.find_solana_programs(&filter);
assert_eq!(programs.len(), 2);
assert!(programs.iter().any(|p| p.name == "token-manager"));
assert!(programs.iter().any(|p| p.name == "governance"));
assert!(!programs.iter().any(|p| p.name == "deprecated"));
assert!(!programs.iter().any(|p| p.name == "test-utils"));
}
#[test]
fn test_manifest_config_conversion() {
let config = ManifestConfig {
include: vec!["programs/*".to_string()],
exclude: vec!["deprecated-*".to_string()],
};
let filter = ProgramFilter::from(&config);
assert!(filter.should_include(Path::new("programs/good")));
assert!(!filter.should_include(Path::new("deprecated-bad")));
}
#[test]
fn test_solana_program_naming() {
let program = SolanaProgram {
name: "token_manager".to_string(),
path: PathBuf::from("programs/token-manager"),
manifest_path: PathBuf::from("programs/token-manager/Cargo.toml"),
};
assert_eq!(
program.env_var_name(),
"PROGRAM_TOKEN_MANAGER_ELF_MAGIC_PATH"
);
assert_eq!(program.constant_name(), "TOKEN_MANAGER_ELF");
}
#[test]
fn test_solana_program_debug_display() {
let program = SolanaProgram {
name: "token_manager".to_string(),
path: PathBuf::from("programs/token-manager"),
manifest_path: PathBuf::from("programs/token-manager/Cargo.toml"),
};
let debug_output = format!("{:?}", program);
println!("debug_output: {}", debug_output);
assert!(debug_output.contains("SolanaProgram"));
assert!(debug_output.contains("token-manager"));
assert!(debug_output.contains("programs/token-manager"));
assert!(debug_output.contains("env_var_name"));
assert!(debug_output.contains("PROGRAM_TOKEN_MANAGER_ELF_MAGIC_PATH"));
assert!(debug_output.contains("constant_name"));
assert!(debug_output.contains("TOKEN_MANAGER_ELF"));
let display_output = format!("{}", program);
assert_eq!(display_output, "token_manager (programs/token-manager)");
}
#[test]
fn test_generation_result_debug_display() {
let programs = vec![
SolanaProgram {
name: "token-manager".to_string(),
path: PathBuf::from("programs/token-manager"),
manifest_path: PathBuf::from("programs/token-manager/Cargo.toml"),
},
SolanaProgram {
name: "governance".to_string(),
path: PathBuf::from("programs/governance"),
manifest_path: PathBuf::from("programs/governance/Cargo.toml"),
},
];
let result = GenerationResult::new(programs);
let debug_output = format!("{:?}", result);
assert!(debug_output.contains("GenerationResult"));
assert!(debug_output.contains("program_count: 2"));
let display_output = format!("{}", result);
assert!(display_output.contains("Generated lib.rs with 2 Solana programs:"));
assert!(display_output.contains("- token-manager (programs/token-manager)"));
assert!(display_output.contains("- governance (programs/governance)"));
let empty_result = GenerationResult::new(vec![]);
let empty_display = format!("{}", empty_result);
assert_eq!(empty_display, "Generated lib.rs (no Solana programs found)");
}
}