algocline_app/service/
resolve.rs1use std::path::{Path, PathBuf};
2
3use super::path::ContainedPath;
4
5#[derive(Clone, Debug, PartialEq, Eq)]
9pub enum SearchPathSource {
10 Env,
12 Default,
14}
15
16impl std::fmt::Display for SearchPathSource {
17 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18 match self {
19 Self::Env => write!(f, "ALC_PACKAGES_PATH"),
20 Self::Default => write!(f, "default"),
21 }
22 }
23}
24
25#[derive(Clone, Debug)]
27pub struct SearchPath {
28 pub path: PathBuf,
29 pub source: SearchPathSource,
30}
31
32impl SearchPath {
33 pub fn env(path: PathBuf) -> Self {
34 Self {
35 path,
36 source: SearchPathSource::Env,
37 }
38 }
39
40 pub fn default_global(path: PathBuf) -> Self {
41 Self {
42 path,
43 source: SearchPathSource::Default,
44 }
45 }
46}
47
48#[derive(Debug)]
52pub struct QueryResponse {
53 pub query_id: String,
55 pub response: String,
57}
58
59pub(crate) fn resolve_code(
62 code: Option<String>,
63 code_file: Option<String>,
64) -> Result<String, String> {
65 match (code, code_file) {
66 (Some(c), None) => Ok(c),
67 (None, Some(path)) => std::fs::read_to_string(Path::new(&path))
68 .map_err(|e| format!("Failed to read {path}: {e}")),
69 (Some(_), Some(_)) => Err("Provide either `code` or `code_file`, not both.".into()),
70 (None, None) => Err("Either `code` or `code_file` must be provided.".into()),
71 }
72}
73
74pub(crate) fn make_require_code(name: &str) -> String {
96 format!(
97 r#"local pkg = require("{name}")
98return pkg.run(ctx)"#
99 )
100}
101
102pub(crate) fn packages_dir() -> Result<PathBuf, String> {
103 let home = dirs::home_dir().ok_or("Cannot determine home directory")?;
104 Ok(home.join(".algocline").join("packages"))
105}
106
107pub(crate) fn scenarios_dir() -> Result<PathBuf, String> {
108 let home = dirs::home_dir().ok_or("Cannot determine home directory")?;
109 Ok(home.join(".algocline").join("scenarios"))
110}
111
112pub(crate) fn resolve_scenario_code(
115 scenario: Option<String>,
116 scenario_file: Option<String>,
117 scenario_name: Option<String>,
118) -> Result<String, String> {
119 match (scenario, scenario_file, scenario_name) {
120 (Some(c), None, None) => Ok(c),
121 (None, Some(path), None) => std::fs::read_to_string(Path::new(&path))
122 .map_err(|e| format!("Failed to read {path}: {e}")),
123 (None, None, Some(name)) => {
124 let dir = scenarios_dir()?;
125 let path = ContainedPath::child(&dir, &format!("{name}.lua"))
126 .map_err(|e| format!("Invalid scenario name: {e}"))?;
127 if !path.as_ref().exists() {
128 return Err(format!(
129 "Scenario '{name}' not found at {}",
130 path.as_ref().display()
131 ));
132 }
133 std::fs::read_to_string(path.as_ref())
134 .map_err(|e| format!("Failed to read scenario '{name}': {e}"))
135 }
136 (None, None, None) => {
137 Err("Provide one of: scenario, scenario_file, or scenario_name.".into())
138 }
139 _ => Err(
140 "Provide only one of: scenario, scenario_file, or scenario_name (not multiple).".into(),
141 ),
142 }
143}
144
145pub(super) const AUTO_INSTALL_SOURCES: &[&str] = &[
148 "https://github.com/ynishi/algocline-bundled-packages",
149 "https://github.com/ynishi/evalframe",
150];
151
152const SYSTEM_PACKAGES: &[&str] = &["evalframe"];
155
156pub(super) fn is_system_package(name: &str) -> bool {
158 SYSTEM_PACKAGES.contains(&name)
159}
160
161pub(super) fn is_package_installed(name: &str) -> bool {
163 packages_dir()
164 .map(|dir| dir.join(name).join("init.lua").exists())
165 .unwrap_or(false)
166}
167
168pub(super) type DirEntryFailures = Vec<String>;
177
178pub(super) fn display_name(path: &Path, file_name: &str) -> String {
180 path.file_stem()
181 .and_then(|s| s.to_str())
182 .map(String::from)
183 .unwrap_or_else(|| file_name.to_string())
184}
185
186pub(super) fn resolve_scenario_source(clone_root: &Path) -> PathBuf {
198 let subdir = clone_root.join("scenarios");
199 if subdir.is_dir() {
200 subdir
201 } else {
202 clone_root.to_path_buf()
203 }
204}
205
206pub(super) fn install_scenarios_from_dir(source: &Path, dest: &Path) -> Result<String, String> {
210 let entries =
211 std::fs::read_dir(source).map_err(|e| format!("Failed to read source dir: {e}"))?;
212
213 let mut installed = Vec::new();
214 let mut skipped = Vec::new();
215 let mut failures: DirEntryFailures = Vec::new();
216
217 for entry_result in entries {
218 let entry = match entry_result {
219 Ok(e) => e,
220 Err(e) => {
221 failures.push(format!("readdir entry: {e}"));
222 continue;
223 }
224 };
225 let path = entry.path();
226 if !path.is_file() {
227 continue;
228 }
229 let ext = path.extension().and_then(|s| s.to_str());
230 if ext != Some("lua") {
231 continue;
232 }
233 let file_name = entry.file_name().to_string_lossy().to_string();
234 let dest_path = match ContainedPath::child(dest, &file_name) {
235 Ok(p) => p,
236 Err(_) => continue,
237 };
238 let name = display_name(&path, &file_name);
239 if dest_path.as_ref().exists() {
240 skipped.push(name);
241 continue;
242 }
243 match std::fs::copy(&path, dest_path.as_ref()) {
244 Ok(_) => installed.push(name),
245 Err(e) => failures.push(format!("{}: {e}", path.display())),
246 }
247 }
248
249 if installed.is_empty() && skipped.is_empty() && failures.is_empty() {
250 return Err("No .lua scenario files found in source.".into());
251 }
252
253 Ok(serde_json::json!({
254 "installed": installed,
255 "skipped": skipped,
256 "failures": failures,
257 })
258 .to_string())
259}