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];
162
163const SYSTEM_PACKAGES: &[&str] = &["evalframe"];
166
167pub(super) fn is_system_package(name: &str) -> bool {
169 SYSTEM_PACKAGES.contains(&name)
170}
171
172pub(super) fn is_package_installed(app_dir: &AppDir, name: &str) -> bool {
174 packages_dir(app_dir).join(name).join("init.lua").exists()
175}
176
177pub(super) type DirEntryFailures = Vec<String>;
186
187pub(super) fn display_name(path: &Path, file_name: &str) -> String {
189 path.file_stem()
190 .and_then(|s| s.to_str())
191 .map(String::from)
192 .unwrap_or_else(|| file_name.to_string())
193}
194
195pub(super) fn resolve_scenario_source(clone_root: &Path) -> PathBuf {
207 let subdir = clone_root.join("scenarios");
208 if subdir.is_dir() {
209 subdir
210 } else {
211 clone_root.to_path_buf()
212 }
213}
214
215pub(super) fn install_scenarios_from_dir(source: &Path, dest: &Path) -> Result<String, String> {
219 let entries =
220 std::fs::read_dir(source).map_err(|e| format!("Failed to read source dir: {e}"))?;
221
222 let mut installed = Vec::new();
223 let mut skipped = Vec::new();
224 let mut failures: DirEntryFailures = Vec::new();
225
226 for entry_result in entries {
227 let entry = match entry_result {
228 Ok(e) => e,
229 Err(e) => {
230 failures.push(format!("readdir entry: {e}"));
231 continue;
232 }
233 };
234 let path = entry.path();
235 if !path.is_file() {
236 continue;
237 }
238 let ext = path.extension().and_then(|s| s.to_str());
239 if ext != Some("lua") {
240 continue;
241 }
242 let file_name = entry.file_name().to_string_lossy().to_string();
243 let dest_path = match ContainedPath::child(dest, &file_name) {
244 Ok(p) => p,
245 Err(_) => continue,
246 };
247 let name = display_name(&path, &file_name);
248 if dest_path.as_ref().exists() {
249 skipped.push(name);
250 continue;
251 }
252 match std::fs::copy(&path, dest_path.as_ref()) {
253 Ok(_) => installed.push(name),
254 Err(e) => failures.push(format!("{}: {e}", path.display())),
255 }
256 }
257
258 if installed.is_empty() && skipped.is_empty() && failures.is_empty() {
259 return Err("No .lua scenario files found in source.".into());
260 }
261
262 Ok(serde_json::json!({
263 "installed": installed,
264 "skipped": skipped,
265 "failures": failures,
266 })
267 .to_string())
268}