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(§ion::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(§ion::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" ); } }
GitRust