provenance-cli 0.2.3

Command-line interface for Provenance requirements traceability
//! Renders the wiki page model to semantic HTML.
//!
//! Pages follow the design mockup: type-colored accent bar, top chrome with
//! wordmark and breadcrumb, display-serif title with type and status badges,
//! a 780px body next to a 220px scholarly margin, and a full-width Discussion
//! band. Everything is a pure `model -> String` function; the CSS and
//! theme switcher are vendored in [`crate::wiki::theme`].

mod chrome;
mod citations;
mod discussion;
mod field_notes;
mod fragments;
mod html;
mod labels;
mod pages;

use crate::wiki::model::WikiCorpus;
use crate::wiki::routes::WikiRoute;

pub use pages::{
    render_decisions, render_domains, render_index, render_not_found, render_requirement,
    render_resolution, render_rule, render_search, render_source, render_unfinished,
};

/// One rendered page: its canonical route, title, and full HTML document.
#[derive(Debug, Clone)]
pub struct RenderedPage {
    pub route: String,
    pub title: String,
    pub html: String,
}

/// Renders every page in the corpus, index first, then requirements,
/// resolutions, rules, and sources in record order.
pub fn render_corpus(corpus: &WikiCorpus) -> Vec<RenderedPage> {
    let scope = corpus.scope.as_str();
    let mut pages = vec![
        rendered(
            WikiRoute::Index,
            &corpus.index.title,
            render_index(scope, &corpus.index),
        ),
        rendered(
            WikiRoute::Domains,
            &corpus.domains.title,
            render_domains(scope, &corpus.domains),
        ),
        rendered(
            WikiRoute::Search,
            &corpus.search.title,
            render_search(scope, &corpus.search),
        ),
        rendered(
            WikiRoute::Decisions,
            &corpus.decisions.title,
            render_decisions(scope, &corpus.decisions),
        ),
        rendered(
            WikiRoute::Unfinished,
            &corpus.unfinished.title,
            render_unfinished(scope, &corpus.unfinished),
        ),
    ];
    for page in &corpus.requirements {
        pages.push(rendered(
            WikiRoute::Record(&page.id),
            &page.title,
            render_requirement(scope, page),
        ));
    }
    for page in &corpus.resolutions {
        pages.push(rendered(
            WikiRoute::Record(&page.id),
            &page.title,
            render_resolution(scope, page),
        ));
    }
    for page in &corpus.rules {
        pages.push(rendered(
            WikiRoute::Record(&page.id),
            &page.title,
            render_rule(scope, page),
        ));
    }
    for page in &corpus.sources {
        pages.push(rendered(
            WikiRoute::Record(&page.id),
            &page.title,
            render_source(scope, page),
        ));
    }
    pages
}

fn rendered(route: WikiRoute<'_>, title: &str, html: String) -> RenderedPage {
    RenderedPage {
        route: route.path(),
        title: title.to_string(),
        html,
    }
}

#[cfg(test)]
mod tests {
    #[path = "corpus.rs"]
    mod corpus;
    #[path = "discovery.rs"]
    mod discovery;
    #[path = "fixtures.rs"]
    mod fixtures;
    #[path = "fixtures_discovery.rs"]
    mod fixtures_discovery;
    #[path = "formatting.rs"]
    mod formatting;
    #[path = "homepage.rs"]
    mod homepage;
    #[path = "records.rs"]
    mod records;
    #[path = "requirement.rs"]
    mod requirement;

    use fixtures::{gappy_requirement_fixture, index_fixture, requirement_fixture, rule_fixture};

    use super::{render_index, render_requirement, render_rule};

    #[test]
    fn snapshot_requirement_page_with_rules_and_thread() {
        insta::assert_snapshot!(render_requirement("default", &requirement_fixture()));
    }

    #[test]
    fn snapshot_requirement_page_with_gaps() {
        insta::assert_snapshot!(render_requirement("default", &gappy_requirement_fixture()));
    }

    #[test]
    fn snapshot_scope_index_page() {
        insta::assert_snapshot!(render_index("default", &index_fixture()));
    }

    #[test]
    fn snapshot_rule_page_with_implementation_and_verifications() {
        insta::assert_snapshot!(render_rule("default", &rule_fixture()));
    }

    #[test]
    fn snapshot_rule_page_without_scanned_binding() {
        let mut page = rule_fixture();
        page.implementations.clear();
        page.verifications.clear();
        insta::assert_snapshot!(render_rule("default", &page));
    }

    #[test]
    fn snapshot_rule_page_without_a_code_scan() {
        let mut page = rule_fixture();
        page.code_scan = None;
        page.implementations.clear();
        page.verifications.clear();
        insta::assert_snapshot!(render_rule("default", &page));
    }

    #[test]
    fn snapshot_rule_page_with_typed_implementation_without_a_code_scan() {
        let mut page = rule_fixture();
        page.code_scan = None;
        page.implementations[0].location =
            crate::wiki::links::LinkResolver::new(Some("git@github.com:exampleorg/ex-api.git"))
                .resolve_at("src/UseCase.php", None);
        page.verifications.clear();
        insta::assert_snapshot!(render_rule("default", &page));
    }
}