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 types_stub_path() -> Option<String> {
95 dirs::home_dir()
96 .map(|h| h.join(".algocline").join("types").join("alc.d.lua"))
97 .filter(|p| p.exists())
98 .map(|p| p.display().to_string())
99}
100
101pub(crate) fn packages_dir() -> Result<PathBuf, String> {
102 let home = dirs::home_dir().ok_or("Cannot determine home directory")?;
103 Ok(home.join(".algocline").join("packages"))
104}
105
106pub(crate) fn scenarios_dir() -> Result<PathBuf, String> {
107 let home = dirs::home_dir().ok_or("Cannot determine home directory")?;
108 Ok(home.join(".algocline").join("scenarios"))
109}
110
111pub(crate) fn resolve_scenario_code(
114 scenario: Option<String>,
115 scenario_file: Option<String>,
116 scenario_name: Option<String>,
117) -> Result<String, String> {
118 match (scenario, scenario_file, scenario_name) {
119 (Some(c), None, None) => Ok(c),
120 (None, Some(path), None) => std::fs::read_to_string(Path::new(&path))
121 .map_err(|e| format!("Failed to read {path}: {e}")),
122 (None, None, Some(name)) => {
123 let dir = scenarios_dir()?;
124 let path = ContainedPath::child(&dir, &format!("{name}.lua"))
125 .map_err(|e| format!("Invalid scenario name: {e}"))?;
126 if !path.as_ref().exists() {
127 return Err(format!(
128 "Scenario '{name}' not found at {}",
129 path.as_ref().display()
130 ));
131 }
132 std::fs::read_to_string(path.as_ref())
133 .map_err(|e| format!("Failed to read scenario '{name}': {e}"))
134 }
135 (None, None, None) => {
136 Err("Provide one of: scenario, scenario_file, or scenario_name.".into())
137 }
138 _ => Err(
139 "Provide only one of: scenario, scenario_file, or scenario_name (not multiple).".into(),
140 ),
141 }
142}
143
144pub(super) const AUTO_INSTALL_SOURCES: &[&str] = &[
147 "https://github.com/ynishi/algocline-bundled-packages",
148 "https://github.com/ynishi/evalframe",
149];
150
151const SYSTEM_PACKAGES: &[&str] = &["evalframe"];
154
155pub(super) fn is_system_package(name: &str) -> bool {
157 SYSTEM_PACKAGES.contains(&name)
158}
159
160pub(super) fn is_package_installed(name: &str) -> bool {
162 packages_dir()
163 .map(|dir| dir.join(name).join("init.lua").exists())
164 .unwrap_or(false)
165}
166
167pub(super) type DirEntryFailures = Vec<String>;
176
177pub(super) fn display_name(path: &Path, file_name: &str) -> String {
179 path.file_stem()
180 .and_then(|s| s.to_str())
181 .map(String::from)
182 .unwrap_or_else(|| file_name.to_string())
183}
184
185pub(super) fn resolve_scenario_source(clone_root: &Path) -> PathBuf {
197 let subdir = clone_root.join("scenarios");
198 if subdir.is_dir() {
199 subdir
200 } else {
201 clone_root.to_path_buf()
202 }
203}
204
205pub(super) fn install_scenarios_from_dir(source: &Path, dest: &Path) -> Result<String, String> {
209 let entries =
210 std::fs::read_dir(source).map_err(|e| format!("Failed to read source dir: {e}"))?;
211
212 let mut installed = Vec::new();
213 let mut skipped = Vec::new();
214 let mut failures: DirEntryFailures = Vec::new();
215
216 for entry_result in entries {
217 let entry = match entry_result {
218 Ok(e) => e,
219 Err(e) => {
220 failures.push(format!("readdir entry: {e}"));
221 continue;
222 }
223 };
224 let path = entry.path();
225 if !path.is_file() {
226 continue;
227 }
228 let ext = path.extension().and_then(|s| s.to_str());
229 if ext != Some("lua") {
230 continue;
231 }
232 let file_name = entry.file_name().to_string_lossy().to_string();
233 let dest_path = match ContainedPath::child(dest, &file_name) {
234 Ok(p) => p,
235 Err(_) => continue,
236 };
237 let name = display_name(&path, &file_name);
238 if dest_path.as_ref().exists() {
239 skipped.push(name);
240 continue;
241 }
242 match std::fs::copy(&path, dest_path.as_ref()) {
243 Ok(_) => installed.push(name),
244 Err(e) => failures.push(format!("{}: {e}", path.display())),
245 }
246 }
247
248 if installed.is_empty() && skipped.is_empty() && failures.is_empty() {
249 return Err("No .lua scenario files found in source.".into());
250 }
251
252 Ok(serde_json::json!({
253 "installed": installed,
254 "skipped": skipped,
255 "failures": failures,
256 })
257 .to_string())
258}