rusty-man 0.2.0

Command-line viewer for rustdoc documentation
// SPDX-FileCopyrightText: 2020 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT

//! Handles documentation sources, for example local directories.

use std::fs;
use std::path;

use anyhow::anyhow;

use crate::doc;
use crate::index;

/// Documentation source, for example a local directory.
pub trait Source {
    fn find_crate(&self, name: &str) -> Option<doc::Crate>;
    fn load_index(&self) -> anyhow::Result<Option<index::Index>>;
}

/// Local directory containing documentation data.
///
/// The directory must contain documentation for one or more crates in subdirectories.  Suitable
/// directories are the `doc` directory generated by `cargo doc` or the root directory of the Rust
/// documentation.
#[derive(Clone, Debug, PartialEq)]
pub struct DirSource {
    path: path::PathBuf,
}

impl DirSource {
    fn new(path: path::PathBuf) -> Self {
        log::info!("Created directory source at '{}'", path.display());
        Self { path }
    }
}

impl Source for DirSource {
    fn find_crate(&self, name: &str) -> Option<doc::Crate> {
        log::info!(
            "Searching crate '{}' in dir source '{}'",
            name,
            self.path.display()
        );
        let crate_path = self.path.join(name.replace('-', "_"));
        if crate_path.join("all.html").is_file() {
            log::info!("Found crate '{}': '{}'", name, crate_path.display());
            Some(doc::Crate::new(name.to_owned(), crate_path))
        } else {
            log::info!("Did not find crate '{}' in '{}'", name, self.path.display());
            None
        }
    }

    fn load_index(&self) -> anyhow::Result<Option<index::Index>> {
        log::info!("Searching search index for '{}'", self.path.display());
        // use the first file that matches the pattern search-index*.js
        for entry in fs::read_dir(&self.path)? {
            let entry = entry?;
            if entry.file_type()?.is_file() {
                if let Some(s) = entry.file_name().to_str() {
                    if s.starts_with("search-index") && s.ends_with(".js") {
                        log::info!("Found search index '{}'", &entry.path().display());
                        return index::Index::load(&entry.path());
                    }
                }
            }
        }
        log::info!("Could not find search index for '{}'", self.path.display());
        Ok(None)
    }
}

pub fn get_source<P: AsRef<path::Path>>(path: P) -> anyhow::Result<Box<dyn Source>> {
    if path.as_ref().is_dir() {
        Ok(Box::new(DirSource::new(path.as_ref().to_path_buf())))
    } else {
        Err(anyhow!(
            "This source is not supported: {}",
            path.as_ref().display()
        ))
    }
}

#[cfg(test)]
mod tests {
    use std::path;

    use super::Source;

    #[test]
    fn dir_source_find_crate() {
        fn assert_crate(source: &dyn super::Source, path: &path::PathBuf, name: &str) {
            assert_eq!(
                source.find_crate(name),
                Some(super::doc::Crate::new(name.to_owned(), path.join(name)))
            );
        }

        let doc = crate::tests::ensure_docs();

        let source = super::DirSource::new(doc.clone());

        assert_crate(&source, &doc, "clap");
        assert_crate(&source, &doc, "lazy_static");
        assert_eq!(source.find_crate("lazystatic"), None);
    }
}