Skip to main content

synapse_core/
scenarios.rs

1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::path::{Path, PathBuf};
4use tokio::fs;
5
6#[derive(Debug, Deserialize, Serialize, Clone)]
7pub struct RegistryEntry {
8    pub name: String,
9    pub description: String,
10    pub version: String,
11    pub location: String,
12}
13
14#[derive(Debug, Deserialize, Serialize, Clone)]
15pub struct Manifest {
16    pub name: String,
17    pub version: String,
18    pub description: String,
19    #[serde(default)]
20    pub ontologies: Vec<String>,
21    #[serde(default)]
22    pub data_files: Vec<String>,
23    #[serde(default)]
24    pub docs: Vec<String>,
25}
26
27pub struct ScenarioManager {
28    base_path: PathBuf,
29    client: reqwest::Client,
30}
31
32impl ScenarioManager {
33    pub fn new(base_path: impl AsRef<Path>) -> Self {
34        Self {
35            base_path: base_path.as_ref().to_path_buf(),
36            client: reqwest::Client::new(),
37        }
38    }
39
40    /// Fetches the list of available scenarios from the registry.
41    pub async fn list_scenarios(&self) -> Result<Vec<RegistryEntry>> {
42        // Try local registry first (useful for development)
43        let local_registry = Path::new("scenarios/registry.json");
44        if local_registry.exists() {
45            let content = fs::read_to_string(local_registry).await?;
46            let registry: Vec<RegistryEntry> = serde_json::from_str(&content)?;
47            return Ok(registry);
48        }
49
50        // Fallback to remote registry
51        let url =
52            "https://raw.githubusercontent.com/pmaojo/synapse-engine/main/scenarios/registry.json";
53        let resp = self
54            .client
55            .get(url)
56            .send()
57            .await
58            .context("Failed to fetch remote registry")?;
59
60        if !resp.status().is_success() {
61            return Err(anyhow::anyhow!(
62                "Failed to fetch registry: {}",
63                resp.status()
64            ));
65        }
66
67        let registry: Vec<RegistryEntry> =
68            resp.json().await.context("Failed to parse registry JSON")?;
69        Ok(registry)
70    }
71
72    /// Installs a scenario by name.
73    /// Returns the path to the installed scenario directory.
74    pub async fn install_scenario(&self, name: &str) -> Result<PathBuf> {
75        let registry = self.list_scenarios().await?;
76        let entry = registry
77            .iter()
78            .find(|e| e.name == name)
79            .ok_or_else(|| anyhow::anyhow!("Scenario '{}' not found in registry", name))?;
80
81        let scenario_dir = self.base_path.join("scenarios").join(name);
82        fs::create_dir_all(&scenario_dir).await?;
83
84        // Check if we can install from local source (dev mode)
85        // Since we are running from the repo root usually, check if `scenarios/{name}` exists there.
86        let local_source = Path::new("scenarios").join(name);
87        if local_source.exists() && local_source.join("manifest.json").exists() {
88            return self
89                .install_from_local_path(&local_source, &scenario_dir)
90                .await;
91        }
92
93        // If not local, try to download from the URL in registry
94        if entry.location.starts_with("http") {
95            return self
96                .install_from_remote(&entry.location, &scenario_dir)
97                .await;
98        }
99
100        Err(anyhow::anyhow!(
101            "Could not find installation source for scenario '{}'",
102            name
103        ))
104    }
105
106    async fn install_from_local_path(&self, source: &Path, dest: &Path) -> Result<PathBuf> {
107        // Prevent self-copy
108        if source.canonicalize()? == dest.canonicalize().unwrap_or(dest.to_path_buf()) {
109            eprintln!("Source and destination are the same, skipping copy.");
110            return Ok(dest.to_path_buf());
111        }
112
113        // 1. Copy Manifest
114        let manifest_path = source.join("manifest.json");
115        fs::copy(&manifest_path, dest.join("manifest.json")).await?;
116
117        let content = fs::read_to_string(&manifest_path).await?;
118        let manifest: Manifest = serde_json::from_str(&content)?;
119
120        // 2. Copy Ontologies
121        if !manifest.ontologies.is_empty() {
122            let schema_dest = dest.join("schema");
123            fs::create_dir_all(&schema_dest).await?;
124            for file in &manifest.ontologies {
125                let src = source.join("schema").join(file);
126                if src.exists() {
127                    fs::copy(&src, schema_dest.join(file)).await?;
128                }
129            }
130        }
131
132        // 3. Copy Data
133        if !manifest.data_files.is_empty() {
134            let data_dest = dest.join("data");
135            fs::create_dir_all(&data_dest).await?;
136            for file in &manifest.data_files {
137                let src = source.join("data").join(file);
138                if src.exists() {
139                    fs::copy(&src, data_dest.join(file)).await?;
140                }
141            }
142        }
143
144        // 4. Copy Docs
145        if !manifest.docs.is_empty() {
146            let docs_dest = dest.join("docs");
147            fs::create_dir_all(&docs_dest).await?;
148            for file in &manifest.docs {
149                let src = source.join("docs").join(file);
150                if src.exists() {
151                    fs::copy(&src, docs_dest.join(file)).await?;
152                }
153            }
154        }
155
156        Ok(dest.to_path_buf())
157    }
158
159    async fn install_from_remote(&self, base_url: &str, dest: &Path) -> Result<PathBuf> {
160        let clean_base = base_url.trim_end_matches('/');
161
162        // 1. Fetch Manifest
163        let manifest_url = format!("{}/manifest.json", clean_base);
164        let resp = self.client.get(&manifest_url).send().await?;
165        if !resp.status().is_success() {
166            return Err(anyhow::anyhow!(
167                "Failed to fetch manifest from {}",
168                manifest_url
169            ));
170        }
171        let content = resp.text().await?;
172        fs::write(dest.join("manifest.json"), &content).await?;
173
174        let manifest: Manifest = serde_json::from_str(&content)?;
175
176        // 2. Download Ontologies
177        if !manifest.ontologies.is_empty() {
178            let schema_dest = dest.join("schema");
179            fs::create_dir_all(&schema_dest).await?;
180            for file in &manifest.ontologies {
181                let url = format!("{}/schema/{}", clean_base, file);
182                self.download_file(&url, &schema_dest.join(file)).await?;
183            }
184        }
185
186        // 3. Download Data
187        if !manifest.data_files.is_empty() {
188            let data_dest = dest.join("data");
189            fs::create_dir_all(&data_dest).await?;
190            for file in &manifest.data_files {
191                let url = format!("{}/data/{}", clean_base, file);
192                self.download_file(&url, &data_dest.join(file)).await?;
193            }
194        }
195
196        // 4. Download Docs
197        if !manifest.docs.is_empty() {
198            let docs_dest = dest.join("docs");
199            fs::create_dir_all(&docs_dest).await?;
200            for file in &manifest.docs {
201                let url = format!("{}/docs/{}", clean_base, file);
202                self.download_file(&url, &docs_dest.join(file)).await?;
203            }
204        }
205
206        Ok(dest.to_path_buf())
207    }
208
209    async fn download_file(&self, url: &str, dest: &Path) -> Result<()> {
210        let resp = self.client.get(url).send().await?;
211        if !resp.status().is_success() {
212            return Err(anyhow::anyhow!(
213                "Failed to download {}: status {}",
214                url,
215                resp.status()
216            ));
217        }
218        let bytes = resp.bytes().await?;
219        fs::write(dest, bytes).await?;
220        Ok(())
221    }
222
223    pub async fn get_manifest(&self, scenario_name: &str) -> Result<Manifest> {
224        let manifest_path = self
225            .base_path
226            .join("scenarios")
227            .join(scenario_name)
228            .join("manifest.json");
229        if !manifest_path.exists() {
230            // Try to find it in the repo root scenarios/ folder if dev
231            let local_dev_path = Path::new("scenarios")
232                .join(scenario_name)
233                .join("manifest.json");
234            if local_dev_path.exists() {
235                let content = fs::read_to_string(local_dev_path).await?;
236                return Ok(serde_json::from_str(&content)?);
237            }
238            return Err(anyhow::anyhow!(
239                "Scenario '{}' is not installed",
240                scenario_name
241            ));
242        }
243        let content = fs::read_to_string(manifest_path).await?;
244        Ok(serde_json::from_str(&content)?)
245    }
246}