algocline_app/service/
resolve.rs1use std::path::{Path, PathBuf};
2
3use super::path::ContainedPath;
4
5#[derive(Debug)]
9pub struct QueryResponse {
10 pub query_id: String,
12 pub response: String,
14}
15
16pub(crate) fn resolve_code(
19 code: Option<String>,
20 code_file: Option<String>,
21) -> Result<String, String> {
22 match (code, code_file) {
23 (Some(c), None) => Ok(c),
24 (None, Some(path)) => std::fs::read_to_string(Path::new(&path))
25 .map_err(|e| format!("Failed to read {path}: {e}")),
26 (Some(_), Some(_)) => Err("Provide either `code` or `code_file`, not both.".into()),
27 (None, None) => Err("Either `code` or `code_file` must be provided.".into()),
28 }
29}
30
31pub(crate) fn make_require_code(name: &str) -> String {
53 format!(
54 r#"local pkg = require("{name}")
55return pkg.run(ctx)"#
56 )
57}
58
59pub(crate) fn packages_dir() -> Result<PathBuf, String> {
60 let home = dirs::home_dir().ok_or("Cannot determine home directory")?;
61 Ok(home.join(".algocline").join("packages"))
62}
63
64pub(crate) fn scenarios_dir() -> Result<PathBuf, String> {
65 let home = dirs::home_dir().ok_or("Cannot determine home directory")?;
66 Ok(home.join(".algocline").join("scenarios"))
67}
68
69pub(crate) fn resolve_scenario_code(
72 scenario: Option<String>,
73 scenario_file: Option<String>,
74 scenario_name: Option<String>,
75) -> Result<String, String> {
76 match (scenario, scenario_file, scenario_name) {
77 (Some(c), None, None) => Ok(c),
78 (None, Some(path), None) => std::fs::read_to_string(Path::new(&path))
79 .map_err(|e| format!("Failed to read {path}: {e}")),
80 (None, None, Some(name)) => {
81 let dir = scenarios_dir()?;
82 let path = ContainedPath::child(&dir, &format!("{name}.lua"))
83 .map_err(|e| format!("Invalid scenario name: {e}"))?;
84 if !path.as_ref().exists() {
85 return Err(format!(
86 "Scenario '{name}' not found at {}",
87 path.as_ref().display()
88 ));
89 }
90 std::fs::read_to_string(path.as_ref())
91 .map_err(|e| format!("Failed to read scenario '{name}': {e}"))
92 }
93 (None, None, None) => {
94 Err("Provide one of: scenario, scenario_file, or scenario_name.".into())
95 }
96 _ => Err(
97 "Provide only one of: scenario, scenario_file, or scenario_name (not multiple).".into(),
98 ),
99 }
100}
101
102pub(super) const AUTO_INSTALL_SOURCES: &[&str] = &[
105 "https://github.com/ynishi/algocline-bundled-packages",
106 "https://github.com/ynishi/evalframe",
107];
108
109const SYSTEM_PACKAGES: &[&str] = &["evalframe"];
112
113pub(super) fn is_system_package(name: &str) -> bool {
115 SYSTEM_PACKAGES.contains(&name)
116}
117
118pub(super) fn is_package_installed(name: &str) -> bool {
120 packages_dir()
121 .map(|dir| dir.join(name).join("init.lua").exists())
122 .unwrap_or(false)
123}
124
125pub(super) type DirEntryFailures = Vec<String>;
134
135pub(super) fn display_name(path: &Path, file_name: &str) -> String {
137 path.file_stem()
138 .and_then(|s| s.to_str())
139 .map(String::from)
140 .unwrap_or_else(|| file_name.to_string())
141}
142
143pub(super) fn resolve_scenario_source(clone_root: &Path) -> PathBuf {
155 let subdir = clone_root.join("scenarios");
156 if subdir.is_dir() {
157 subdir
158 } else {
159 clone_root.to_path_buf()
160 }
161}
162
163pub(super) fn install_scenarios_from_dir(source: &Path, dest: &Path) -> Result<String, String> {
167 let entries =
168 std::fs::read_dir(source).map_err(|e| format!("Failed to read source dir: {e}"))?;
169
170 let mut installed = Vec::new();
171 let mut skipped = Vec::new();
172 let mut failures: DirEntryFailures = Vec::new();
173
174 for entry_result in entries {
175 let entry = match entry_result {
176 Ok(e) => e,
177 Err(e) => {
178 failures.push(format!("readdir entry: {e}"));
179 continue;
180 }
181 };
182 let path = entry.path();
183 if !path.is_file() {
184 continue;
185 }
186 let ext = path.extension().and_then(|s| s.to_str());
187 if ext != Some("lua") {
188 continue;
189 }
190 let file_name = entry.file_name().to_string_lossy().to_string();
191 let dest_path = match ContainedPath::child(dest, &file_name) {
192 Ok(p) => p,
193 Err(_) => continue,
194 };
195 let name = display_name(&path, &file_name);
196 if dest_path.as_ref().exists() {
197 skipped.push(name);
198 continue;
199 }
200 match std::fs::copy(&path, dest_path.as_ref()) {
201 Ok(_) => installed.push(name),
202 Err(e) => failures.push(format!("{}: {e}", path.display())),
203 }
204 }
205
206 if installed.is_empty() && skipped.is_empty() && failures.is_empty() {
207 return Err("No .lua scenario files found in source.".into());
208 }
209
210 Ok(serde_json::json!({
211 "installed": installed,
212 "skipped": skipped,
213 "failures": failures,
214 })
215 .to_string())
216}