use std::ffi::OsStr;
use std::fmt;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ResolverLevel {
CliOverride,
LegacyConfig,
EnvVar,
XdgCache,
NextToBinary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TrustMode {
Trusted,
Custom,
}
impl fmt::Display for ResolverLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
ResolverLevel::CliOverride => "cli_override",
ResolverLevel::LegacyConfig => "legacy_config",
ResolverLevel::EnvVar => "env_var",
ResolverLevel::XdgCache => "xdg_cache",
ResolverLevel::NextToBinary => "next_to_binary",
};
f.write_str(name)
}
}
impl fmt::Display for TrustMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
TrustMode::Trusted => "Trusted",
TrustMode::Custom => "Custom",
};
f.write_str(name)
}
}
impl From<ResolverLevel> for TrustMode {
fn from(level: ResolverLevel) -> Self {
match level {
ResolverLevel::CliOverride | ResolverLevel::LegacyConfig | ResolverLevel::EnvVar => {
TrustMode::Custom
}
ResolverLevel::XdgCache | ResolverLevel::NextToBinary => TrustMode::Trusted,
}
}
}
pub trait DirsLike {
fn cache_dir(&self) -> Option<PathBuf>;
}
pub struct RealDirs;
impl DirsLike for RealDirs {
fn cache_dir(&self) -> Option<PathBuf> {
dirs::cache_dir()
}
}
pub fn resolve_model_dir(
cli_override: Option<&Path>,
legacy: Option<&Path>,
env: Option<&OsStr>,
dirs: &dyn DirsLike,
exe: Option<&Path>,
) -> Option<(PathBuf, ResolverLevel)> {
if let Some(p) = cli_override
&& is_valid_model_dir(p)
{
return Some((p.to_path_buf(), ResolverLevel::CliOverride));
}
if let Some(p) = legacy
&& is_valid_model_dir(p)
{
return Some((p.to_path_buf(), ResolverLevel::LegacyConfig));
}
if let Some(env_value) = env {
let candidate = PathBuf::from(env_value);
if is_valid_model_dir(&candidate) {
return Some((candidate, ResolverLevel::EnvVar));
}
}
if let Some(cache_root) = dirs.cache_dir() {
let candidate = cache_root.join("sqry/models");
if is_valid_model_dir(&candidate) {
return Some((candidate, ResolverLevel::XdgCache));
}
}
if let Some(exe_path) = exe
&& let Some(exe_parent) = exe_path.parent()
{
let candidate = exe_parent.join("models");
if is_valid_model_dir(&candidate) {
return Some((candidate, ResolverLevel::NextToBinary));
}
}
None
}
fn is_valid_model_dir(path: &Path) -> bool {
path.exists() && path.join("manifest.json").exists()
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
struct MockDirs {
root: Option<PathBuf>,
}
impl DirsLike for MockDirs {
fn cache_dir(&self) -> Option<PathBuf> {
self.root.clone()
}
}
fn stage_model_dir(parent: &Path, name: &str) -> PathBuf {
let dir = parent.join(name);
fs::create_dir_all(&dir).expect("create model dir");
fs::write(dir.join("manifest.json"), "{}").expect("write manifest");
dir
}
#[test]
fn cli_override_wins_over_legacy() {
let tmp = TempDir::new().unwrap();
let cli = stage_model_dir(tmp.path(), "cli");
let legacy = stage_model_dir(tmp.path(), "legacy");
let dirs = MockDirs { root: None };
let (resolved, level) =
resolve_model_dir(Some(&cli), Some(&legacy), None, &dirs, None).expect("hit");
assert_eq!(resolved, cli);
assert_eq!(level, ResolverLevel::CliOverride);
assert_eq!(TrustMode::from(level), TrustMode::Custom);
}
#[test]
fn legacy_wins_over_env() {
let tmp = TempDir::new().unwrap();
let legacy = stage_model_dir(tmp.path(), "legacy");
let env_dir = stage_model_dir(tmp.path(), "env");
let dirs = MockDirs { root: None };
let (resolved, level) =
resolve_model_dir(None, Some(&legacy), Some(env_dir.as_os_str()), &dirs, None)
.expect("hit");
assert_eq!(resolved, legacy);
assert_eq!(level, ResolverLevel::LegacyConfig);
assert_eq!(TrustMode::from(level), TrustMode::Custom);
}
#[test]
fn env_wins_over_xdg() {
let tmp = TempDir::new().unwrap();
let env_dir = stage_model_dir(tmp.path(), "env");
let xdg_root = tmp.path().join("xdg-root");
let _xdg_dir = stage_model_dir(&xdg_root, "sqry/models");
let dirs = MockDirs {
root: Some(xdg_root.clone()),
};
let (resolved, level) =
resolve_model_dir(None, None, Some(env_dir.as_os_str()), &dirs, None).expect("hit");
assert_eq!(resolved, env_dir);
assert_eq!(level, ResolverLevel::EnvVar);
assert_eq!(TrustMode::from(level), TrustMode::Custom);
}
#[test]
fn xdg_wins_over_next_to_binary() {
let tmp = TempDir::new().unwrap();
let xdg_root = tmp.path().join("xdg-root");
let xdg_models_dir = xdg_root.join("sqry/models");
fs::create_dir_all(&xdg_models_dir).unwrap();
fs::write(xdg_models_dir.join("manifest.json"), "{}").unwrap();
let exe_dir = tmp.path().join("bin");
fs::create_dir_all(&exe_dir).unwrap();
let exe_path = exe_dir.join("sqry");
fs::write(&exe_path, b"").unwrap();
let exe_models = exe_dir.join("models");
fs::create_dir_all(&exe_models).unwrap();
fs::write(exe_models.join("manifest.json"), "{}").unwrap();
let dirs = MockDirs {
root: Some(xdg_root),
};
let (resolved, level) =
resolve_model_dir(None, None, None, &dirs, Some(&exe_path)).expect("hit");
assert_eq!(resolved, xdg_models_dir);
assert_eq!(level, ResolverLevel::XdgCache);
assert_eq!(TrustMode::from(level), TrustMode::Trusted);
}
#[test]
fn next_to_binary_used_when_others_missing() {
let tmp = TempDir::new().unwrap();
let exe_dir = tmp.path().join("bin");
fs::create_dir_all(&exe_dir).unwrap();
let exe_path = exe_dir.join("sqry");
fs::write(&exe_path, b"").unwrap();
let exe_models = exe_dir.join("models");
fs::create_dir_all(&exe_models).unwrap();
fs::write(exe_models.join("manifest.json"), "{}").unwrap();
let dirs = MockDirs { root: None };
let (resolved, level) =
resolve_model_dir(None, None, None, &dirs, Some(&exe_path)).expect("hit");
assert_eq!(resolved, exe_models);
assert_eq!(level, ResolverLevel::NextToBinary);
assert_eq!(TrustMode::from(level), TrustMode::Trusted);
}
#[test]
fn returns_none_when_all_missing() {
let tmp = TempDir::new().unwrap();
let missing_cli = tmp.path().join("missing-cli");
let missing_legacy = tmp.path().join("missing-legacy");
let missing_env = tmp.path().join("missing-env");
let xdg_root = tmp.path().join("xdg-root-empty");
fs::create_dir_all(&xdg_root).unwrap();
let exe_dir = tmp.path().join("bin-empty");
fs::create_dir_all(&exe_dir).unwrap();
let exe_path = exe_dir.join("sqry");
fs::write(&exe_path, b"").unwrap();
let dirs = MockDirs {
root: Some(xdg_root),
};
let resolved = resolve_model_dir(
Some(&missing_cli),
Some(&missing_legacy),
Some(missing_env.as_os_str()),
&dirs,
Some(&exe_path),
);
assert!(resolved.is_none(), "expected None, got {resolved:?}");
}
#[test]
fn path_without_manifest_is_skipped() {
let tmp = TempDir::new().unwrap();
let cli = tmp.path().join("cli-no-manifest");
fs::create_dir_all(&cli).unwrap();
let legacy = stage_model_dir(tmp.path(), "legacy");
let dirs = MockDirs { root: None };
let (resolved, level) =
resolve_model_dir(Some(&cli), Some(&legacy), None, &dirs, None).expect("hit");
assert_eq!(
resolved, legacy,
"level 1 must be skipped (missing manifest) so level 2 wins"
);
assert_eq!(level, ResolverLevel::LegacyConfig);
}
}