use std::{
borrow::Cow,
collections::{BTreeSet, HashMap},
iter,
};
use anyhow::{Context, Result, bail};
use lsp_types::Url;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use sha2::{Digest, Sha256};
use tap::{Pipe, Tap};
use tokio::task::JoinSet;
use tracing::{debug, instrument};
use mdbookkit::url::{ExpectUrl, UrlToPath};
use crate::{
env::Environment,
link::{ItemLinks, LinkState},
page::{PageKey, Pages},
resolver::Resolver,
};
pub trait Cache: DeserializeOwned + Serialize {
type Validated: Resolver;
async fn reuse(self, env: &Environment) -> Result<Self::Validated>;
async fn build<K>(env: &Environment, content: &Pages<'_, K>) -> Result<Self>
where
K: PageKey;
#[instrument("cache_load", skip_all)]
async fn load(env: &Environment) -> Result<Self::Validated> {
env.load_temp::<Self, _>("cache.json")?.reuse(env).await
}
#[instrument("cache_save", skip_all)]
async fn save<K>(env: &Environment, content: &Pages<'_, K>) -> Result<()>
where
K: PageKey,
{
let this = Self::build(env, content).await?;
env.save_temp::<Self, _>("cache.json", &this)
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum FileCache {
V1(FileCacheV1),
}
impl Cache for FileCache {
type Validated = HashMap<String, ItemLinks>;
async fn reuse(self, env: &Environment) -> Result<Self::Validated> {
match self {
Self::V1(cache) => Ok(cache.reuse(env).await?),
}
}
async fn build<K>(env: &Environment, content: &Pages<'_, K>) -> Result<Self>
where
K: PageKey,
{
Ok(Self::V1(FileCacheV1::build(env, content).await?))
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct FileCacheV1 {
hash: String,
urls: Vec<(String, ItemLinks)>,
}
impl Cache for FileCacheV1 {
type Validated = HashMap<String, ItemLinks>;
async fn reuse(self, env: &Environment) -> Result<Self::Validated> {
let deps = self
.urls
.iter()
.flat_map(|(_, links)| links.deps())
.map(Cow::Borrowed);
let hash = Self::hash(env, deps).await;
if hash != self.hash {
bail!("Checksum mismatch, expected {}, actual {hash}", self.hash)
}
Ok(self.urls.into_iter().collect())
}
async fn build<K>(env: &Environment, content: &Pages<'_, K>) -> Result<Self>
where
K: PageKey,
{
let urls = content
.iter()
.deduped(|link| match link.state() {
LinkState::Resolved(links) => Some(links),
_ => None,
})
.into_iter()
.filter_map(|(k, v)| Some((k.to_string(), v?.clone())))
.collect::<Vec<_>>();
let deps = urls
.iter()
.flat_map(|(_, links)| links.deps())
.map(Cow::Borrowed);
let hash = Self::hash(env, deps).await;
Ok(Self { hash, urls })
}
}
impl FileCacheV1 {
#[instrument(level = "debug", skip_all, ret)]
async fn hash<'a, D>(env: &'a Environment, deps: D) -> String
where
D: Iterator<Item = Cow<'a, Url>>,
{
[
Cow::Owned(env.source_dir.join("Cargo.toml").expect_url()),
Cow::Owned(env.crate_dir.join("Cargo.toml").expect_url()),
Cow::Borrowed(&env.entrypoint),
]
.into_iter()
.chain(iter::once(Cow::Borrowed(&env.entrypoint)))
.chain(deps)
.collect::<BTreeSet<_>>()
.into_iter()
.filter_map(|dep| {
if env.source_dir.make_relative(&dep)?.starts_with("../") {
None
} else {
Some(dep.into_owned())
}
})
.map(read_dep)
.collect::<JoinSet<_>>()
.join_all()
.await
.into_iter()
.filter_map(|result| {
result.context("Failed to read cache dependency").ok()
})
.collect::<Vec<_>>()
.tap_mut(|deps| deps.sort_by(|(k1, _), (k2, _)| k1.cmp(k2))) .into_iter()
.fold(Sha256::new(), |mut hash, (name, src)| {
debug!("hashing {name}");
hash.update(src);
hash
})
.pipe(|hash| hash.finalize().digest())
}
}
async fn read_dep(url: Url) -> Result<(String, String)> {
let content = tokio::fs::read_to_string(&url.to_path()?).await?;
Ok((url.to_string(), content))
}
trait HexDigest {
fn digest(&self) -> String;
}
impl HexDigest for sha2::digest::Output<Sha256> {
fn digest(&self) -> String {
format!("{:064x}", self)
}
}