use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
use super::Tag;
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum DecisionStatus {
Draft,
#[default]
Proposed,
Accepted,
Rejected,
Superseded,
Deprecated,
}
impl std::fmt::Display for DecisionStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DecisionStatus::Draft => write!(f, "Draft"),
DecisionStatus::Proposed => write!(f, "Proposed"),
DecisionStatus::Accepted => write!(f, "Accepted"),
DecisionStatus::Rejected => write!(f, "Rejected"),
DecisionStatus::Superseded => write!(f, "Superseded"),
DecisionStatus::Deprecated => write!(f, "Deprecated"),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum DecisionCategory {
#[default]
Architecture,
Technology,
Process,
Security,
Data,
Integration,
DataDesign,
Workflow,
Model,
Governance,
Performance,
Compliance,
Infrastructure,
Tooling,
}
impl std::fmt::Display for DecisionCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DecisionCategory::Architecture => write!(f, "Architecture"),
DecisionCategory::Technology => write!(f, "Technology"),
DecisionCategory::Process => write!(f, "Process"),
DecisionCategory::Security => write!(f, "Security"),
DecisionCategory::Data => write!(f, "Data"),
DecisionCategory::Integration => write!(f, "Integration"),
DecisionCategory::DataDesign => write!(f, "Data Design"),
DecisionCategory::Workflow => write!(f, "Workflow"),
DecisionCategory::Model => write!(f, "Model"),
DecisionCategory::Governance => write!(f, "Governance"),
DecisionCategory::Performance => write!(f, "Performance"),
DecisionCategory::Compliance => write!(f, "Compliance"),
DecisionCategory::Infrastructure => write!(f, "Infrastructure"),
DecisionCategory::Tooling => write!(f, "Tooling"),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum DriverPriority {
High,
#[default]
Medium,
Low,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DecisionDriver {
pub description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<DriverPriority>,
}
impl DecisionDriver {
pub fn new(description: impl Into<String>) -> Self {
Self {
description: description.into(),
priority: None,
}
}
pub fn with_priority(description: impl Into<String>, priority: DriverPriority) -> Self {
Self {
description: description.into(),
priority: Some(priority),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DecisionOption {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub pros: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub cons: Vec<String>,
pub selected: bool,
}
impl DecisionOption {
pub fn new(name: impl Into<String>, selected: bool) -> Self {
Self {
name: name.into(),
description: None,
pros: Vec::new(),
cons: Vec::new(),
selected,
}
}
pub fn with_details(
name: impl Into<String>,
description: impl Into<String>,
pros: Vec<String>,
cons: Vec<String>,
selected: bool,
) -> Self {
Self {
name: name.into(),
description: Some(description.into()),
pros,
cons,
selected,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum AssetRelationship {
Affects,
Implements,
Deprecates,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AssetLink {
#[serde(alias = "asset_type")]
pub asset_type: String,
#[serde(alias = "asset_id")]
pub asset_id: Uuid,
#[serde(alias = "asset_name")]
pub asset_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub relationship: Option<AssetRelationship>,
}
impl AssetLink {
pub fn new(
asset_type: impl Into<String>,
asset_id: Uuid,
asset_name: impl Into<String>,
) -> Self {
Self {
asset_type: asset_type.into(),
asset_id,
asset_name: asset_name.into(),
relationship: None,
}
}
pub fn with_relationship(
asset_type: impl Into<String>,
asset_id: Uuid,
asset_name: impl Into<String>,
relationship: AssetRelationship,
) -> Self {
Self {
asset_type: asset_type.into(),
asset_id,
asset_name: asset_name.into(),
relationship: Some(relationship),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct ComplianceAssessment {
#[serde(skip_serializing_if = "Option::is_none", alias = "regulatory_impact")]
pub regulatory_impact: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", alias = "privacy_assessment")]
pub privacy_assessment: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", alias = "security_assessment")]
pub security_assessment: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub frameworks: Vec<String>,
}
impl ComplianceAssessment {
pub fn is_empty(&self) -> bool {
self.regulatory_impact.is_none()
&& self.privacy_assessment.is_none()
&& self.security_assessment.is_none()
&& self.frameworks.is_empty()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct DecisionContact {
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct RaciMatrix {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub responsible: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub accountable: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub consulted: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub informed: Vec<String>,
}
impl RaciMatrix {
pub fn is_empty(&self) -> bool {
self.responsible.is_empty()
&& self.accountable.is_empty()
&& self.consulted.is_empty()
&& self.informed.is_empty()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Decision {
pub id: Uuid,
pub number: u64,
pub title: String,
pub status: DecisionStatus,
pub category: DecisionCategory,
#[serde(skip_serializing_if = "Option::is_none")]
pub domain: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", alias = "domain_id")]
pub domain_id: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none", alias = "workspace_id")]
pub workspace_id: Option<Uuid>,
pub date: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none", alias = "decided_at")]
pub decided_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub authors: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub deciders: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub consulted: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub informed: Vec<String>,
pub context: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub drivers: Vec<DecisionDriver>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub options: Vec<DecisionOption>,
pub decision: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub consequences: Option<String>,
#[serde(
default,
skip_serializing_if = "Vec::is_empty",
alias = "linked_assets"
)]
pub linked_assets: Vec<AssetLink>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supersedes: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none", alias = "superseded_by")]
pub superseded_by: Option<Uuid>,
#[serde(
default,
skip_serializing_if = "Vec::is_empty",
alias = "related_decisions"
)]
pub related_decisions: Vec<Uuid>,
#[serde(
default,
skip_serializing_if = "Vec::is_empty",
alias = "related_knowledge"
)]
pub related_knowledge: Vec<Uuid>,
#[serde(
default,
skip_serializing_if = "Vec::is_empty",
alias = "linked_sketches"
)]
pub linked_sketches: Vec<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub compliance: Option<ComplianceAssessment>,
#[serde(skip_serializing_if = "Option::is_none", alias = "confirmation_date")]
pub confirmation_date: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none", alias = "confirmation_notes")]
pub confirmation_notes: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<Tag>,
#[serde(skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
#[serde(
default,
skip_serializing_if = "HashMap::is_empty",
alias = "custom_properties"
)]
pub custom_properties: HashMap<String, serde_json::Value>,
#[serde(alias = "created_at")]
pub created_at: DateTime<Utc>,
#[serde(alias = "updated_at")]
pub updated_at: DateTime<Utc>,
}
impl Decision {
pub fn new(
number: u64,
title: impl Into<String>,
context: impl Into<String>,
decision: impl Into<String>,
author: impl Into<String>,
) -> Self {
let now = Utc::now();
Self {
id: Self::generate_id(number),
number,
title: title.into(),
status: DecisionStatus::Proposed,
category: DecisionCategory::Architecture,
domain: None,
domain_id: None,
workspace_id: None,
date: now,
decided_at: None,
authors: vec![author.into()],
deciders: Vec::new(),
consulted: Vec::new(),
informed: Vec::new(),
context: context.into(),
drivers: Vec::new(),
options: Vec::new(),
decision: decision.into(),
consequences: None,
linked_assets: Vec::new(),
supersedes: None,
superseded_by: None,
related_decisions: Vec::new(),
related_knowledge: Vec::new(),
linked_sketches: Vec::new(),
compliance: None,
confirmation_date: None,
confirmation_notes: None,
tags: Vec::new(),
notes: None,
custom_properties: HashMap::new(),
created_at: now,
updated_at: now,
}
}
pub fn new_with_timestamp(
title: impl Into<String>,
context: impl Into<String>,
decision: impl Into<String>,
author: impl Into<String>,
) -> Self {
let now = Utc::now();
let number = Self::generate_timestamp_number(&now);
Self::new(number, title, context, decision, author)
}
pub fn generate_timestamp_number(dt: &DateTime<Utc>) -> u64 {
let formatted = dt.format("%y%m%d%H%M").to_string();
formatted.parse().unwrap_or(0)
}
pub fn generate_id(number: u64) -> Uuid {
let namespace = Uuid::parse_str("6ba7b810-9dad-11d1-80b4-00c04fd430c8").unwrap(); let name = format!("decision:{}", number);
Uuid::new_v5(&namespace, name.as_bytes())
}
pub fn add_author(mut self, author: impl Into<String>) -> Self {
self.authors.push(author.into());
self.updated_at = Utc::now();
self
}
pub fn add_consulted(mut self, consulted: impl Into<String>) -> Self {
self.consulted.push(consulted.into());
self.updated_at = Utc::now();
self
}
pub fn add_informed(mut self, informed: impl Into<String>) -> Self {
self.informed.push(informed.into());
self.updated_at = Utc::now();
self
}
pub fn add_related_decision(mut self, decision_id: Uuid) -> Self {
self.related_decisions.push(decision_id);
self.updated_at = Utc::now();
self
}
pub fn add_related_knowledge(mut self, article_id: Uuid) -> Self {
self.related_knowledge.push(article_id);
self.updated_at = Utc::now();
self
}
pub fn link_sketch(mut self, sketch_id: Uuid) -> Self {
if !self.linked_sketches.contains(&sketch_id) {
self.linked_sketches.push(sketch_id);
self.updated_at = Utc::now();
}
self
}
pub fn with_decided_at(mut self, decided_at: DateTime<Utc>) -> Self {
self.decided_at = Some(decided_at);
self.updated_at = Utc::now();
self
}
pub fn with_domain_id(mut self, domain_id: Uuid) -> Self {
self.domain_id = Some(domain_id);
self.updated_at = Utc::now();
self
}
pub fn with_workspace_id(mut self, workspace_id: Uuid) -> Self {
self.workspace_id = Some(workspace_id);
self.updated_at = Utc::now();
self
}
pub fn with_status(mut self, status: DecisionStatus) -> Self {
self.status = status;
self.updated_at = Utc::now();
self
}
pub fn with_category(mut self, category: DecisionCategory) -> Self {
self.category = category;
self.updated_at = Utc::now();
self
}
pub fn with_domain(mut self, domain: impl Into<String>) -> Self {
self.domain = Some(domain.into());
self.updated_at = Utc::now();
self
}
pub fn add_decider(mut self, decider: impl Into<String>) -> Self {
self.deciders.push(decider.into());
self.updated_at = Utc::now();
self
}
pub fn add_driver(mut self, driver: DecisionDriver) -> Self {
self.drivers.push(driver);
self.updated_at = Utc::now();
self
}
pub fn add_option(mut self, option: DecisionOption) -> Self {
self.options.push(option);
self.updated_at = Utc::now();
self
}
pub fn with_consequences(mut self, consequences: impl Into<String>) -> Self {
self.consequences = Some(consequences.into());
self.updated_at = Utc::now();
self
}
pub fn add_asset_link(mut self, link: AssetLink) -> Self {
self.linked_assets.push(link);
self.updated_at = Utc::now();
self
}
pub fn with_compliance(mut self, compliance: ComplianceAssessment) -> Self {
self.compliance = Some(compliance);
self.updated_at = Utc::now();
self
}
pub fn supersedes_decision(mut self, other_id: Uuid) -> Self {
self.supersedes = Some(other_id);
self.updated_at = Utc::now();
self
}
pub fn superseded_by_decision(&mut self, other_id: Uuid) {
self.superseded_by = Some(other_id);
self.status = DecisionStatus::Superseded;
self.updated_at = Utc::now();
}
pub fn add_tag(mut self, tag: Tag) -> Self {
self.tags.push(tag);
self.updated_at = Utc::now();
self
}
pub fn is_timestamp_number(&self) -> bool {
self.number >= 1000000000 && self.number <= 9999999999
}
pub fn formatted_number(&self) -> String {
if self.is_timestamp_number() {
format!("ADR-{}", self.number)
} else {
format!("ADR-{:04}", self.number)
}
}
pub fn filename(&self, workspace_name: &str) -> String {
let number_str = if self.is_timestamp_number() {
format!("{}", self.number)
} else {
format!("{:04}", self.number)
};
match &self.domain {
Some(domain) => format!(
"{}_{}_adr-{}.madr.yaml",
sanitize_name(workspace_name),
sanitize_name(domain),
number_str
),
None => format!(
"{}_adr-{}.madr.yaml",
sanitize_name(workspace_name),
number_str
),
}
}
pub fn markdown_filename(&self) -> String {
let slug = slugify(&self.title);
if self.is_timestamp_number() {
format!("ADR-{}-{}.md", self.number, slug)
} else {
format!("ADR-{:04}-{}.md", self.number, slug)
}
}
pub fn from_yaml(yaml_content: &str) -> Result<Self, serde_yaml::Error> {
serde_yaml::from_str(yaml_content)
}
pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
serde_yaml::to_string(self)
}
pub fn to_yaml_pretty(&self) -> Result<String, serde_yaml::Error> {
serde_yaml::to_string(self)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DecisionIndexEntry {
pub number: u64,
pub id: Uuid,
pub title: String,
pub status: DecisionStatus,
pub category: DecisionCategory,
#[serde(skip_serializing_if = "Option::is_none")]
pub domain: Option<String>,
pub file: String,
}
impl From<&Decision> for DecisionIndexEntry {
fn from(decision: &Decision) -> Self {
Self {
number: decision.number,
id: decision.id,
title: decision.title.clone(),
status: decision.status.clone(),
category: decision.category.clone(),
domain: decision.domain.clone(),
file: String::new(), }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DecisionIndex {
#[serde(alias = "schema_version")]
pub schema_version: String,
#[serde(skip_serializing_if = "Option::is_none", alias = "last_updated")]
pub last_updated: Option<DateTime<Utc>>,
#[serde(default)]
pub decisions: Vec<DecisionIndexEntry>,
#[serde(alias = "next_number")]
pub next_number: u64,
#[serde(default, alias = "use_timestamp_numbering")]
pub use_timestamp_numbering: bool,
}
impl Default for DecisionIndex {
fn default() -> Self {
Self::new()
}
}
impl DecisionIndex {
pub fn new() -> Self {
Self {
schema_version: "1.0".to_string(),
last_updated: Some(Utc::now()),
decisions: Vec::new(),
next_number: 1,
use_timestamp_numbering: false,
}
}
pub fn new_with_timestamp_numbering() -> Self {
Self {
schema_version: "1.0".to_string(),
last_updated: Some(Utc::now()),
decisions: Vec::new(),
next_number: 1,
use_timestamp_numbering: true,
}
}
pub fn add_decision(&mut self, decision: &Decision, filename: String) {
let mut entry = DecisionIndexEntry::from(decision);
entry.file = filename;
self.decisions.retain(|d| d.number != decision.number);
self.decisions.push(entry);
self.decisions.sort_by_key(|d| d.number);
if !self.use_timestamp_numbering && decision.number >= self.next_number {
self.next_number = decision.number + 1;
}
self.last_updated = Some(Utc::now());
}
pub fn get_next_number(&self) -> u64 {
if self.use_timestamp_numbering {
Decision::generate_timestamp_number(&Utc::now())
} else {
self.next_number
}
}
pub fn find_by_number(&self, number: u64) -> Option<&DecisionIndexEntry> {
self.decisions.iter().find(|d| d.number == number)
}
pub fn from_yaml(yaml_content: &str) -> Result<Self, serde_yaml::Error> {
serde_yaml::from_str(yaml_content)
}
pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
serde_yaml::to_string(self)
}
}
fn sanitize_name(name: &str) -> String {
name.chars()
.map(|c| match c {
' ' | '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '-',
_ => c,
})
.collect::<String>()
.to_lowercase()
}
fn slugify(title: &str) -> String {
title
.to_lowercase()
.chars()
.map(|c| if c.is_alphanumeric() { c } else { '-' })
.collect::<String>()
.split('-')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join("-")
.chars()
.take(50) .collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decision_new() {
let decision = Decision::new(
1,
"Use ODCS v3.1.0",
"We need a standard format",
"We will use ODCS v3.1.0",
"author@example.com",
);
assert_eq!(decision.number, 1);
assert_eq!(decision.title, "Use ODCS v3.1.0");
assert_eq!(decision.status, DecisionStatus::Proposed);
assert_eq!(decision.category, DecisionCategory::Architecture);
assert_eq!(decision.authors.len(), 1);
assert_eq!(decision.authors[0], "author@example.com");
}
#[test]
fn test_decision_builder_pattern() {
let decision = Decision::new(1, "Test", "Context", "Decision", "author@example.com")
.with_status(DecisionStatus::Accepted)
.with_category(DecisionCategory::DataDesign)
.with_domain("sales")
.add_decider("team@example.com")
.add_driver(DecisionDriver::with_priority(
"Need consistency",
DriverPriority::High,
))
.with_consequences("Better consistency");
assert_eq!(decision.status, DecisionStatus::Accepted);
assert_eq!(decision.category, DecisionCategory::DataDesign);
assert_eq!(decision.domain, Some("sales".to_string()));
assert_eq!(decision.deciders.len(), 1);
assert_eq!(decision.drivers.len(), 1);
assert!(decision.consequences.is_some());
}
#[test]
fn test_decision_id_generation() {
let id1 = Decision::generate_id(1);
let id2 = Decision::generate_id(1);
let id3 = Decision::generate_id(2);
assert_eq!(id1, id2);
assert_ne!(id1, id3);
}
#[test]
fn test_decision_filename() {
let decision = Decision::new(1, "Test", "Context", "Decision", "author@example.com");
assert_eq!(
decision.filename("enterprise"),
"enterprise_adr-0001.madr.yaml"
);
let decision_with_domain = decision.with_domain("sales");
assert_eq!(
decision_with_domain.filename("enterprise"),
"enterprise_sales_adr-0001.madr.yaml"
);
}
#[test]
fn test_decision_markdown_filename() {
let decision = Decision::new(
1,
"Use ODCS v3.1.0 for all data contracts",
"Context",
"Decision",
"author@example.com",
);
let filename = decision.markdown_filename();
assert!(filename.starts_with("ADR-0001-"));
assert!(filename.ends_with(".md"));
}
#[test]
fn test_decision_yaml_roundtrip() {
let decision = Decision::new(
1,
"Test Decision",
"Some context",
"The decision",
"author@example.com",
)
.with_status(DecisionStatus::Accepted)
.with_domain("test");
let yaml = decision.to_yaml().unwrap();
let parsed = Decision::from_yaml(&yaml).unwrap();
assert_eq!(decision.id, parsed.id);
assert_eq!(decision.title, parsed.title);
assert_eq!(decision.status, parsed.status);
assert_eq!(decision.domain, parsed.domain);
}
#[test]
fn test_decision_index() {
let mut index = DecisionIndex::new();
assert_eq!(index.get_next_number(), 1);
let decision1 = Decision::new(1, "First", "Context", "Decision", "author@example.com");
index.add_decision(&decision1, "test_adr-0001.madr.yaml".to_string());
assert_eq!(index.decisions.len(), 1);
assert_eq!(index.get_next_number(), 2);
let decision2 = Decision::new(2, "Second", "Context", "Decision", "author@example.com");
index.add_decision(&decision2, "test_adr-0002.madr.yaml".to_string());
assert_eq!(index.decisions.len(), 2);
assert_eq!(index.get_next_number(), 3);
}
#[test]
fn test_slugify() {
assert_eq!(slugify("Use ODCS v3.1.0"), "use-odcs-v3-1-0");
assert_eq!(slugify("Hello World"), "hello-world");
assert_eq!(slugify("test--double"), "test-double");
}
#[test]
fn test_decision_status_display() {
assert_eq!(format!("{}", DecisionStatus::Proposed), "Proposed");
assert_eq!(format!("{}", DecisionStatus::Accepted), "Accepted");
assert_eq!(format!("{}", DecisionStatus::Deprecated), "Deprecated");
assert_eq!(format!("{}", DecisionStatus::Superseded), "Superseded");
assert_eq!(format!("{}", DecisionStatus::Rejected), "Rejected");
}
#[test]
fn test_asset_link() {
let link = AssetLink::with_relationship(
"odcs",
Uuid::new_v4(),
"orders",
AssetRelationship::Implements,
);
assert_eq!(link.asset_type, "odcs");
assert_eq!(link.asset_name, "orders");
assert_eq!(link.relationship, Some(AssetRelationship::Implements));
}
#[test]
fn test_timestamp_number_generation() {
use chrono::TimeZone;
let dt = Utc.with_ymd_and_hms(2026, 1, 10, 14, 30, 0).unwrap();
let number = Decision::generate_timestamp_number(&dt);
assert_eq!(number, 2601101430);
}
#[test]
fn test_is_timestamp_number() {
let sequential_decision =
Decision::new(1, "Test", "Context", "Decision", "author@example.com");
assert!(!sequential_decision.is_timestamp_number());
let timestamp_decision = Decision::new(
2601101430,
"Test",
"Context",
"Decision",
"author@example.com",
);
assert!(timestamp_decision.is_timestamp_number());
}
#[test]
fn test_timestamp_decision_filename() {
let decision = Decision::new(
2601101430,
"Test",
"Context",
"Decision",
"author@example.com",
);
assert_eq!(
decision.filename("enterprise"),
"enterprise_adr-2601101430.madr.yaml"
);
}
#[test]
fn test_timestamp_decision_markdown_filename() {
let decision = Decision::new(
2601101430,
"Test Decision",
"Context",
"Decision",
"author@example.com",
);
let filename = decision.markdown_filename();
assert!(filename.starts_with("ADR-2601101430-"));
assert!(filename.ends_with(".md"));
}
#[test]
fn test_decision_with_consulted_informed() {
let decision = Decision::new(1, "Test", "Context", "Decision", "author@example.com")
.add_consulted("security@example.com")
.add_informed("stakeholders@example.com");
assert_eq!(decision.consulted.len(), 1);
assert_eq!(decision.informed.len(), 1);
assert_eq!(decision.consulted[0], "security@example.com");
assert_eq!(decision.informed[0], "stakeholders@example.com");
}
#[test]
fn test_decision_with_authors() {
let decision = Decision::new(1, "Test", "Context", "Decision", "author1@example.com")
.add_author("author2@example.com")
.add_author("author3@example.com");
assert_eq!(decision.authors.len(), 3);
}
#[test]
fn test_decision_index_with_timestamp_numbering() {
let index = DecisionIndex::new_with_timestamp_numbering();
assert!(index.use_timestamp_numbering);
let next = index.get_next_number();
assert!(next >= 1000000000); }
#[test]
fn test_new_categories() {
assert_eq!(format!("{}", DecisionCategory::Data), "Data");
assert_eq!(format!("{}", DecisionCategory::Integration), "Integration");
}
#[test]
fn test_decision_with_related() {
let related_decision_id = Uuid::new_v4();
let related_knowledge_id = Uuid::new_v4();
let decision = Decision::new(1, "Test", "Context", "Decision", "author@example.com")
.add_related_decision(related_decision_id)
.add_related_knowledge(related_knowledge_id);
assert_eq!(decision.related_decisions.len(), 1);
assert_eq!(decision.related_knowledge.len(), 1);
assert_eq!(decision.related_decisions[0], related_decision_id);
assert_eq!(decision.related_knowledge[0], related_knowledge_id);
}
#[test]
fn test_decision_status_draft() {
let decision = Decision::new(1, "Test", "Context", "Decision", "author@example.com")
.with_status(DecisionStatus::Draft);
assert_eq!(decision.status, DecisionStatus::Draft);
assert_eq!(format!("{}", DecisionStatus::Draft), "Draft");
}
}