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 alc_shapes_types_stub_path(app_dir: &AppDir) -> Option<String> {
106 let p = app_dir.types_dir().join("alc_shapes.d.lua");
107 if p.exists() {
108 Some(p.display().to_string())
109 } else {
110 None
111 }
112}
113
114pub(crate) fn packages_dir(app_dir: &AppDir) -> PathBuf {
115 app_dir.packages_dir()
116}
117
118pub(crate) fn scenarios_dir(app_dir: &AppDir) -> PathBuf {
119 app_dir.scenarios_dir()
120}
121
122pub(crate) fn resolve_scenario_code(
125 app_dir: &AppDir,
126 scenario: Option<String>,
127 scenario_file: Option<String>,
128 scenario_name: Option<String>,
129) -> Result<String, String> {
130 match (scenario, scenario_file, scenario_name) {
131 (Some(c), None, None) => Ok(c),
132 (None, Some(path), None) => std::fs::read_to_string(Path::new(&path))
133 .map_err(|e| format!("Failed to read {path}: {e}")),
134 (None, None, Some(name)) => {
135 let dir = scenarios_dir(app_dir);
136 let path = ContainedPath::child(&dir, &format!("{name}.lua"))
137 .map_err(|e| format!("Invalid scenario name: {e}"))?;
138 if !path.as_ref().exists() {
139 return Err(format!(
140 "Scenario '{name}' not found at {}",
141 path.as_ref().display()
142 ));
143 }
144 std::fs::read_to_string(path.as_ref())
145 .map_err(|e| format!("Failed to read scenario '{name}': {e}"))
146 }
147 (None, None, None) => {
148 Err("Provide one of: scenario, scenario_file, or scenario_name.".into())
149 }
150 _ => Err(
151 "Provide only one of: scenario, scenario_file, or scenario_name (not multiple).".into(),
152 ),
153 }
154}
155
156pub(super) const AUTO_INSTALL_SOURCES: &[&str] = &[
159 "https://github.com/ynishi/algocline-bundled-packages",
160 "https://github.com/ynishi/evalframe",
161 "https://github.com/ynishi/algocline-swarm-frame",
162];
163
164const SYSTEM_PACKAGES: &[&str] = &["evalframe"];
167
168pub(super) fn is_system_package(name: &str) -> bool {
170 SYSTEM_PACKAGES.contains(&name)
171}
172
173pub(super) fn is_package_installed(app_dir: &AppDir, name: &str) -> bool {
175 packages_dir(app_dir).join(name).join("init.lua").exists()
176}
177
178pub(super) type DirEntryFailures = Vec<String>;
187
188pub(super) fn display_name(path: &Path, file_name: &str) -> String {
190 path.file_stem()
191 .and_then(|s| s.to_str())
192 .map(String::from)
193 .unwrap_or_else(|| file_name.to_string())
194}
195
196pub(super) fn resolve_scenario_source(clone_root: &Path) -> PathBuf {
208 let subdir = clone_root.join("scenarios");
209 if subdir.is_dir() {
210 subdir
211 } else {
212 clone_root.to_path_buf()
213 }
214}
215
216pub(super) fn install_scenarios_from_dir(source: &Path, dest: &Path) -> Result<String, String> {
220 let entries =
221 std::fs::read_dir(source).map_err(|e| format!("Failed to read source dir: {e}"))?;
222
223 let mut installed = Vec::new();
224 let mut skipped = Vec::new();
225 let mut failures: DirEntryFailures = Vec::new();
226
227 for entry_result in entries {
228 let entry = match entry_result {
229 Ok(e) => e,
230 Err(e) => {
231 failures.push(format!("readdir entry: {e}"));
232 continue;
233 }
234 };
235 let path = entry.path();
236 if !path.is_file() {
237 continue;
238 }
239 let ext = path.extension().and_then(|s| s.to_str());
240 if ext != Some("lua") {
241 continue;
242 }
243 let file_name = entry.file_name().to_string_lossy().to_string();
244 let dest_path = match ContainedPath::child(dest, &file_name) {
245 Ok(p) => p,
246 Err(_) => continue,
247 };
248 let name = display_name(&path, &file_name);
249 if dest_path.as_ref().exists() {
250 skipped.push(name);
251 continue;
252 }
253 match std::fs::copy(&path, dest_path.as_ref()) {
254 Ok(_) => installed.push(name),
255 Err(e) => failures.push(format!("{}: {e}", path.display())),
256 }
257 }
258
259 if installed.is_empty() && skipped.is_empty() && failures.is_empty() {
260 return Err("No .lua scenario files found in source.".into());
261 }
262
263 Ok(serde_json::json!({
264 "installed": installed,
265 "skipped": skipped,
266 "failures": failures,
267 })
268 .to_string())
269}