use std::path::{Path, PathBuf};
use std::rc::Rc;
use gix::ObjectId;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use crate::HashMap;
use crate::parse::FileItems;
const SCHEMA: u32 = 17;
pub(crate) type Parsed = Rc<Result<FileItems, String>>;
pub(crate) struct FileCache {
files: Store,
expansions: Store,
}
impl FileCache {
pub(crate) fn load(dir: &Path) -> Self {
Self {
files: Store::load(dir, "files"),
expansions: Store::load(dir, "expansions"),
}
}
pub(crate) fn get_or_parse<E>(
&mut self,
id: ObjectId,
read: impl FnOnce() -> Result<Vec<u8>, E>,
) -> Result<Parsed, E> {
if let Some(parsed) = self.files.get(id) {
return Ok(Rc::new(parsed));
}
let bytes = read()?;
let parsed = match std::str::from_utf8(&bytes) {
Ok(source) => crate::parse::parse(source),
Err(error) => Err(format!("not UTF-8: {error}")),
};
self.files.insert(id, &parsed);
Ok(Rc::new(parsed))
}
pub(crate) fn expansion(&self, key: ObjectId) -> Option<Expanded> {
self.expansions.get(key)
}
pub(crate) fn store_expansion(&mut self, key: ObjectId, expanded: &Expanded) {
self.expansions.insert(key, expanded);
}
pub(crate) fn save(&self) {
self.files.save();
self.expansions.save();
}
}
#[derive(Serialize, Deserialize)]
#[serde(transparent)]
pub(crate) struct Expanded(Option<FileItems>);
impl Expanded {
pub(crate) fn items(self) -> Option<FileItems> {
self.0
}
}
impl From<Option<FileItems>> for Expanded {
fn from(items: Option<FileItems>) -> Self {
Self(items)
}
}
struct Store {
file: PathBuf,
entries: HashMap<ObjectId, Box<RawValue>>,
dirty: bool,
}
impl Store {
fn load(dir: &Path, kind: &str) -> Self {
let file = dir.join(format!(
"{kind}-{}-{SCHEMA}.json",
env!("CARGO_PKG_VERSION")
));
let stored: std::collections::HashMap<String, Box<RawValue>> = std::fs::read(&file)
.ok()
.and_then(|bytes| serde_json::from_slice(&bytes).ok())
.unwrap_or_default();
let entries = stored
.into_iter()
.filter_map(|(hex, raw)| Some((ObjectId::from_hex(hex.as_bytes()).ok()?, raw)))
.collect();
Self {
file,
entries,
dirty: false,
}
}
fn get<T: DeserializeOwned>(&self, id: ObjectId) -> Option<T> {
serde_json::from_str(self.entries.get(&id)?.get()).ok()
}
fn insert<T: Serialize>(&mut self, id: ObjectId, value: &T) {
if let Ok(raw) = serde_json::value::to_raw_value(value) {
self.entries.insert(id, raw);
self.dirty = true;
}
}
fn save(&self) {
if !self.dirty {
return;
}
let stored: std::collections::HashMap<String, &RawValue> = self
.entries
.iter()
.map(|(id, raw)| (id.to_string(), &**raw))
.collect();
let Ok(bytes) = serde_json::to_vec(&stored) else {
return;
};
let Some(dir) = self.file.parent() else {
return;
};
if std::fs::create_dir_all(dir).is_err() {
return;
}
let temporary = self.file.with_extension("json.tmp");
if std::fs::write(&temporary, bytes).is_ok() {
let _ = std::fs::rename(&temporary, &self.file);
}
}
}
#[cfg(test)]
mod tests {
use super::FileCache;
use crate::ItemKind;
#[test]
fn a_persisted_parse_does_not_require_the_blob_reader() {
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("lib.rs");
let bytes = b"pub struct Remembered;";
std::fs::write(&source, bytes).unwrap();
let id =
gix::objs::compute_hash(gix::hash::Kind::Sha1, gix::objs::Kind::Blob, bytes).unwrap();
let stored = dir.path().join("cache");
let mut cache = FileCache::load(&stored);
cache.get_or_parse(id, || std::fs::read(&source)).unwrap();
cache.save();
drop(cache);
std::fs::remove_file(&source).unwrap();
let mut cache = FileCache::load(&stored);
let parsed = cache.get_or_parse(id, || std::fs::read(&source)).unwrap();
let items = parsed.as_ref().as_ref().unwrap();
let [item] = items.items.as_slice() else {
panic!("expected the cached struct");
};
assert_eq!(item.path, ["Remembered"]);
assert_eq!(item.kind, ItemKind::Struct);
assert!(item.public);
}
}