bookyard-core 0.1.0

Core configuration and catalog model for Bookyard.
Documentation
use std::fs;

use bookyard_core::{
    BookConfig, BookEngine, BookyardConfig, build_folder_tree, create_build_plan,
    detect_book_source, insert_book, slugify_book_id,
};

#[test]
fn config_roundtrips_with_defaults() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("bookyard.toml");
    let mut config = BookyardConfig::new("Docs");
    insert_book(
        &mut config,
        BookConfig {
            id: "term-forge".into(),
            title: "TermForge".into(),
            source: "docs/term-forge".into(),
            engine: BookEngine::MdBook,
            folders: vec!["Products/TermForge".into()],
            tags: vec!["rust".into()],
            order: 10,
        },
    )
    .unwrap();

    config.save_to(&path).unwrap();
    let raw = fs::read_to_string(&path).unwrap();
    assert!(raw.contains("engine = \"mdbook\""));
    let loaded = BookyardConfig::load_from(&path).unwrap();

    assert_eq!(loaded.workspace.title, "Docs");
    assert_eq!(loaded.workspace.output, "bookyard");
    assert_eq!(loaded.build.jobs, 4);
    assert_eq!(loaded.books[0].id, "term-forge");
    assert_eq!(loaded.books[0].folders, vec!["Products/TermForge"]);
}

#[test]
fn insert_book_rejects_duplicate_ids() {
    let mut config = BookyardConfig::new("Docs");
    let book = BookConfig::new_mdbook("guide", "Guide", "docs/guide", vec!["Guides"]);

    insert_book(&mut config, book.clone()).unwrap();
    let err = insert_book(&mut config, book).unwrap_err();

    assert!(err.to_string().contains("duplicate book id"));
}

#[test]
fn slugifies_book_ids_from_titles_or_paths() {
    assert_eq!(slugify_book_id("Term Forge"), "term-forge");
    assert_eq!(slugify_book_id("docs/My API Book"), "my-api-book");
    assert_eq!(slugify_book_id("!!!"), "book");
}

#[test]
fn builds_folder_tree_from_logical_paths() {
    let books = vec![
        BookConfig::new_mdbook(
            "term-forge",
            "TermForge",
            "docs/term",
            vec!["Products/TermForge"],
        ),
        BookConfig::new_mdbook(
            "ink-forge",
            "InkForge",
            "docs/ink",
            vec!["Products/InkForge"],
        ),
        BookConfig::new_mdbook(
            "rust-notes",
            "Rust Notes",
            "docs/rust",
            vec!["Knowledge/Rust"],
        ),
    ];

    let tree = build_folder_tree(&books);

    assert_eq!(tree.len(), 2);
    assert_eq!(tree[0].path, "Knowledge");
    assert_eq!(tree[1].path, "Products");
    assert_eq!(tree[1].children.len(), 2);
    assert_eq!(tree[1].children[0].path, "Products/InkForge");
    assert_eq!(tree[1].children[1].path, "Products/TermForge");
}

#[test]
fn detects_mdbook_sources() {
    let dir = tempfile::tempdir().unwrap();
    fs::create_dir_all(dir.path().join("src")).unwrap();
    fs::write(
        dir.path().join("book.toml"),
        "[book]\ntitle = \"Example Book\"\n",
    )
    .unwrap();

    let detected = detect_book_source(dir.path()).unwrap();

    assert_eq!(detected.engine, BookEngine::MdBook);
    assert_eq!(detected.title.as_deref(), Some("Example Book"));
}

#[test]
fn creates_build_plan_for_each_book() {
    let mut config = BookyardConfig::new("Docs");
    insert_book(
        &mut config,
        BookConfig::new_mdbook("guide", "Guide", "docs/guide", vec!["Guides"]),
    )
    .unwrap();

    let plan = create_build_plan(std::path::Path::new("repo"), &config).unwrap();

    assert_eq!(
        plan.output_dir,
        std::path::Path::new("repo").join("bookyard")
    );
    assert_eq!(plan.items.len(), 1);
    assert_eq!(
        plan.items[0].source_dir,
        std::path::Path::new("repo").join("docs/guide")
    );
    assert_eq!(
        plan.items[0].dest_dir,
        std::path::Path::new("repo").join("bookyard/books/guide")
    );
}