use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::OnceLock;
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
static CACHE_FILE_NAME: &str = "flake_edit.json";
fn cache_dir() -> &'static PathBuf {
static CACHE_DIR: OnceLock<PathBuf> = OnceLock::new();
CACHE_DIR.get_or_init(|| {
let project_dir = ProjectDirs::from("com", "a-kenji", "flake-edit").unwrap();
project_dir.data_dir().to_path_buf()
})
}
fn cache_file() -> &'static PathBuf {
static CACHE_FILE: OnceLock<PathBuf> = OnceLock::new();
CACHE_FILE.get_or_init(|| cache_dir().join(CACHE_FILE_NAME))
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
struct CacheEntry {
id: String,
uri: String,
hit: u32,
}
fn entry_key(id: &str, uri: &str) -> String {
format!("{}.{}", id, uri)
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Cache {
entries: HashMap<String, CacheEntry>,
}
impl Cache {
pub fn commit(&self) -> std::io::Result<()> {
let cache_dir = cache_dir();
if !cache_dir.exists() {
std::fs::create_dir_all(cache_dir)?;
}
let cache_file_location = cache_file();
let cache_file = std::fs::File::create(cache_file_location)?;
serde_json::to_writer(cache_file, self)
.map_err(|e| std::io::Error::other(e.to_string()))?;
Ok(())
}
pub fn load() -> Self {
Self::from_path(cache_file())
}
pub fn from_path(path: &std::path::Path) -> Self {
Self::try_from_path(path).unwrap_or_else(|e| {
tracing::warn!("Could not read cache file {:?}: {}", path, e);
Self::default()
})
}
pub fn try_from_path(path: &std::path::Path) -> std::io::Result<Self> {
let file = std::fs::File::open(path)?;
serde_json::from_reader(file).map_err(|e| std::io::Error::other(e.to_string()))
}
pub fn add_entry(&mut self, id: String, uri: String) {
let key = entry_key(&id, &uri);
match self.entries.get_mut(&key) {
Some(entry) => entry.hit += 1,
None => {
let entry = CacheEntry { id, uri, hit: 0 };
self.entries.insert(key, entry);
}
}
}
pub fn list_uris(&self) -> Vec<String> {
let mut entries: Vec<_> = self.entries.values().collect();
entries.sort_by(|a, b| b.hit.cmp(&a.hit));
entries.iter().map(|e| e.uri.clone()).collect()
}
pub fn list_uris_for_id(&self, id: &str) -> Vec<String> {
let mut entries: Vec<_> = self.entries.values().filter(|e| e.id == id).collect();
entries.sort_by(|a, b| b.hit.cmp(&a.hit));
entries.iter().map(|e| e.uri.clone()).collect()
}
pub fn populate_from_inputs<'a>(&mut self, inputs: impl Iterator<Item = (&'a str, &'a str)>) {
for (id, uri) in inputs {
let key = entry_key(id, uri);
self.entries.entry(key).or_insert_with(|| CacheEntry {
id: id.to_string(),
uri: uri.to_string(),
hit: 0,
});
}
}
}
pub fn populate_cache_from_inputs<'a>(
inputs: impl Iterator<Item = (&'a str, &'a str)>,
no_cache: bool,
) {
if no_cache {
return;
}
let mut cache = Cache::load();
let initial_len = cache.entries.len();
cache.populate_from_inputs(inputs);
if cache.entries.len() > initial_len
&& let Err(e) = cache.commit()
{
tracing::debug!("Could not write to cache: {}", e);
}
}
pub fn populate_cache_from_input_map(inputs: &crate::edit::InputMap, no_cache: bool) {
populate_cache_from_inputs(
inputs
.iter()
.map(|(id, input)| (id.as_str(), input.url().trim_matches('"'))),
no_cache,
);
}
pub const DEFAULT_URI_TYPES: [&str; 14] = [
"github:",
"gitlab:",
"sourcehut:",
"git+https://",
"git+ssh://",
"git+http://",
"git+file://",
"git://",
"path:",
"file://",
"tarball:",
"https://",
"http://",
"flake:",
];
#[derive(Debug, Clone, Default)]
pub enum CacheConfig {
#[default]
Default,
None,
Custom(std::path::PathBuf),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_add_and_list() {
let mut cache = Cache::default();
cache.add_entry("nixpkgs".into(), "github:NixOS/nixpkgs".into());
cache.add_entry(
"home-manager".into(),
"github:nix-community/home-manager".into(),
);
cache.add_entry("nixpkgs".into(), "github:NixOS/nixpkgs".into());
let uris = cache.list_uris();
assert_eq!(uris.len(), 2);
assert_eq!(uris[0], "github:NixOS/nixpkgs");
}
#[test]
fn test_list_uris_for_id() {
let mut cache = Cache::default();
cache.add_entry("treefmt-nix".into(), "github:numtide/treefmt-nix".into());
cache.add_entry(
"treefmt-nix".into(),
"path:/home/user/dev/treefmt-nix".into(),
);
cache.add_entry("nixpkgs".into(), "github:NixOS/nixpkgs".into());
cache.add_entry("treefmt-nix".into(), "github:numtide/treefmt-nix".into());
let uris = cache.list_uris_for_id("treefmt-nix");
assert_eq!(uris.len(), 2);
assert_eq!(uris[0], "github:numtide/treefmt-nix");
assert_eq!(uris[1], "path:/home/user/dev/treefmt-nix");
assert!(!uris.contains(&"github:NixOS/nixpkgs".to_string()));
}
#[test]
fn test_list_uris_for_id_empty() {
let cache = Cache::default();
let uris = cache.list_uris_for_id("nonexistent");
assert!(uris.is_empty());
}
#[test]
fn test_populate_from_inputs() {
let mut cache = Cache::default();
cache.add_entry("nixpkgs".into(), "github:NixOS/nixpkgs".into());
cache.add_entry("nixpkgs".into(), "github:NixOS/nixpkgs".into());
let inputs = vec![
("nixpkgs", "github:NixOS/nixpkgs"), ("flake-utils", "github:numtide/flake-utils"), ("home-manager", "github:nix-community/home-manager"), ];
cache.populate_from_inputs(inputs.into_iter());
let uris = cache.list_uris();
assert_eq!(uris.len(), 3);
assert_eq!(uris[0], "github:NixOS/nixpkgs");
assert!(uris.contains(&"github:numtide/flake-utils".to_string()));
assert!(uris.contains(&"github:nix-community/home-manager".to_string()));
}
#[test]
fn test_populate_does_not_increment_hits() {
let mut cache = Cache::default();
cache.add_entry("nixpkgs".into(), "github:NixOS/nixpkgs".into());
cache.add_entry("nixpkgs".into(), "github:NixOS/nixpkgs".into());
let inputs = vec![("nixpkgs", "github:NixOS/nixpkgs")];
cache.populate_from_inputs(inputs.into_iter());
let entry = cache.entries.get("nixpkgs.github:NixOS/nixpkgs").unwrap();
assert_eq!(entry.hit, 1);
}
}