use std::path::{Path, PathBuf};
use super::path::ContainedPath;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SearchPathSource {
Env,
Default,
}
impl std::fmt::Display for SearchPathSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Env => write!(f, "ALC_PACKAGES_PATH"),
Self::Default => write!(f, "default"),
}
}
}
#[derive(Clone, Debug)]
pub struct SearchPath {
pub path: PathBuf,
pub source: SearchPathSource,
}
impl SearchPath {
pub fn env(path: PathBuf) -> Self {
Self {
path,
source: SearchPathSource::Env,
}
}
pub fn default_global(path: PathBuf) -> Self {
Self {
path,
source: SearchPathSource::Default,
}
}
}
pub use algocline_core::QueryResponse;
pub(crate) fn resolve_code(
code: Option<String>,
code_file: Option<String>,
) -> Result<String, String> {
match (code, code_file) {
(Some(c), None) => Ok(c),
(None, Some(path)) => std::fs::read_to_string(Path::new(&path))
.map_err(|e| format!("Failed to read {path}: {e}")),
(Some(_), Some(_)) => Err("Provide either `code` or `code_file`, not both.".into()),
(None, None) => Err("Either `code` or `code_file` must be provided.".into()),
}
}
pub(crate) fn make_require_code(name: &str) -> String {
format!(
r#"local pkg = require("{name}")
return pkg.run(ctx)"#
)
}
pub(crate) fn types_stub_path() -> Option<String> {
dirs::home_dir()
.map(|h| h.join(".algocline").join("types").join("alc.d.lua"))
.filter(|p| p.exists())
.map(|p| p.display().to_string())
}
pub(crate) fn packages_dir() -> Result<PathBuf, String> {
let home = dirs::home_dir().ok_or("Cannot determine home directory")?;
Ok(home.join(".algocline").join("packages"))
}
pub(crate) fn scenarios_dir() -> Result<PathBuf, String> {
let home = dirs::home_dir().ok_or("Cannot determine home directory")?;
Ok(home.join(".algocline").join("scenarios"))
}
pub(crate) fn resolve_scenario_code(
scenario: Option<String>,
scenario_file: Option<String>,
scenario_name: Option<String>,
) -> Result<String, String> {
match (scenario, scenario_file, scenario_name) {
(Some(c), None, None) => Ok(c),
(None, Some(path), None) => std::fs::read_to_string(Path::new(&path))
.map_err(|e| format!("Failed to read {path}: {e}")),
(None, None, Some(name)) => {
let dir = scenarios_dir()?;
let path = ContainedPath::child(&dir, &format!("{name}.lua"))
.map_err(|e| format!("Invalid scenario name: {e}"))?;
if !path.as_ref().exists() {
return Err(format!(
"Scenario '{name}' not found at {}",
path.as_ref().display()
));
}
std::fs::read_to_string(path.as_ref())
.map_err(|e| format!("Failed to read scenario '{name}': {e}"))
}
(None, None, None) => {
Err("Provide one of: scenario, scenario_file, or scenario_name.".into())
}
_ => Err(
"Provide only one of: scenario, scenario_file, or scenario_name (not multiple).".into(),
),
}
}
pub(super) const AUTO_INSTALL_SOURCES: &[&str] = &[
"https://github.com/ynishi/algocline-bundled-packages",
"https://github.com/ynishi/evalframe",
];
const SYSTEM_PACKAGES: &[&str] = &["evalframe"];
pub(super) fn is_system_package(name: &str) -> bool {
SYSTEM_PACKAGES.contains(&name)
}
pub(super) fn is_package_installed(name: &str) -> bool {
packages_dir()
.map(|dir| dir.join(name).join("init.lua").exists())
.unwrap_or(false)
}
pub(super) type DirEntryFailures = Vec<String>;
pub(super) fn display_name(path: &Path, file_name: &str) -> String {
path.file_stem()
.and_then(|s| s.to_str())
.map(String::from)
.unwrap_or_else(|| file_name.to_string())
}
pub(super) fn resolve_scenario_source(clone_root: &Path) -> PathBuf {
let subdir = clone_root.join("scenarios");
if subdir.is_dir() {
subdir
} else {
clone_root.to_path_buf()
}
}
pub(super) fn install_scenarios_from_dir(source: &Path, dest: &Path) -> Result<String, String> {
let entries =
std::fs::read_dir(source).map_err(|e| format!("Failed to read source dir: {e}"))?;
let mut installed = Vec::new();
let mut skipped = Vec::new();
let mut failures: DirEntryFailures = Vec::new();
for entry_result in entries {
let entry = match entry_result {
Ok(e) => e,
Err(e) => {
failures.push(format!("readdir entry: {e}"));
continue;
}
};
let path = entry.path();
if !path.is_file() {
continue;
}
let ext = path.extension().and_then(|s| s.to_str());
if ext != Some("lua") {
continue;
}
let file_name = entry.file_name().to_string_lossy().to_string();
let dest_path = match ContainedPath::child(dest, &file_name) {
Ok(p) => p,
Err(_) => continue,
};
let name = display_name(&path, &file_name);
if dest_path.as_ref().exists() {
skipped.push(name);
continue;
}
match std::fs::copy(&path, dest_path.as_ref()) {
Ok(_) => installed.push(name),
Err(e) => failures.push(format!("{}: {e}", path.display())),
}
}
if installed.is_empty() && skipped.is_empty() && failures.is_empty() {
return Err("No .lua scenario files found in source.".into());
}
Ok(serde_json::json!({
"installed": installed,
"skipped": skipped,
"failures": failures,
})
.to_string())
}