algocline_app/service/
resolve.rs1use std::path::{Path, PathBuf};
2
3use algocline_core::AppDir;
4
5use super::path::ContainedPath;
6
7#[derive(Clone, Debug, PartialEq, Eq)]
11pub enum SearchPathSource {
12 Env,
14 Default,
16}
17
18impl std::fmt::Display for SearchPathSource {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 match self {
21 Self::Env => write!(f, "ALC_PACKAGES_PATH"),
22 Self::Default => write!(f, "default"),
23 }
24 }
25}
26
27#[derive(Clone, Debug)]
29pub struct SearchPath {
30 pub path: PathBuf,
31 pub source: SearchPathSource,
32}
33
34impl SearchPath {
35 pub fn env(path: PathBuf) -> Self {
36 Self {
37 path,
38 source: SearchPathSource::Env,
39 }
40 }
41
42 pub fn default_global(path: PathBuf) -> Self {
43 Self {
44 path,
45 source: SearchPathSource::Default,
46 }
47 }
48}
49
50pub use algocline_core::QueryResponse;
52
53pub(crate) fn resolve_code(
56 code: Option<String>,
57 code_file: Option<String>,
58) -> Result<String, String> {
59 match (code, code_file) {
60 (Some(c), None) => Ok(c),
61 (None, Some(path)) => std::fs::read_to_string(Path::new(&path))
62 .map_err(|e| format!("Failed to read {path}: {e}")),
63 (Some(_), Some(_)) => Err("Provide either `code` or `code_file`, not both.".into()),
64 (None, None) => Err("Either `code` or `code_file` must be provided.".into()),
65 }
66}
67
68pub(crate) fn make_require_code(name: &str) -> String {
90 format!(
91 r#"local pkg = require("{name}")
92return pkg.run(ctx)"#
93 )
94}
95
96pub(crate) fn types_stub_path(app_dir: &AppDir) -> Option<String> {
97 let p = app_dir.types_dir().join("alc.d.lua");
98 if p.exists() {
99 Some(p.display().to_string())
100 } else {
101 None
102 }
103}
104
105pub(crate) fn packages_dir(app_dir: &AppDir) -> PathBuf {
106 app_dir.packages_dir()
107}
108
109pub(crate) fn scenarios_dir(app_dir: &AppDir) -> PathBuf {
110 app_dir.scenarios_dir()
111}
112
113pub(crate) fn resolve_scenario_code(
116 app_dir: &AppDir,
117 scenario: Option<String>,
118 scenario_file: Option<String>,
119 scenario_name: Option<String>,
120) -> Result<String, String> {
121 match (scenario, scenario_file, scenario_name) {
122 (Some(c), None, None) => Ok(c),
123 (None, Some(path), None) => std::fs::read_to_string(Path::new(&path))
124 .map_err(|e| format!("Failed to read {path}: {e}")),
125 (None, None, Some(name)) => {
126 let dir = scenarios_dir(app_dir);
127 let path = ContainedPath::child(&dir, &format!("{name}.lua"))
128 .map_err(|e| format!("Invalid scenario name: {e}"))?;
129 if !path.as_ref().exists() {
130 return Err(format!(
131 "Scenario '{name}' not found at {}",
132 path.as_ref().display()
133 ));
134 }
135 std::fs::read_to_string(path.as_ref())
136 .map_err(|e| format!("Failed to read scenario '{name}': {e}"))
137 }
138 (None, None, None) => {
139 Err("Provide one of: scenario, scenario_file, or scenario_name.".into())
140 }
141 _ => Err(
142 "Provide only one of: scenario, scenario_file, or scenario_name (not multiple).".into(),
143 ),
144 }
145}
146
147pub(super) const AUTO_INSTALL_SOURCES: &[&str] = &[
150 "https://github.com/ynishi/algocline-bundled-packages",
151 "https://github.com/ynishi/evalframe",
152];
153
154const SYSTEM_PACKAGES: &[&str] = &["evalframe"];
157
158pub(super) fn is_system_package(name: &str) -> bool {
160 SYSTEM_PACKAGES.contains(&name)
161}
162
163pub(super) fn is_package_installed(app_dir: &AppDir, name: &str) -> bool {
165 packages_dir(app_dir).join(name).join("init.lua").exists()
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}