use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use tokio::fs;
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct RegistryEntry {
pub name: String,
pub description: String,
pub version: String,
pub location: String,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Manifest {
pub name: String,
pub version: String,
pub description: String,
#[serde(default)]
pub ontologies: Vec<String>,
#[serde(default)]
pub data_files: Vec<String>,
#[serde(default)]
pub docs: Vec<String>,
}
pub struct ScenarioManager {
base_path: PathBuf,
client: reqwest::Client,
}
impl ScenarioManager {
pub fn new(base_path: impl AsRef<Path>) -> Self {
Self {
base_path: base_path.as_ref().to_path_buf(),
client: reqwest::Client::new(),
}
}
pub async fn list_scenarios(&self) -> Result<Vec<RegistryEntry>> {
let local_registry = Path::new("scenarios/registry.json");
if local_registry.exists() {
let content = fs::read_to_string(local_registry).await?;
let registry: Vec<RegistryEntry> = serde_json::from_str(&content)?;
return Ok(registry);
}
let url =
"https://raw.githubusercontent.com/pmaojo/synapse-engine/main/scenarios/registry.json";
let resp = self
.client
.get(url)
.send()
.await
.context("Failed to fetch remote registry")?;
if !resp.status().is_success() {
return Err(anyhow::anyhow!(
"Failed to fetch registry: {}",
resp.status()
));
}
let registry: Vec<RegistryEntry> =
resp.json().await.context("Failed to parse registry JSON")?;
Ok(registry)
}
pub async fn install_scenario(&self, name: &str) -> Result<PathBuf> {
let registry = self.list_scenarios().await?;
let entry = registry
.iter()
.find(|e| e.name == name)
.ok_or_else(|| anyhow::anyhow!("Scenario '{}' not found in registry", name))?;
let scenario_dir = self.base_path.join("scenarios").join(name);
fs::create_dir_all(&scenario_dir).await?;
let local_source = Path::new("scenarios").join(name);
if local_source.exists() && local_source.join("manifest.json").exists() {
return self
.install_from_local_path(&local_source, &scenario_dir)
.await;
}
if entry.location.starts_with("http") {
return self
.install_from_remote(&entry.location, &scenario_dir)
.await;
}
Err(anyhow::anyhow!(
"Could not find installation source for scenario '{}'",
name
))
}
async fn install_from_local_path(&self, source: &Path, dest: &Path) -> Result<PathBuf> {
if source.canonicalize()? == dest.canonicalize().unwrap_or(dest.to_path_buf()) {
eprintln!("Source and destination are the same, skipping copy.");
return Ok(dest.to_path_buf());
}
let manifest_path = source.join("manifest.json");
fs::copy(&manifest_path, dest.join("manifest.json")).await?;
let content = fs::read_to_string(&manifest_path).await?;
let manifest: Manifest = serde_json::from_str(&content)?;
if !manifest.ontologies.is_empty() {
let schema_dest = dest.join("schema");
fs::create_dir_all(&schema_dest).await?;
for file in &manifest.ontologies {
let src = source.join("schema").join(file);
if src.exists() {
fs::copy(&src, schema_dest.join(file)).await?;
}
}
}
if !manifest.data_files.is_empty() {
let data_dest = dest.join("data");
fs::create_dir_all(&data_dest).await?;
for file in &manifest.data_files {
let src = source.join("data").join(file);
if src.exists() {
fs::copy(&src, data_dest.join(file)).await?;
}
}
}
if !manifest.docs.is_empty() {
let docs_dest = dest.join("docs");
fs::create_dir_all(&docs_dest).await?;
for file in &manifest.docs {
let src = source.join("docs").join(file);
if src.exists() {
fs::copy(&src, docs_dest.join(file)).await?;
}
}
}
Ok(dest.to_path_buf())
}
async fn install_from_remote(&self, base_url: &str, dest: &Path) -> Result<PathBuf> {
let clean_base = base_url.trim_end_matches('/');
let manifest_url = format!("{}/manifest.json", clean_base);
let resp = self.client.get(&manifest_url).send().await?;
if !resp.status().is_success() {
return Err(anyhow::anyhow!(
"Failed to fetch manifest from {}",
manifest_url
));
}
let content = resp.text().await?;
fs::write(dest.join("manifest.json"), &content).await?;
let manifest: Manifest = serde_json::from_str(&content)?;
if !manifest.ontologies.is_empty() {
let schema_dest = dest.join("schema");
fs::create_dir_all(&schema_dest).await?;
for file in &manifest.ontologies {
let url = format!("{}/schema/{}", clean_base, file);
self.download_file(&url, &schema_dest.join(file)).await?;
}
}
if !manifest.data_files.is_empty() {
let data_dest = dest.join("data");
fs::create_dir_all(&data_dest).await?;
for file in &manifest.data_files {
let url = format!("{}/data/{}", clean_base, file);
self.download_file(&url, &data_dest.join(file)).await?;
}
}
if !manifest.docs.is_empty() {
let docs_dest = dest.join("docs");
fs::create_dir_all(&docs_dest).await?;
for file in &manifest.docs {
let url = format!("{}/docs/{}", clean_base, file);
self.download_file(&url, &docs_dest.join(file)).await?;
}
}
Ok(dest.to_path_buf())
}
async fn download_file(&self, url: &str, dest: &Path) -> Result<()> {
let resp = self.client.get(url).send().await?;
if !resp.status().is_success() {
return Err(anyhow::anyhow!(
"Failed to download {}: status {}",
url,
resp.status()
));
}
let bytes = resp.bytes().await?;
fs::write(dest, bytes).await?;
Ok(())
}
pub async fn get_manifest(&self, scenario_name: &str) -> Result<Manifest> {
let manifest_path = self
.base_path
.join("scenarios")
.join(scenario_name)
.join("manifest.json");
if !manifest_path.exists() {
let local_dev_path = Path::new("scenarios")
.join(scenario_name)
.join("manifest.json");
if local_dev_path.exists() {
let content = fs::read_to_string(local_dev_path).await?;
return Ok(serde_json::from_str(&content)?);
}
return Err(anyhow::anyhow!(
"Scenario '{}' is not installed",
scenario_name
));
}
let content = fs::read_to_string(manifest_path).await?;
Ok(serde_json::from_str(&content)?)
}
}