use anyhow::{Context, Result};
use colored::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Serialize, Deserialize)]
pub struct Template {
pub template: TemplateMetadata,
pub files: HashMap<String, String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TemplateMetadata {
pub name: String,
pub description: String,
pub version: String,
pub author: String,
}
#[derive(Debug)]
pub enum ProjectTemplate {
BasicDapp,
Nep17Token,
NftCollection,
DefiProtocol,
OracleConsumer,
}
impl ProjectTemplate {
pub fn display_name(&self) -> &str {
match self {
ProjectTemplate::BasicDapp => "Basic Neo dApp",
ProjectTemplate::Nep17Token => "NEP-17 Token",
ProjectTemplate::NftCollection => "NFT Collection (NEP-11)",
ProjectTemplate::DefiProtocol => "DeFi Protocol",
ProjectTemplate::OracleConsumer => "Oracle Consumer",
}
}
}
pub fn generate_project(
template_type: ProjectTemplate,
project_name: &str,
target_dir: Option<PathBuf>,
) -> Result<()> {
println!("🔧 {} project: {}", "Generating".cyan(), project_name.green());
let target = target_dir.unwrap_or_else(|| PathBuf::from(".")).join(project_name);
if target.exists() {
return Err(anyhow::anyhow!("Directory '{}' already exists", target.display()));
}
let template = load_template(&template_type)?;
fs::create_dir_all(&target).context("Failed to create project directory")?;
for (file_path, content) in &template.files {
generate_file(&target, file_path, content, project_name)?;
}
println!("✅ {} created successfully!", format!("Project '{}'", project_name).green().bold());
print_next_steps(project_name, &template_type);
Ok(())
}
fn load_template(template_type: &ProjectTemplate) -> Result<Template> {
let template_content = match template_type {
ProjectTemplate::BasicDapp => {
include_str!("../templates/basic_dapp.toml")
},
ProjectTemplate::Nep17Token => {
include_str!("../templates/nep17_token.toml")
},
ProjectTemplate::NftCollection => {
include_str!("../templates/nft_collection.toml")
},
ProjectTemplate::DefiProtocol => {
include_str!("../templates/defi_protocol.toml")
},
ProjectTemplate::OracleConsumer => {
include_str!("../templates/oracle_consumer.toml")
},
};
toml::from_str(template_content).context("Failed to parse template")
}
fn generate_file(
target_dir: &Path,
file_path: &str,
content: &str,
project_name: &str,
) -> Result<()> {
let file_path = target_dir.join(file_path);
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent).context("Failed to create directory")?;
}
let content = content
.replace("{{project_name}}", project_name)
.replace("{{neo3_version}}", neo3::VERSION);
fs::write(&file_path, content)
.with_context(|| format!("Failed to write file: {}", file_path.display()))?;
println!(
" 📄 Created: {}",
file_path
.strip_prefix(target_dir)
.unwrap_or(&file_path)
.display()
.to_string()
.dimmed()
);
Ok(())
}
fn print_next_steps(project_name: &str, template_type: &ProjectTemplate) {
println!("\n{}", "Next steps:".cyan().bold());
println!(" 1. cd {}", project_name.green());
println!(" 2. cargo build");
println!(" 3. cargo test");
match template_type {
ProjectTemplate::Nep17Token => {
println!(" 4. Compile contract: neo3-boa contracts/token.py");
println!(" 5. Deploy contract: neo-cli contract deploy");
},
ProjectTemplate::NftCollection => {
println!(" 4. Design your NFT metadata");
println!(" 5. Deploy NFT contract");
},
ProjectTemplate::DefiProtocol => {
println!(" 4. Configure liquidity pools");
println!(" 5. Deploy DeFi contracts");
},
_ => {
println!(" 4. Configure your application");
println!(" 5. Deploy to Neo blockchain");
},
}
println!("\n📚 Documentation: {}", "https://github.com/R3E-Network/NeoRust".blue());
}
pub fn list_templates() {
println!("{}", "Available Project Templates:".cyan().bold());
println!();
let templates = vec![
(ProjectTemplate::BasicDapp, "General purpose blockchain application"),
(ProjectTemplate::Nep17Token, "Fungible token following NEP-17 standard"),
(ProjectTemplate::NftCollection, "Non-fungible token collection (NEP-11)"),
(ProjectTemplate::DefiProtocol, "Decentralized finance protocol"),
(ProjectTemplate::OracleConsumer, "Application that consumes oracle data"),
];
for (template, description) in templates {
println!(
" {} {}",
format!("• {}", template.display_name()).green().bold(),
format!("- {}", description).dimmed()
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_embedded_templates_parse() {
let basic =
load_template(&ProjectTemplate::BasicDapp).expect("basic_dapp.toml should parse");
assert!(basic.files.contains_key("Cargo.toml"), "basic template should include Cargo.toml");
assert!(
basic.files.contains_key("src/main.rs"),
"basic template should include src/main.rs"
);
let nep17 =
load_template(&ProjectTemplate::Nep17Token).expect("nep17_token.toml should parse");
assert!(nep17.files.contains_key("Cargo.toml"), "nep17 template should include Cargo.toml");
assert!(
nep17.files.contains_key("contracts/token.py"),
"nep17 template should include contracts/token.py"
);
let nft = load_template(&ProjectTemplate::NftCollection)
.expect("nft_collection.toml should parse");
assert!(nft.files.contains_key("Cargo.toml"), "nft template should include Cargo.toml");
assert!(nft.files.contains_key("src/main.rs"), "nft template should include src/main.rs");
let defi =
load_template(&ProjectTemplate::DefiProtocol).expect("defi_protocol.toml should parse");
assert!(defi.files.contains_key("Cargo.toml"), "defi template should include Cargo.toml");
assert!(defi.files.contains_key("src/main.rs"), "defi template should include src/main.rs");
let oracle = load_template(&ProjectTemplate::OracleConsumer)
.expect("oracle_consumer.toml should parse");
assert!(
oracle.files.contains_key("Cargo.toml"),
"oracle template should include Cargo.toml"
);
assert!(
oracle.files.contains_key("src/main.rs"),
"oracle template should include src/main.rs"
);
}
#[test]
fn test_project_generation() {
let temp_dir = TempDir::new().unwrap();
let project_name = "test_project";
let result = generate_project(
ProjectTemplate::BasicDapp,
project_name,
Some(temp_dir.path().to_path_buf()),
);
assert!(result.is_ok());
let project_dir = temp_dir.path().join(project_name);
assert!(project_dir.exists());
let main_file = project_dir.join("src").join("main.rs");
assert!(main_file.exists());
let cargo_file = project_dir.join("Cargo.toml");
assert!(cargo_file.exists());
let cargo_contents = std::fs::read_to_string(&cargo_file).unwrap();
assert!(
!cargo_contents.contains("{{neo3_version}}"),
"Generated Cargo.toml should not contain unresolved placeholders"
);
assert!(
cargo_contents.contains(&format!("neo3 = \"{}\"", neo3::VERSION)),
"Generated Cargo.toml should pin neo3 dependency to the current SDK version"
);
}
#[test]
fn test_template_variable_replacement() {
let content = "name = \"{{project_name}}\"";
let replaced = content.replace("{{project_name}}", "my_project");
assert_eq!(replaced, "name = \"my_project\"");
}
}