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
48pub use algocline_core::QueryResponse;
50
51pub(crate) fn resolve_code(
54 code: Option<String>,
55 code_file: Option<String>,
56) -> Result<String, String> {
57 match (code, code_file) {
58 (Some(c), None) => Ok(c),
59 (None, Some(path)) => std::fs::read_to_string(Path::new(&path))
60 .map_err(|e| format!("Failed to read {path}: {e}")),
61 (Some(_), Some(_)) => Err("Provide either `code` or `code_file`, not both.".into()),
62 (None, None) => Err("Either `code` or `code_file` must be provided.".into()),
63 }
64}
65
66pub(crate) fn make_require_code(name: &str) -> String {
88 format!(
89 r#"local pkg = require("{name}")
90return pkg.run(ctx)"#
91 )
92}
93
94pub(crate) fn packages_dir() -> Result<PathBuf, String> {
95 let home = dirs::home_dir().ok_or("Cannot determine home directory")?;
96 Ok(home.join(".algocline").join("packages"))
97}
98
99pub(crate) fn scenarios_dir() -> Result<PathBuf, String> {
100 let home = dirs::home_dir().ok_or("Cannot determine home directory")?;
101 Ok(home.join(".algocline").join("scenarios"))
102}
103
104pub(crate) fn resolve_scenario_code(
107 scenario: Option<String>,
108 scenario_file: Option<String>,
109 scenario_name: Option<String>,
110) -> Result<String, String> {
111 match (scenario, scenario_file, scenario_name) {
112 (Some(c), None, None) => Ok(c),
113 (None, Some(path), None) => std::fs::read_to_string(Path::new(&path))
114 .map_err(|e| format!("Failed to read {path}: {e}")),
115 (None, None, Some(name)) => {
116 let dir = scenarios_dir()?;
117 let path = ContainedPath::child(&dir, &format!("{name}.lua"))
118 .map_err(|e| format!("Invalid scenario name: {e}"))?;
119 if !path.as_ref().exists() {
120 return Err(format!(
121 "Scenario '{name}' not found at {}",
122 path.as_ref().display()
123 ));
124 }
125 std::fs::read_to_string(path.as_ref())
126 .map_err(|e| format!("Failed to read scenario '{name}': {e}"))
127 }
128 (None, None, None) => {
129 Err("Provide one of: scenario, scenario_file, or scenario_name.".into())
130 }
131 _ => Err(
132 "Provide only one of: scenario, scenario_file, or scenario_name (not multiple).".into(),
133 ),
134 }
135}
136
137pub(super) const AUTO_INSTALL_SOURCES: &[&str] = &[
140 "https://github.com/ynishi/algocline-bundled-packages",
141 "https://github.com/ynishi/evalframe",
142];
143
144const SYSTEM_PACKAGES: &[&str] = &["evalframe"];
147
148pub(super) fn is_system_package(name: &str) -> bool {
150 SYSTEM_PACKAGES.contains(&name)
151}
152
153pub(super) fn is_package_installed(name: &str) -> bool {
155 packages_dir()
156 .map(|dir| dir.join(name).join("init.lua").exists())
157 .unwrap_or(false)
158}
159
160pub(super) type DirEntryFailures = Vec<String>;
169
170pub(super) fn display_name(path: &Path, file_name: &str) -> String {
172 path.file_stem()
173 .and_then(|s| s.to_str())
174 .map(String::from)
175 .unwrap_or_else(|| file_name.to_string())
176}
177
178pub(super) fn resolve_scenario_source(clone_root: &Path) -> PathBuf {
190 let subdir = clone_root.join("scenarios");
191 if subdir.is_dir() {
192 subdir
193 } else {
194 clone_root.to_path_buf()
195 }
196}
197
198pub(super) fn install_scenarios_from_dir(source: &Path, dest: &Path) -> Result<String, String> {
202 let entries =
203 std::fs::read_dir(source).map_err(|e| format!("Failed to read source dir: {e}"))?;
204
205 let mut installed = Vec::new();
206 let mut skipped = Vec::new();
207 let mut failures: DirEntryFailures = Vec::new();
208
209 for entry_result in entries {
210 let entry = match entry_result {
211 Ok(e) => e,
212 Err(e) => {
213 failures.push(format!("readdir entry: {e}"));
214 continue;
215 }
216 };
217 let path = entry.path();
218 if !path.is_file() {
219 continue;
220 }
221 let ext = path.extension().and_then(|s| s.to_str());
222 if ext != Some("lua") {
223 continue;
224 }
225 let file_name = entry.file_name().to_string_lossy().to_string();
226 let dest_path = match ContainedPath::child(dest, &file_name) {
227 Ok(p) => p,
228 Err(_) => continue,
229 };
230 let name = display_name(&path, &file_name);
231 if dest_path.as_ref().exists() {
232 skipped.push(name);
233 continue;
234 }
235 match std::fs::copy(&path, dest_path.as_ref()) {
236 Ok(_) => installed.push(name),
237 Err(e) => failures.push(format!("{}: {e}", path.display())),
238 }
239 }
240
241 if installed.is_empty() && skipped.is_empty() && failures.is_empty() {
242 return Err("No .lua scenario files found in source.".into());
243 }
244
245 Ok(serde_json::json!({
246 "installed": installed,
247 "skipped": skipped,
248 "failures": failures,
249 })
250 .to_string())
251}