bookyard-core 0.1.0

Core configuration and catalog model for Bookyard.
Documentation
use std::{
    fs,
    path::{Path, PathBuf},
    process::Command,
};

use serde::{Deserialize, Serialize};

use crate::{BookyardConfig, BookyardError, Result, build_catalog, error::io_error};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BuildPlan {
    pub output_dir: PathBuf,
    pub items: Vec<BuildItem>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BuildItem {
    pub id: String,
    pub source_dir: PathBuf,
    pub dest_dir: PathBuf,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BuildOptions {
    pub run_mdbook: bool,
}

impl Default for BuildOptions {
    fn default() -> Self {
        Self { run_mdbook: true }
    }
}

pub fn create_build_plan(root: &Path, config: &BookyardConfig) -> Result<BuildPlan> {
    config.validate()?;
    let output_dir = root.join(&config.workspace.output);
    let items = config
        .books
        .iter()
        .map(|book| BuildItem {
            id: book.id.clone(),
            source_dir: root.join(&book.source),
            dest_dir: output_dir.join("books").join(&book.id),
        })
        .collect();
    Ok(BuildPlan { output_dir, items })
}

pub fn build_shelf(
    root: &Path,
    config: &BookyardConfig,
    options: &BuildOptions,
) -> Result<BuildPlan> {
    let plan = create_build_plan(root, config)?;
    fs::create_dir_all(&plan.output_dir).map_err(|source| io_error(&plan.output_dir, source))?;
    fs::create_dir_all(plan.output_dir.join("books"))
        .map_err(|source| io_error(plan.output_dir.join("books"), source))?;

    for item in &plan.items {
        if options.run_mdbook {
            build_mdbook(item)?;
        } else {
            fs::create_dir_all(&item.dest_dir)
                .map_err(|source| io_error(&item.dest_dir, source))?;
        }
    }

    let catalog = build_catalog(config);
    let catalog_json = serde_json::to_string_pretty(&catalog).expect("catalog is serializable");
    let catalog_path = plan.output_dir.join("catalog.json");
    fs::write(&catalog_path, catalog_json).map_err(|source| io_error(catalog_path, source))?;
    Ok(plan)
}

fn build_mdbook(item: &BuildItem) -> Result<()> {
    let status = Command::new("mdbook")
        .arg("build")
        .arg(&item.source_dir)
        .arg("--dest-dir")
        .arg(&item.dest_dir)
        .status()
        .map_err(|source| io_error("mdbook", source))?;
    if status.success() {
        Ok(())
    } else {
        Err(BookyardError::MdBookBuildFailed {
            id: item.id.clone(),
            status: status.to_string(),
        })
    }
}