rush_ecs_manifest/
manifest.rs

1//! Rush Manifest definition and utils
2//!
3//! Filename: Rush.toml
4
5use crate::error::{utils::ensure_syntax, ManifestError};
6use anyhow::{bail, Result};
7use std::{
8    convert::From,
9    fs::{read_to_string, File},
10    io::Write,
11    path::Path,
12};
13use toml::Table;
14
15#[derive(Debug, Clone, Eq, PartialEq)]
16pub enum Repository {
17    InMemory,
18    Solana,
19}
20
21impl From<Repository> for String {
22    fn from(repository: Repository) -> Self {
23        match repository {
24            Repository::InMemory => "memory".to_string(),
25            Repository::Solana => "solana".to_string(),
26        }
27    }
28}
29
30#[derive(Debug, Clone, Eq, PartialEq)]
31pub enum Chain {
32    Solana {
33        // proxy: String,
34        store: String,
35        rpc: String,
36        // websocket: String,
37        keypair: String,
38    },
39}
40
41#[derive(Debug, Clone, Eq, PartialEq)]
42pub struct Manifest {
43    pub name: String,
44    pub storage: Repository,
45    pub chain: Chain,
46}
47
48impl Manifest {
49    pub const FILENAME: &'static str = "Rush.toml";
50
51    pub fn new_solana(name: String) -> Self {
52        Self {
53            name,
54            storage: Repository::Solana,
55            chain: Chain::Solana {
56                // proxy: String::default(),
57                store: String::default(),
58                rpc: String::default(),
59                // websocket: String::default(),
60                keypair: String::default(),
61            },
62        }
63    }
64
65    // TODO: (REVIEW) Consider creating a trait for parsing
66    pub fn from_toml(path: &str) -> Result<Manifest> {
67        let manifest_path = match Path::new(path).canonicalize() {
68            Ok(p) => p,
69            Err(e) => bail!(e),
70        };
71
72        let manifest_string = read_to_string(manifest_path)?;
73
74        let table: Table = match manifest_string.parse::<Table>() {
75            Ok(t) => t,
76            Err(e) => bail!(e),
77        };
78
79        /*
80         * Workspace Table
81         */
82
83        let workspace_table = match table["workspace"].as_table() {
84            Some(t) => t,
85            None => bail!(ManifestError::MissingTable("workspace".to_string())),
86        };
87
88        ensure_syntax(
89            "Workspace must have a name".to_string(),
90            workspace_table.contains_key("name"),
91        );
92
93        // unwrap, ok
94        let name = workspace_table.get("name").unwrap().as_str().unwrap();
95
96        /*
97         * Storage Table
98         */
99
100        let storage_table = match table["storage"].as_table() {
101            Some(t) => t,
102            None => bail!(ManifestError::MissingTable("storage".to_string())),
103        };
104
105        ensure_syntax(
106            "Workspace storage must have a repository".to_string(),
107            storage_table.contains_key("repository"),
108        );
109
110        // unwrap, ok
111        let repo_value = storage_table.get("repository").unwrap().as_str().unwrap();
112        let repository = Self::parse_repository(repo_value.to_string())?;
113
114        let mut manifest = Self::new_solana(name.to_string());
115        manifest.storage = repository;
116
117        /*
118         * Solana Table
119         */
120
121        let solana_table = match table["solana"].as_table() {
122            Some(t) => t,
123            None => bail!(ManifestError::MissingTable("solana".to_string())),
124        };
125
126        // ensure_syntax(
127        //     "Solana table must have a proxy".to_string(),
128        //     solana_table.contains_key("proxy"),
129        // );
130        ensure_syntax(
131            "Solana table must have a store".to_string(),
132            solana_table.contains_key("store"),
133        );
134        ensure_syntax(
135            "Solana table must have an rpc".to_string(),
136            solana_table.contains_key("rpc"),
137        );
138        // ensure_syntax(
139        //     "Solana table must have a websocket".to_string(),
140        //     solana_table.contains_key("websocket"),
141        // );
142        ensure_syntax(
143            "Solana table must have a keypair".to_string(),
144            solana_table.contains_key("keypair"),
145        );
146
147        // TODO: Remove unwraps
148        let store = solana_table
149            .get("store")
150            .unwrap()
151            .as_str()
152            .unwrap()
153            .to_string();
154        let rpc = solana_table
155            .get("rpc")
156            .unwrap()
157            .as_str()
158            .unwrap()
159            .to_string();
160        // let websocket_url = solana_table.get("websocket").unwrap().as_str().unwrap().to_string();
161        let keypair = solana_table
162            .get("keypair")
163            .unwrap()
164            .as_str()
165            .unwrap()
166            .to_string();
167
168        manifest.chain = Chain::Solana {
169            store,
170            rpc,
171            keypair,
172        };
173
174        Ok(manifest)
175    }
176
177    pub fn save_toml(manifest: Manifest, path: &str) -> Result<()> {
178        let name = manifest.name;
179        let repo: String = manifest.storage.into();
180        let Chain::Solana {
181            store,
182            rpc,
183            keypair,
184        } = manifest.chain;
185
186        let filename = Self::FILENAME;
187
188        let path = Path::new(path);
189        let manifest_path = path.join(filename);
190        let mut toml_file = File::create(manifest_path)?;
191
192        // TODO: (REVIEW) Find better formatting (r#?)
193        toml_file.write_all(
194            format!("[workspace]\nname = \"{name}\"\n\n[storage]\nrepository = \"{repo}\"\n\n[solana]\nstore = \"{store}\"\nrpc = \"{rpc}\"\nkeypair = \"{keypair}\"",
195            ).as_bytes())?;
196
197        Ok(())
198    }
199
200    fn parse_repository(repo_string: String) -> Result<Repository> {
201        let repo = match repo_string.as_str() {
202            "solana" => Repository::Solana,
203            "memory" => Repository::InMemory,
204            _ => bail!(ManifestError::UnsupportedRepo(repo_string)),
205        };
206
207        Ok(repo)
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    // TODO: CHANGE ALL TESTS TO TOKIO
216    #[test]
217    fn test_from_toml() {
218        let manifest = Manifest::from_toml("fixtures/Rush.toml").unwrap();
219        assert_eq!(manifest.name, "WORKSPACE");
220        assert_eq!(manifest.storage, Repository::Solana);
221        assert_eq!(
222            manifest.chain,
223            Chain::Solana {
224                store: "STORE".to_string(),
225                rpc: "RPC".to_string(),
226                keypair: "KEYPAIR".to_string()
227            }
228        );
229    }
230
231    #[test]
232    // TODO: Add string matching for test
233    fn test_save_toml_without_trailing_slash() {
234        let mut manifest = Manifest::new_solana("WORKSPACE".to_string());
235        let store = "STORE".to_string();
236        let rpc = "RPC".to_string();
237        let keypair = "KEYPAIR".to_string();
238
239        manifest.chain = Chain::Solana {
240            store: store.clone(),
241            rpc: rpc.clone(),
242            keypair: keypair.clone(),
243        };
244
245        Manifest::save_toml(manifest, "fixtures/save").unwrap();
246    }
247
248    #[test]
249    // TODO: Add string matching for test
250    fn test_save_toml_with_trailing_slash() {
251        let mut manifest = Manifest::new_solana("WORKSPACE".to_string());
252        let store = "STORE".to_string();
253        let rpc = "RPC".to_string();
254        let keypair = "KEYPAIR".to_string();
255
256        manifest.chain = Chain::Solana {
257            store: store.clone(),
258            rpc: rpc.clone(),
259            keypair: keypair.clone(),
260        };
261
262        Manifest::save_toml(manifest, "fixtures/save/").unwrap();
263    }
264}