term-rustdoc 0.2.0

A TUI for Rust docs.
Documentation
use super::{
    features::Features,
    pkg_key::PkgKey,
    util::{decode, encode},
    DocMeta, PkgWithFeatures,
};
use crate::{
    database::util,
    err,
    local_registry::{PkgInfo, PkgNameVersion},
    Result, WrapErr,
};
use serde::{Deserialize, Serialize};
use std::{
    fs,
    path::{Path, PathBuf},
    time::{Instant, SystemTime},
};
use term_rustdoc::tree::CrateDoc;

#[derive(Debug, Deserialize, Serialize)]
pub struct CachedDocInfo {
    pub pkg: PkgKey,
    /// file name for doc db (with parent path included); usually is `self.pkg-self.ver.db`.
    db_file: PathBuf,
    meta: DocMeta,
}

impl CachedDocInfo {
    pub fn new_with_default_feature(name_ver: PkgNameVersion, mut db_dir: PathBuf) -> Self {
        let fname = name_ver.doc_db_file_name();
        db_dir.push(&*fname);
        let pkg = PkgKey::new_with_default_feature(name_ver);
        CachedDocInfo {
            pkg,
            db_file: db_dir,
            meta: DocMeta::new(),
        }
    }

    pub fn new(name_ver: PkgNameVersion, features: Features, mut db_dir: PathBuf) -> Self {
        let fname = name_ver.doc_db_file_name();
        db_dir.push(&*fname);
        let pkg = PkgKey::new(name_ver, features);
        CachedDocInfo {
            pkg,
            db_file: db_dir,
            meta: DocMeta::new(),
        }
    }

    pub fn load_doc(&self) -> Result<CrateDoc> {
        let now = Instant::now();
        debug!(?self.pkg, "Start to load");
        let db = redb::Database::open(&self.db_file)?;
        let bytes = read_from_doc_db::<PkgKey, Vec<u8>>(&db, "host-parsed", &self.pkg)?;
        let doc = util::decode_with_xz(&bytes)?;
        info!(?self.pkg, "Loaded in {:.2}s", now.elapsed().as_secs_f32());
        Ok(doc)
    }

    /// Get PkgInfo from db and use PkgWithFeatures to recompile the doc.
    ///
    /// FIXME: PkgInfo may contain invalid data because registry caches can be cleaned up.
    /// We should put forward a better way to determin if the broken doc can be recompiled.
    pub fn load_pkg_info_features(&self) -> Result<PkgWithFeatures> {
        let db = redb::Database::create(&self.db_file)?;

        // read PkgInfo from db
        let bytes = read_from_doc_db::<PkgKey, Vec<u8>>(&db, "host-pkg-info", &self.pkg)?;
        let info = decode::<PkgInfo>(&bytes)?;
        info!(?self.pkg, "PkgInfo is succeefully read from db file `{}`", self.db_file.display());

        Ok(PkgWithFeatures {
            features: self.pkg.features().clone(),
            info,
        })
    }

    pub fn save_doc(&self, json_path: &Path, pkg_info: PkgInfo) -> Result<()> {
        let file = fs::File::open(json_path).wrap_err_with(|| {
            format!(
                "Failed to open compiled json doc under {}",
                json_path.display()
            )
        })?;
        // parse json doc
        let doc = CrateDoc::new(serde_json::from_reader(file)?);

        let db = redb::Database::create(&self.db_file)?;

        // write PkgInfo into db
        self.write_to_db(&db, "host-pkg-info", encode(&pkg_info)?)?;
        info!(?self.pkg, "PkgInfo is succeefully saved");

        // write raw json string into db
        let json_bytes = fs::read(json_path)?;
        let compressed = util::xz_encode_on_bytes(&json_bytes)?;
        self.write_to_db(&db, "host-json", compressed)?;
        info!(?self.pkg, "raw json is succeefully saved");

        // write parsed doc into db
        self.write_to_db(&db, "host-parsed", util::encode_with_xz(doc)?)?;
        info!(?self.pkg, "parsed data is succeefully saved");

        // write to index.db
        self.write_self_to_db()?;
        Ok(())
    }

    /// NOTE: written to `pkg-version.db`, and the tables are
    /// * `host-pkg-info`: name, version, local pkg dir etc
    /// * `host-json`: raw json file generated by rustdoc
    /// * `host-parsed`: directly used in term-rustdoc to save json parsing
    fn write_to_db(&self, db: &redb::Database, name: &str, value: Vec<u8>) -> Result<()> {
        write_to_db::<PkgKey, Vec<u8>>(db, name, &self.pkg, &value)?;
        Ok(())
    }

    /// NOTE: CachedDocInfo is written to `index.db`, not its `pkg-version.db`
    fn write_self_to_db(&self) -> Result<()> {
        let db = redb::Database::create(self.db_file.with_file_name("index.db"))?;
        write_to_db::<PkgKey, CachedDocInfo>(&db, "CachedDocInfo", &self.pkg, self)?;
        Ok(())
    }

    pub(super) fn meta_mut(&mut self) -> &mut DocMeta {
        &mut self.meta
    }

    pub fn started_time(&self) -> SystemTime {
        self.meta.started_time()
    }
}

fn write_to_db<K, V>(
    db: &redb::Database,
    name: &str,
    key: &K::SelfType<'_>,
    value: &V::SelfType<'_>,
) -> Result<()>
where
    K: 'static + redb::RedbKey,
    V: 'static + redb::RedbValue,
{
    let table = redb::TableDefinition::<K, V>::new(name);
    let write_txn = db.begin_write()?;
    {
        let mut table = write_txn.open_table(table)?;
        table.insert(key, value)?;
    }
    write_txn.commit()?;
    Ok(())
}

fn read_from_doc_db<K, V>(db: &redb::Database, name: &str, key: &K) -> Result<V>
where
    K: 'static + for<'a> redb::RedbKey<SelfType<'a> = K> + std::fmt::Debug,
    V: 'static + for<'a> redb::RedbValue<SelfType<'a> = V>,
{
    use redb::ReadableTable;
    let table = redb::TableDefinition::<K, V>::new(name);
    let read_txn = db.begin_read()?;
    let value = read_txn
        .open_table(table)?
        .get(key)?
        .ok_or_else(|| err!("Failed to get the key `{key:?}` from table `{name}`"))?
        .value();
    Ok(value)
}

impl redb::RedbValue for CachedDocInfo {
    type SelfType<'a> = CachedDocInfo;

    type AsBytes<'a> = Vec<u8>;

    fn fixed_width() -> Option<usize> {
        None
    }

    fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a>
    where
        Self: 'a,
    {
        decode(data).unwrap()
    }

    fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a>
    where
        Self: 'a,
        Self: 'b,
    {
        encode(value).unwrap()
    }

    fn type_name() -> redb::TypeName {
        redb::TypeName::new("CachedDocInfo")
    }
}