pub mod format;
pub mod frontmatter;
mod adr001;
mod adr002;
mod adr003;
mod adr004;
mod adr005;
mod adr006;
mod adr007;
mod adr008;
mod adr009;
mod adr010;
mod adr011;
mod adr012;
mod adr013;
mod adr014;
mod adr015;
mod adr016;
mod adr017;
use crate::{RuleProvider, RuleRegistry};
use mdbook_lint_core::Config;
pub use adr001::Adr001;
pub use adr002::Adr002;
pub use adr003::Adr003;
pub use adr004::Adr004;
pub use adr005::Adr005;
pub use adr006::Adr006;
pub use adr007::Adr007;
pub use adr008::Adr008;
pub use adr009::Adr009;
pub use adr010::Adr010;
pub use adr011::Adr011;
pub use adr012::Adr012;
pub use adr013::Adr013;
pub use adr014::Adr014;
pub use adr015::Adr015;
pub use adr016::Adr016;
pub use adr017::Adr017;
pub use format::AdrFormat;
pub use frontmatter::AdrFrontmatter;
pub struct AdrRuleProvider;
impl RuleProvider for AdrRuleProvider {
fn provider_id(&self) -> &'static str {
"adr"
}
fn description(&self) -> &'static str {
"Architecture Decision Record linting rules (ADR001-ADR017)"
}
fn version(&self) -> &'static str {
"0.1.0"
}
fn register_rules(&self, registry: &mut RuleRegistry) {
registry.register(Box::new(Adr001::default()));
registry.register(Box::new(Adr002::default()));
registry.register(Box::new(Adr003::default()));
registry.register(Box::new(Adr004::default()));
registry.register(Box::new(Adr005::default()));
registry.register(Box::new(Adr006::default()));
registry.register(Box::new(Adr007::default()));
registry.register(Box::new(Adr008::default()));
registry.register(Box::new(Adr009::default()));
registry.register(Box::new(Adr014::default()));
registry.register(Box::new(Adr015::default()));
registry.register(Box::new(Adr016::default()));
registry.register(Box::new(Adr017::default()));
registry.register_collection_rule(Box::new(Adr010));
registry.register_collection_rule(Box::new(Adr011));
registry.register_collection_rule(Box::new(Adr012));
registry.register_collection_rule(Box::new(Adr013));
}
fn register_rules_with_config(&self, registry: &mut RuleRegistry, config: Option<&Config>) {
let provider_cfg = config.and_then(|c| c.rule_configs.get("ADR"));
let cfg = |id: &str| config.and_then(|c| c.rule_configs.get(id)).or(provider_cfg);
macro_rules! register_configurable {
($id:literal, $ty:ty) => {{
let rule = match cfg($id) {
Some(c) => <$ty>::from_config(c),
None => <$ty>::default(),
};
registry.register(Box::new(rule));
}};
}
register_configurable!("ADR001", Adr001);
register_configurable!("ADR002", Adr002);
register_configurable!("ADR003", Adr003);
register_configurable!("ADR004", Adr004);
register_configurable!("ADR005", Adr005);
register_configurable!("ADR006", Adr006);
register_configurable!("ADR007", Adr007);
register_configurable!("ADR008", Adr008);
register_configurable!("ADR009", Adr009);
register_configurable!("ADR014", Adr014);
register_configurable!("ADR015", Adr015);
register_configurable!("ADR016", Adr016);
register_configurable!("ADR017", Adr017);
registry.register_collection_rule(Box::new(Adr010));
registry.register_collection_rule(Box::new(Adr011));
registry.register_collection_rule(Box::new(Adr012));
registry.register_collection_rule(Box::new(Adr013));
}
fn rule_ids(&self) -> Vec<&'static str> {
vec![
"ADR001", "ADR002", "ADR003", "ADR004", "ADR005", "ADR006", "ADR007", "ADR008",
"ADR009", "ADR010", "ADR011", "ADR012", "ADR013", "ADR014", "ADR015", "ADR016",
"ADR017",
]
}
}
#[cfg(test)]
mod tests {
use super::*;
use mdbook_lint_core::Document;
use mdbook_lint_core::rule::Rule;
use std::path::PathBuf;
fn create_test_document(content: &str) -> Document {
Document::new(content.to_string(), PathBuf::from("adr/0001-test-adr.md")).unwrap()
}
#[test]
fn test_adr_format_override_threads_through_provider() {
use mdbook_lint_core::{Config, PluginRegistry};
let content = "# 1. Use PostgreSQL\n\n\
Date: 2024-01-01\n\n\
## Status\n\nAccepted\n\n\
## Context\n\nWe need a database.\n\n\
## Decision\n\nWe will use PostgreSQL.\n\n\
## Consequences\n\nIt works.\n";
let doc = create_test_document(content);
let adr004_violations = |toml: &str| {
let config: Config = toml::from_str(toml).unwrap();
let mut registry = PluginRegistry::new();
registry
.register_provider(Box::new(AdrRuleProvider))
.unwrap();
let engine = registry.create_engine_with_config(Some(&config)).unwrap();
engine
.lint_document_with_config(&doc, &config)
.unwrap()
.into_iter()
.filter(|v| v.rule_id == "ADR004")
.count()
};
assert_eq!(
adr004_violations("enabled-rules = [\"ADR004\"]"),
0,
"ADR004 should not apply to an auto-detected Nygard document"
);
assert!(
adr004_violations("enabled-rules = [\"ADR004\"]\n[ADR]\nformat = \"madr\"") > 0,
"[ADR] format = madr must thread through the provider and make ADR004 apply"
);
}
#[test]
fn test_provider_metadata() {
let provider = AdrRuleProvider;
assert_eq!(provider.provider_id(), "adr");
assert!(provider.description().contains("ADR"));
assert_eq!(provider.version(), "0.1.0");
}
#[test]
fn test_provider_rule_ids() {
let provider = AdrRuleProvider;
let ids = provider.rule_ids();
assert!(ids.contains(&"ADR001"));
assert!(ids.contains(&"ADR002"));
assert!(ids.contains(&"ADR003"));
}
#[test]
fn test_valid_nygard_adr() {
let content = r#"# 1. Use Rust for implementation
Date: 2024-01-15
## Status
Accepted
## Context
We need to choose a programming language.
## Decision
We will use Rust.
## Consequences
Team will need Rust training.
"#;
let doc = create_test_document(content);
let rule1 = Adr001::default();
let rule2 = Adr002::default();
let rule3 = Adr003::default();
assert!(rule1.check(&doc).unwrap().is_empty());
assert!(rule2.check(&doc).unwrap().is_empty());
assert!(rule3.check(&doc).unwrap().is_empty());
}
#[test]
fn test_valid_madr_adr() {
let content = r#"---
status: accepted
date: 2024-01-15
decision-makers:
- Alice Smith
---
# Use PostgreSQL for persistence
## Context and Problem Statement
We need to select a database.
## Decision Outcome
Chosen option: PostgreSQL.
"#;
let doc = create_test_document(content);
let rule1 = Adr001::default();
let rule2 = Adr002::default();
let rule3 = Adr003::default();
assert!(rule1.check(&doc).unwrap().is_empty());
assert!(rule2.check(&doc).unwrap().is_empty());
assert!(rule3.check(&doc).unwrap().is_empty());
}
#[test]
fn test_invalid_nygard_adr_all_rules_fail() {
let content = r#"# Use Rust
## Context
We need a language.
"#;
let doc = create_test_document(content);
let rule1 = Adr001::default();
let rule2 = Adr002::default();
let rule3 = Adr003::default();
assert!(!rule1.check(&doc).unwrap().is_empty(), "ADR001 should fail");
assert!(!rule2.check(&doc).unwrap().is_empty(), "ADR002 should fail");
assert!(!rule3.check(&doc).unwrap().is_empty(), "ADR003 should fail");
}
#[test]
fn test_invalid_madr_adr_missing_fields() {
let content = r#"---
decision-makers:
- Alice
---
# Title
## Context
Content.
"#;
let doc = create_test_document(content);
let rule1 = Adr001::default();
let rule2 = Adr002::default();
let rule3 = Adr003::default();
assert!(
rule1.check(&doc).unwrap().is_empty(),
"ADR001 should pass (title exists)"
);
assert!(
!rule2.check(&doc).unwrap().is_empty(),
"ADR002 should fail (no status)"
);
assert!(
!rule3.check(&doc).unwrap().is_empty(),
"ADR003 should fail (no date)"
);
}
}