loam_cli/commands/
init.rs

1use clap::Parser;
2use rust_embed::{EmbeddedFile, RustEmbed};
3use soroban_cli::commands::contract::init as soroban_init;
4use std::{
5    fs::{self, create_dir_all, metadata, read_to_string, remove_dir_all, write, Metadata},
6    io,
7    path::{Path, PathBuf},
8};
9use toml_edit::{DocumentMut, TomlError};
10
11const FRONTEND_TEMPLATE: &str = "https://github.com/loambuild/frontend";
12
13#[derive(RustEmbed)]
14#[folder = "./src/examples/soroban/core"]
15struct ExampleCore;
16
17#[derive(RustEmbed)]
18#[folder = "./src/examples/soroban/status_message"]
19struct ExampleStatusMessage;
20
21/// A command to initialize a new project
22#[derive(Parser, Debug, Clone)]
23pub struct Cmd {
24    /// The path to the project must be provided to initialize
25    pub project_path: PathBuf,
26    /// The name of the project
27    #[arg(default_value = "loam-example")]
28    pub name: String,
29}
30/// Errors that can occur during initialization
31#[derive(thiserror::Error, Debug)]
32pub enum Error {
33    #[error("Io error: {0}")]
34    IoError(#[from] io::Error),
35    #[error("Soroban init error: {0}")]
36    SorobanInitError(#[from] soroban_init::Error),
37    #[error("Failed to convert bytes to string: {0}")]
38    ConverBytesToStringErr(#[from] std::str::Utf8Error),
39    #[error("Failed to parse toml file: {0}")]
40    TomlParseError(#[from] TomlError),
41}
42
43impl Cmd {
44    /// Run the initialization command by calling the soroban init command
45    ///
46    /// # Example:
47    ///
48    /// ```
49    /// /// From the command line
50    /// loam init /path/to/project
51    /// ```
52    #[allow(clippy::unused_self)]
53    pub fn run(&self) -> Result<(), Error> {
54        // Create a new project using the soroban init command
55        // by default uses a provided frontend template
56        // Examples cannot currently be added by user
57        soroban_init::Cmd {
58            project_path: self.project_path.to_string_lossy().to_string(),
59            name: self.name.clone(),
60            with_example: None,
61            frontend_template: Some(FRONTEND_TEMPLATE.to_string()),
62            overwrite: true,
63        }
64        .run(&soroban_cli::commands::global::Args::default())?;
65
66        // remove soroban hello_world default contract
67        remove_dir_all(self.project_path.join("contracts/hello_world/")).map_err(|e| {
68            eprintln!("Error removing directory");
69            e
70        })?;
71
72        copy_example_contracts(&self.project_path)?;
73        rename_cargo_toml_remove(&self.project_path, "core")?;
74        rename_cargo_toml_remove(&self.project_path, "status_message")?;
75        update_workspace_cargo_toml(&self.project_path.join("Cargo.toml"))?;
76        Ok(())
77    }
78}
79
80// update a soroban project to a loam project
81fn update_workspace_cargo_toml(cargo_path: &Path) -> Result<(), Error> {
82    let cargo_toml_str = read_to_string(cargo_path).map_err(|e| {
83        eprintln!("Error reading Cargo.toml file in: {cargo_path:?}");
84        e
85    })?;
86
87    let cargo_toml_str = regex::Regex::new(r#"soroban-sdk = "[^\"]+""#)
88        .unwrap()
89        .replace_all(
90            cargo_toml_str.as_str(),
91            r#"loam-sdk = "0.6.12"
92loam-subcontract-core = "0.7.5""#,
93        );
94
95    let doc = cargo_toml_str.parse::<DocumentMut>().map_err(|e| {
96        eprintln!("Error parsing Cargo.toml file in: {cargo_path:?}");
97        e
98    })?;
99
100    write(cargo_path, doc.to_string()).map_err(|e| {
101        eprintln!("Error writing to Cargo.toml file in: {cargo_path:?}");
102        e
103    })?;
104
105    Ok(())
106}
107
108fn copy_example_contracts(to: &Path) -> Result<(), Error> {
109    for item in ExampleCore::iter() {
110        copy_file(
111            &to.join("contracts/core"),
112            item.as_ref(),
113            ExampleCore::get(&item),
114        )?;
115    }
116    for item in ExampleStatusMessage::iter() {
117        copy_file(
118            &to.join("contracts/status_message"),
119            item.as_ref(),
120            ExampleStatusMessage::get(&item),
121        )?;
122    }
123
124    Ok(())
125}
126
127fn copy_file(
128    example_path: &Path,
129    filename: &str,
130    embedded_file: Option<EmbeddedFile>,
131) -> Result<(), Error> {
132    let to = example_path.join(filename);
133    if file_exists(&to) {
134        println!(
135            "ℹ️  Skipped creating {} as it already exists",
136            &to.to_string_lossy()
137        );
138        return Ok(());
139    }
140    create_dir_all(to.parent().expect("invalid path")).map_err(|e| {
141        eprintln!("Error creating directory path for: {to:?}");
142        e
143    })?;
144
145    let Some(embedded_file) = embedded_file else {
146        println!("⚠️  Failed to read file: {filename}");
147        return Ok(());
148    };
149
150    let file_contents = std::str::from_utf8(embedded_file.data.as_ref()).map_err(|e| {
151        eprintln!("Error converting file contents in {filename:?} to string",);
152        e
153    })?;
154
155    println!("➕  Writing {}", &to.to_string_lossy());
156    write(&to, file_contents).map_err(|e| {
157        eprintln!("Error writing file: {to:?}");
158        e
159    })?;
160    Ok(())
161}
162
163// TODO: import from stellar-cli init (not currently pub there)
164fn file_exists(file_path: &Path) -> bool {
165    metadata(file_path)
166        .as_ref()
167        .map(Metadata::is_file)
168        .unwrap_or(false)
169}
170
171fn rename_cargo_toml_remove(project: &Path, name: &str) -> Result<(), Error> {
172    let from = project.join(format!("contracts/{name}/Cargo.toml.remove"));
173    let to = from.with_extension("");
174    println!("Renaming to {from:?} to {to:?}");
175    fs::rename(from, to)?;
176    Ok(())
177}