algocline_app/service/
scenario.rs1use std::path::Path;
2
3use super::path::ContainedPath;
4use super::resolve::{
5 install_scenarios_from_dir, resolve_scenario_source, scenarios_dir, DirEntryFailures,
6};
7use super::AppService;
8
9impl AppService {
10 pub fn scenario_list(&self) -> Result<String, String> {
14 let dir = scenarios_dir()?;
15 if !dir.exists() {
16 return Ok(serde_json::json!({ "scenarios": [], "failures": [] }).to_string());
17 }
18
19 let entries =
20 std::fs::read_dir(&dir).map_err(|e| format!("Failed to read scenarios dir: {e}"))?;
21
22 let mut scenarios: Vec<serde_json::Value> = Vec::new();
23 let mut failures: DirEntryFailures = Vec::new();
24 for entry_result in entries {
25 let entry = match entry_result {
26 Ok(e) => e,
27 Err(e) => {
28 failures.push(format!("readdir entry: {e}"));
29 continue;
30 }
31 };
32 let path = entry.path();
33 let name = match path.file_stem().and_then(|s| s.to_str()) {
34 Some(s) => s.to_string(),
35 None => continue,
36 };
37 let ext = path.extension().and_then(|s| s.to_str());
38 if ext != Some("lua") {
39 continue;
40 }
41 let metadata = std::fs::metadata(&path);
42 let size_bytes = metadata.as_ref().map(|m| m.len()).unwrap_or(0);
43 scenarios.push(serde_json::json!({
44 "name": name,
45 "path": path.to_string_lossy(),
46 "size_bytes": size_bytes,
47 }));
48 }
49
50 scenarios.sort_by(|a, b| {
51 a.get("name")
52 .and_then(|v| v.as_str())
53 .cmp(&b.get("name").and_then(|v| v.as_str()))
54 });
55
56 Ok(serde_json::json!({
57 "scenarios": scenarios,
58 "failures": failures,
59 })
60 .to_string())
61 }
62
63 pub fn scenario_show(&self, name: &str) -> Result<String, String> {
65 let dir = scenarios_dir()?;
66 let path = ContainedPath::child(&dir, &format!("{name}.lua"))
67 .map_err(|e| format!("Invalid scenario name: {e}"))?;
68 if !path.as_ref().exists() {
69 return Err(format!("Scenario '{name}' not found"));
70 }
71 let content = std::fs::read_to_string(path.as_ref())
72 .map_err(|e| format!("Failed to read scenario '{name}': {e}"))?;
73 Ok(serde_json::json!({
74 "name": name,
75 "path": path.as_ref().to_string_lossy(),
76 "content": content,
77 })
78 .to_string())
79 }
80
81 pub async fn scenario_install(&self, url: String) -> Result<String, String> {
85 let dest_dir = scenarios_dir()?;
86 std::fs::create_dir_all(&dest_dir)
87 .map_err(|e| format!("Failed to create scenarios dir: {e}"))?;
88
89 let local_path = Path::new(&url);
91 if local_path.is_absolute() && local_path.is_dir() {
92 return install_scenarios_from_dir(local_path, &dest_dir);
93 }
94
95 let git_url = if url.starts_with("http://")
97 || url.starts_with("https://")
98 || url.starts_with("file://")
99 || url.starts_with("git@")
100 {
101 url.clone()
102 } else {
103 format!("https://{url}")
104 };
105
106 let staging = tempfile::tempdir().map_err(|e| format!("Failed to create temp dir: {e}"))?;
107
108 let output = tokio::process::Command::new("git")
109 .args([
110 "clone",
111 "--depth",
112 "1",
113 &git_url,
114 &staging.path().to_string_lossy(),
115 ])
116 .output()
117 .await
118 .map_err(|e| format!("Failed to run git: {e}"))?;
119
120 if !output.status.success() {
121 let stderr = String::from_utf8_lossy(&output.stderr);
122 return Err(format!("git clone failed: {stderr}"));
123 }
124
125 let source = resolve_scenario_source(staging.path());
126 install_scenarios_from_dir(&source, &dest_dir)
127 }
128}