use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum KnowledgeKind {
Decision,
Requirement,
Guidance,
Proposal,
}
impl KnowledgeKind {
pub fn parse(s: &str) -> Option<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"decision" => Some(KnowledgeKind::Decision),
"requirement" => Some(KnowledgeKind::Requirement),
"guidance" => Some(KnowledgeKind::Guidance),
"proposal" => Some(KnowledgeKind::Proposal),
_ => None,
}
}
pub fn dir(self) -> &'static str {
match self {
KnowledgeKind::Decision => "decisions",
KnowledgeKind::Requirement => "requirements",
KnowledgeKind::Guidance => "guidance",
KnowledgeKind::Proposal => "proposals",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DecisionStatus {
Proposed,
Accepted,
Superseded,
Deprecated,
Rejected,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum LivenessDeclared {
#[default]
Auto,
Na,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Liveness {
Honored,
Violated,
Unproven,
Na,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct KnowledgeMeta {
pub kind: KnowledgeKind,
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<DecisionStatus>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub supersedes: Option<String>,
#[serde(default)]
pub liveness: LivenessDeclared,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct KSection {
pub heading: String,
pub body: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KnowledgeDoc {
pub meta: KnowledgeMeta,
pub sections: Vec<KSection>,
#[serde(skip)]
pub source_path: PathBuf,
}
pub type DecisionDoc = KnowledgeDoc;
impl KnowledgeDoc {
pub fn section(&self, heading: &str) -> Option<&KSection> {
self.sections
.iter()
.find(|s| s.heading.eq_ignore_ascii_case(heading))
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_section_lookup_is_case_insensitive() {
let doc = DecisionDoc {
meta: KnowledgeMeta {
kind: KnowledgeKind::Decision,
id: "ADR-001".into(),
title: None,
status: Some(DecisionStatus::Accepted),
supersedes: None,
liveness: LivenessDeclared::Auto,
tags: vec![],
},
sections: vec![KSection {
heading: "Context".into(),
body: "x".into(),
}],
source_path: PathBuf::new(),
};
assert!(doc.section("context").is_some());
assert!(doc.section("Decision").is_none());
}
}