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 const LUA_TYPE_AUTODETECT: &str = r#"meta.type = type(pkg.run) == "function" and "runnable" or "library"
173meta.type_source = "auto_detected_" .. meta.type"#;
174
175pub(super) const AUTO_INSTALL_SOURCES: &[&str] = &[
178 "https://github.com/ynishi/algocline-bundled-packages",
179 "https://github.com/ynishi/evalframe",
180 "https://github.com/ynishi/algocline-swarm-frame",
181];
182
183const SYSTEM_PACKAGES: &[&str] = &["evalframe"];
186
187pub(super) fn is_system_package(name: &str) -> bool {
189 SYSTEM_PACKAGES.contains(&name)
190}
191
192pub(super) fn is_package_installed(app_dir: &AppDir, name: &str) -> bool {
194 packages_dir(app_dir).join(name).join("init.lua").exists()
195}
196
197pub(super) type DirEntryFailures = Vec<String>;
206
207pub(super) fn display_name(path: &Path, file_name: &str) -> String {
209 path.file_stem()
210 .and_then(|s| s.to_str())
211 .map(String::from)
212 .unwrap_or_else(|| file_name.to_string())
213}
214
215pub(super) fn resolve_scenario_source(clone_root: &Path) -> PathBuf {
227 let subdir = clone_root.join("scenarios");
228 if subdir.is_dir() {
229 subdir
230 } else {
231 clone_root.to_path_buf()
232 }
233}
234
235pub(super) fn install_scenarios_from_dir(source: &Path, dest: &Path) -> Result<String, String> {
239 let entries =
240 std::fs::read_dir(source).map_err(|e| format!("Failed to read source dir: {e}"))?;
241
242 let mut installed = Vec::new();
243 let mut skipped = Vec::new();
244 let mut failures: DirEntryFailures = Vec::new();
245
246 for entry_result in entries {
247 let entry = match entry_result {
248 Ok(e) => e,
249 Err(e) => {
250 failures.push(format!("readdir entry: {e}"));
251 continue;
252 }
253 };
254 let path = entry.path();
255 if !path.is_file() {
256 continue;
257 }
258 let ext = path.extension().and_then(|s| s.to_str());
259 if ext != Some("lua") {
260 continue;
261 }
262 let file_name = entry.file_name().to_string_lossy().to_string();
263 let dest_path = match ContainedPath::child(dest, &file_name) {
264 Ok(p) => p,
265 Err(_) => continue,
266 };
267 let name = display_name(&path, &file_name);
268 if dest_path.as_ref().exists() {
269 skipped.push(name);
270 continue;
271 }
272 match std::fs::copy(&path, dest_path.as_ref()) {
273 Ok(_) => installed.push(name),
274 Err(e) => failures.push(format!("{}: {e}", path.display())),
275 }
276 }
277
278 if installed.is_empty() && skipped.is_empty() && failures.is_empty() {
279 return Err("No .lua scenario files found in source.".into());
280 }
281
282 Ok(serde_json::json!({
283 "installed": installed,
284 "skipped": skipped,
285 "failures": failures,
286 })
287 .to_string())
288}