synapse_core/
scenarios.rs1use 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 pub async fn list_scenarios(&self) -> Result<Vec<RegistryEntry>> {
42 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}