substrate_manager/util/
config.rs

1use anyhow::Context as _;
2use core::fmt;
3use inquire::Confirm;
4use std::{
5    env,
6    fmt::Debug,
7    path::{Path, PathBuf},
8    str::FromStr,
9};
10use toml_edit::{value, Document, Item, Table, Value};
11
12use crate::core::manifest::Manifest;
13
14use super::SubstrateResult;
15
16fn find_field_recursive<'a>(table: &'a Table, field_path: &str) -> Option<&'a Item> {
17    let mut current = table;
18
19    for field in field_path.split('.') {
20        if let Some(value) = current.get(field) {
21            if let Some(next_table) = value.as_table() {
22                current = next_table;
23            } else {
24                return Some(value);
25            }
26        } else {
27            return None;
28        }
29    }
30
31    None
32}
33
34fn from_manifest_or_default<T>(manifest: Option<&Document>, field_path: &str, default: &str) -> T
35where
36    T: FromStr,
37    T::Err: Debug,
38{
39    let option = match manifest.map(|doc| doc.as_table()) {
40        Some(table) => find_field_recursive(table, field_path)
41            .and_then(|v| v.as_str())
42            .unwrap_or(default),
43        None => default,
44    };
45
46    option
47        .parse::<T>()
48        .unwrap_or_else(|_| panic!("Could not parse configuration option: {}", field_path))
49}
50
51#[derive(Debug, Clone)]
52pub struct Config {
53    // shell: RefCell<Shell>,
54    pub cwd: PathBuf,
55    pub project_type: Option<ProjectType>,
56}
57
58// Each chain has a node name and a runtime name
59#[derive(Debug, Clone)]
60pub struct ChainInfo {
61    pub node_path: PathBuf,
62    pub node_name: Option<String>,
63    pub runtime_path: PathBuf,
64    pub runtime_name: Option<String>,
65    pub frontend_path: PathBuf,
66}
67
68// Each contract has a name
69#[derive(Debug, Clone)]
70pub struct ContractInfo {
71    pub name: String,
72}
73
74#[derive(Debug, Clone)]
75pub enum ProjectType {
76    Chain(ChainInfo),
77    Contract(ContractInfo),
78}
79
80impl fmt::Display for ProjectType {
81    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
82        match self {
83            Self::Chain(info) => {
84                let node_name = info.node_name.as_deref().unwrap_or("unknown");
85                let runtime_name = info.runtime_name.as_deref().unwrap_or("unknown");
86                write!(f, "Chain: node - {}, runtime - {}", node_name, runtime_name)
87            }
88            Self::Contract(info) => write!(f, "Contract: {}", info.name),
89        }
90    }
91}
92
93pub fn get_package_name(path: &Path) -> SubstrateResult<Option<String>> {
94    // Get node name from manifest file
95    let name = if path.join("Cargo.toml").exists() {
96        let manifest = Manifest::new(path.join("Cargo.toml")).read_document()?;
97        Some(
98            manifest["package"]
99                .get("name")
100                .with_context(|| "no name found in manifest")?
101                .as_str()
102                .with_context(|| "unknown name type in manifest")?
103                .to_string(),
104        )
105    } else {
106        None
107    };
108
109    Ok(name)
110}
111
112// TODO: Move `inquire` related code to the bin module
113fn deduce_project_type_for_wd(
114    cwd: &Path,
115    node_path: &Path,
116    runtime_path: &Path,
117    frontend_path: &Path,
118) -> SubstrateResult<Option<ProjectType>> {
119    if cwd.join(node_path).exists() || cwd.join(runtime_path).exists() {
120        if Confirm::new("Found a potential chain project in the current directory. Do you want to continue? (y/n)").prompt()? {
121                let mut manifest = Manifest::new(cwd.join("Substrate.toml"));
122                let mut document = Document::new();
123                document.insert("type", value("chain"));
124                manifest.write_document(document)?;
125            } else {
126                return Ok(None);
127            }
128
129        let node_name = get_package_name(&cwd.join(node_path))?;
130        let runtime_name = get_package_name(&cwd.join(runtime_path))?;
131
132        return Ok(Some(ProjectType::Chain(ChainInfo {
133            node_path: PathBuf::from(node_path),
134            node_name,
135            runtime_path: PathBuf::from(runtime_path),
136            runtime_name,
137            frontend_path: PathBuf::from(frontend_path),
138        })));
139    } else if cwd.join("lib.rs").exists() {
140        if Confirm::new("Found a potential smart contract project in the current directory. Do you want to continue? (y/n)").prompt()? {
141                let mut manifest = Manifest::new(cwd.join("Substrate.toml"));
142                let mut document = Document::new();
143                document.insert("type", value("contract"));
144                manifest.write_document(document)?;
145            } else {
146                return Ok(None);
147            }
148
149        // Get contract name from manifest file
150        let contract_manifest = Manifest::new(cwd.join("Cargo.toml")).read_document()?;
151        let contract_name = contract_manifest["package"]
152            .get("name")
153            .with_context(|| "no name found in contract manifest")?
154            .as_str()
155            .with_context(|| "unknown name type in contract manifest")?
156            .to_string();
157
158        return Ok(Some(ProjectType::Contract(ContractInfo {
159            name: contract_name,
160        })));
161    }
162
163    Ok(None)
164}
165
166impl Config {
167    // This is typically used for tests.
168    // Normally we'd used the default method to build a config.
169    pub fn new(cwd: PathBuf, project_type: Option<ProjectType>) -> Self {
170        Self {
171            // shell: RefCell::new(shell),
172            cwd,
173            project_type,
174        }
175    }
176
177    #[allow(clippy::should_implement_trait)]
178    pub fn default() -> SubstrateResult<Self> {
179        // let shell = Shell::new();
180        let cwd = env::current_dir()
181            .with_context(|| "couldn't get the current directory of the process")?;
182
183        let manifest = Manifest::new(cwd.join("Substrate.toml"))
184            .read_document()
185            .ok();
186
187        let node_path =
188            from_manifest_or_default::<PathBuf>(manifest.as_ref(), "paths.node", "node");
189        let runtime_path =
190            from_manifest_or_default::<PathBuf>(manifest.as_ref(), "paths.runtime", "runtime");
191        let frontend_path =
192            from_manifest_or_default::<PathBuf>(manifest.as_ref(), "paths.frontend", "frontend");
193
194        let contract_path =
195            from_manifest_or_default::<PathBuf>(manifest.as_ref(), "paths.contract", "");
196
197        let project_type = match manifest.as_ref() {
198            Some(doc) => match doc["type"].as_str() {
199                Some("chain") => {
200                    let node_name = get_package_name(&cwd.join(&node_path))?;
201                    let runtime_name = get_package_name(&cwd.join(&runtime_path))?;
202
203                    Some(ProjectType::Chain(ChainInfo {
204                        node_path,
205                        node_name,
206                        runtime_path,
207                        runtime_name,
208                        frontend_path,
209                    }))
210                }
211                Some("contract") => {
212                    let name = get_package_name(&cwd.join(contract_path))?;
213                    Some(ProjectType::Contract(ContractInfo {
214                        name: name.unwrap(),
215                    }))
216                }
217                _ => anyhow::bail!("Incorrect project type in Substrate.toml.\nSupported types: \"chain\" and \"contract\"."),
218            },
219            None => deduce_project_type_for_wd(&cwd, &node_path, &runtime_path, &frontend_path)?,
220        };
221
222        Ok(Self::new(cwd, project_type))
223    }
224
225    /// Gets a reference to the shell, e.g., for writing error messages.
226    // pub fn shell(&self) -> RefMut<'_, Shell> {
227    //     self.shell.borrow_mut()
228    // }
229
230    /// The current working directory.
231    pub fn cwd(&self) -> &Path {
232        &self.cwd
233    }
234
235    /// Retrieves the project type.
236    pub fn project_type(&self) -> &Option<ProjectType> {
237        &self.project_type
238    }
239}