candding 0.1.0

Pure candle embeddings for Rust with no ONNX: dense, sparse, multi-vector and rerank models today, image models on the roadmap
Documentation
//! The cache root as huggingface_hub's `constants.py` computes it: `HF_HUB_CACHE`, the legacy `HUGGINGFACE_HUB_CACHE`,
//! `HF_HOME/hub`, `XDG_CACHE_HOME/huggingface/hub`, then `~/.cache/huggingface/hub`, with `~` and `$VAR` expanded.

use std::ffi::OsStr;
use std::ffi::OsString;
use std::path::Path;
use std::path::PathBuf;
use std::path::is_separator;

/// `var` reads an environment variable and `home` stands for `~`; the default path is expanded twice, as in Python.
pub fn resolve(var: impl Fn(&str) -> Option<OsString>, home: Option<&Path>) -> PathBuf {
  let expand = |value: OsString| vars(&user(value, home), &var);
  let tilde = home.map_or_else(|| PathBuf::from("~"), Path::to_path_buf);
  let xdg = var("XDG_CACHE_HOME").map_or_else(|| tilde.join(".cache"), PathBuf::from);
  let hf_home = expand(var("HF_HOME").unwrap_or_else(|| xdg.join("huggingface").into()));
  let legacy =
    var("HUGGINGFACE_HUB_CACHE").unwrap_or_else(|| Path::new(&hf_home).join("hub").into());
  PathBuf::from(expand(var("HF_HUB_CACHE").unwrap_or(legacy)))
}

/// Python's `expanduser` for `~` alone or before a separator; `~user` stays as written.
fn user(value: OsString, home: Option<&Path>) -> OsString {
  let (Some(text), Some(home)) = (value.to_str(), home.and_then(Path::to_str)) else {
    return value;
  };
  match text.strip_prefix('~') {
    Some(rest) if rest.is_empty() || rest.starts_with(is_separator) => {
      let joined = format!("{}{rest}", home.trim_end_matches(is_separator));
      OsString::from(if joined.is_empty() { "/" } else { &joined })
    }
    _ => value,
  }
}

/// Python's `expandvars`: `$name` with ASCII letters, digits and `_`, or `${name}`; an unset one stays as written,
/// and a value put in is not expanded again.
fn vars(value: &OsStr, var: &impl Fn(&str) -> Option<OsString>) -> OsString {
  let Some(mut rest) = value.to_str() else {
    return value.to_os_string();
  };
  let mut out = OsString::new();
  while let Some(at) = rest.find('$') {
    out.push(&rest[..at]);
    let after = &rest[at + 1..];
    let (name, len) = match after.strip_prefix('{') {
      Some(braced) => braced
        .find('}')
        .map_or(("", 0), |end| (&braced[..end], end + 2)),
      None => {
        let word = |c: char| c.is_ascii_alphanumeric() || c == '_';
        let end = after.find(|c: char| !word(c)).unwrap_or(after.len());
        (&after[..end], end)
      }
    };
    let set = (!name.is_empty() && !name.contains(['=', '\0']))
      .then(|| var(name))
      .flatten();
    match (len, set) {
      (0, _) => out.push("$"),
      (_, Some(found)) => out.push(found),
      (_, None) => out.push(&rest[at..at + 1 + len]),
    }
    rest = &after[len..];
  }
  out.push(rest);
  out
}