use crate::{
backend, documentation::Documentation, Backend, Callbacks, ConfigFile, Error,
Resolver, Result,
};
use std::{fs, path::PathBuf};
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Package {
Name(String),
Root(PathBuf),
}
#[derive(Debug)]
pub struct Builder {
resolver: Resolver,
backends: Vec<(Backend, Box<dyn Callbacks>)>,
user_config: Option<PathBuf>,
package: Option<Package>,
markdown_options: pulldown_cmark::Options,
}
impl Builder {
pub fn new() -> Self {
Self {
resolver: Resolver::default(),
backends: Vec::new(),
user_config: None,
package: None,
markdown_options: pulldown_cmark::Options::empty(),
}
}
pub fn user_config(mut self, path: PathBuf) -> Self {
self.user_config = Some(path);
self
}
pub fn package(mut self, package: Package) -> Self {
self.package = Some(package);
self
}
pub fn add_backend(mut self, backend: Backend) -> Self {
let callbacks: Box<dyn Callbacks> = match &backend {
Backend::Markdown { .. } => Box::new(backend::MarkdownCallbacks::default()),
Backend::Html { .. } => Box::new(backend::HtmlCallbacks::default()),
Backend::Gut { .. } => Box::new(backend::GutCallbacks::default()),
};
self.backends.push((backend, callbacks));
self
}
pub fn add_backend_with_callbacks(
mut self,
backend: Backend,
callbacks: Box<dyn Callbacks>,
) -> Self {
self.backends.push((backend, callbacks));
self
}
pub fn build(mut self) -> Result<()> {
if let Some(path) = self.user_config.take() {
self.load_user_config(path)?
}
let documentation = self.build_documentation()?;
for (backend, callbacks) in self.backends {
log::debug!("generating documentation for backend {:?}", backend);
let extension = callbacks.extension();
let mut generator = backend::Generator::new(
&self.resolver,
&documentation,
callbacks,
self.markdown_options,
);
let files = generator.generate_files();
let root_file = generator.generate_root_file(extension);
let backend_is_gut = matches!(&backend, Backend::Gut { .. });
match &backend {
Backend::Markdown { output_dir }
| Backend::Html { output_dir }
| Backend::Gut { output_dir } => {
if let Err(err) = fs::create_dir_all(&output_dir) {
return Err(Error::Io(output_dir.clone(), err));
}
if !backend_is_gut {
let out_file = output_dir.join("index").with_extension(extension);
if let Err(err) = fs::write(&out_file, root_file) {
return Err(Error::Io(out_file, err));
}
}
for (name, content) in files {
let out_file = output_dir.join(name).with_extension(extension);
if let Err(err) = fs::write(&out_file, content) {
return Err(Error::Io(out_file, err));
}
}
}
}
}
Ok(())
}
fn load_user_config(&mut self, path: PathBuf) -> Result<()> {
log::debug!("loading user config at {:?}", path);
let user_config = ConfigFile::read_from(&match fs::read_to_string(&path) {
Ok(config) => config,
Err(err) => return Err(Error::Io(path, err)),
})?;
self.markdown_options = user_config
.markdown_options()
.unwrap_or(pulldown_cmark::Options::empty());
self.resolver.apply_user_config(user_config);
Ok(())
}
fn build_documentation(&mut self) -> Result<Documentation> {
log::debug!("building documentation");
let root_file = match self.package.take() {
Some(Package::Root(root_file)) => root_file,
Some(Package::Name(name)) => find_root_file(Some(&name))?,
None => find_root_file(None)?,
};
let mut documentation = Documentation::from_root_file(root_file)?;
self.resolver.rename_classes(&mut documentation);
Ok(documentation)
}
}
fn find_root_file(package_name: Option<&str>) -> Result<PathBuf> {
let metadata = cargo_metadata::MetadataCommand::new().exec()?;
let mut root_files = Vec::new();
for package in metadata.packages {
if metadata.workspace_members.contains(&package.id) {
if let Some(target) = package
.targets
.into_iter()
.find(|target| target.kind.iter().any(|kind| kind == "cdylib"))
{
root_files.push((package.name, target.src_path))
}
}
}
if let Some(package_name) = package_name {
match root_files
.into_iter()
.find(|(name, _)| name == package_name)
{
Some((_, root_file)) => Ok(root_file),
None => Err(Error::NoMatchingCrate(package_name.to_string())),
}
} else {
if root_files.len() > 1 {
return Err(Error::MultipleCandidateCrate(
root_files.into_iter().map(|(name, _)| name).collect(),
));
}
if let Some((_, root_file)) = root_files.pop() {
Ok(root_file)
} else {
Err(Error::NoCandidateCrate)
}
}
}