generate.rs 147 lignes · 5544 octets
//! Construction des lignes de todo et rendu du plan Markdown complet.

use crate::catalogue::Functionality;
use crate::todo::{self, TodoRow};
use crate::{section, security};

/// Entrées de génération du plan.
pub struct PlanInputs<'a> {
    pub project: &'a str,
    pub selected: Vec<&'static Functionality>,
    pub design_md: Option<String>,
    pub layout: Option<String>,
}

/// Aplati les fonctionnalités sélectionnées en lignes de todo (non faites).
pub fn rows_for(selected: &[&'static Functionality]) -> Vec<TodoRow> {
    let mut rows = Vec::new();
    for f in selected {
        for s in f.screens {
            rows.push(TodoRow {
                done: false,
                functionality: f.label.to_string(),
                screen: s.name.to_string(),
                route: s.suggested_route.to_string(),
                channel: s.channel.label().to_string(),
                protected: s.auth_level.is_protected(),
                methods: s.core_methods.to_string(),
                io: s.io.to_string(),
            });
        }
    }
    rows
}

/// Rend le document complet : titre, charte, structure, tableau.
pub fn render_plan(inputs: &PlanInputs, rows: &[TodoRow]) -> String {
    let mut out = String::new();
    out.push_str(&format!("# Plan UI — {}\n\n", inputs.project));
    out.push_str("> Généré par `cargo run -p rustwarden-core --bin screen-select`. Recochez le réalisé via `cargo run -p rustwarden-core --bin screen-select -- --check`.\n\n");
    out.push_str(&format!(
        "Codez ces écrans dans une crate dédiée **`{}-ui`** consommant `rustwarden-core`, montée dans le routeur principal.\n\n",
        inputs.project
    ));
    // Garde-fous transversaux + conventions de rendu sûr (statique, vu en premier).
    out.push_str(&section::block(
        security::GUIDES_ID,
        &security::guides_body(),
    ));
    out.push_str("\n\n");
    if let Some(design) = &inputs.design_md {
        out.push_str("## Charte graphique (à respecter)\n\n");
        out.push_str(design.trim());
        out.push_str("\n\n");
    }
    if let Some(layout) = &inputs.layout {
        out.push_str("## Structure de page\n\n");
        out.push_str(layout.trim());
        out.push_str("\n\n");
    }
    out.push_str("## Écrans à coder\n\n");
    out.push_str(&todo::render(rows));
    out.push('\n');
    // Contrat de sécurité par écran (dérivé du catalogue, en référence sous le tableau).
    out.push_str(&section::block(
        security::PER_SCREEN_ID,
        &security::reference_body(&inputs.selected),
    ));
    out.push('\n');
    out
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::catalogue;

    #[test]
    fn rows_for_aplati_les_ecrans() {
        let two_factor = catalogue::by_key("two_factor").unwrap();
        let rows = rows_for(&[two_factor]);
        assert_eq!(rows.len(), 3); // challenge + setup + backup
        assert!(rows.iter().all(|r| !r.done));
        assert!(rows.iter().any(|r| r.screen == "TwoFactorSetup"));
    }

    #[test]
    fn render_plan_inclut_charte_et_tableau() {
        let auth = catalogue::by_key("authentication").unwrap();
        let inputs = PlanInputs {
            project: "demo",
            selected: vec![auth],
            design_md: Some("Police: Inter. Couleur: #123456".into()),
            layout: Some("navbar: gauche; 2 colonnes".into()),
        };
        let rows = rows_for(&inputs.selected);
        let out = render_plan(&inputs, &rows);
        assert!(out.contains("# Plan UI — demo"));
        assert!(
            out.contains("**`demo-ui`**"),
            "l'intro doit nommer la crate <projet>-ui"
        );
        assert!(out.contains("Police: Inter"));
        assert!(out.contains("navbar: gauche"));
        assert!(out.contains("| Statut | Fonctionnalité |"));
        assert!(out.contains("Login"));
    }

    #[test]
    fn rows_for_derive_protected_du_niveau_auth() {
        // Login est Public ⇒ non protégé ; TwoFactorSetup est Authentifié ⇒ protégé.
        let auth = catalogue::by_key("authentication").unwrap();
        let tf = catalogue::by_key("two_factor").unwrap();
        let login = &rows_for(&[auth])[0];
        assert!(!login.protected);
        let setup = rows_for(&[tf])
            .into_iter()
            .find(|r| r.screen == "TwoFactorSetup")
            .unwrap();
        assert!(setup.protected);
    }

    #[test]
    fn render_plan_inclut_les_sections_securite() {
        let auth = catalogue::by_key("authentication").unwrap();
        let inputs = PlanInputs {
            project: "demo",
            selected: vec![auth],
            design_md: None,
            layout: None,
        };
        let rows = rows_for(&inputs.selected);
        let out = render_plan(&inputs, &rows);
        assert!(out.contains("<!-- screen-select:security-guides -->"));
        assert!(out.contains("Sécurité transversale"));
        assert!(out.contains("Conventions de rendu sûr"));
        assert!(out.contains("<!-- screen-select:security-per-screen -->"));
        assert!(out.contains("## Contrat de sécurité par écran"));
        assert!(out.contains("### Login (`/login`) — Public"));
        // le bloc statique doit précéder le tableau (vu en premier par le dev)
        let guides = out.find("Sécurité transversale").unwrap();
        let table = out.find("| Statut").unwrap();
        assert!(
            guides < table,
            "les garde-fous transversaux doivent précéder le tableau"
        );
    }
}