use std::path::PathBuf;
use crate::domain::model::check::Severity;
use crate::domain::model::entity_ref::EntityRef;
use crate::domain::model::tag_validation::TagViolation;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CheckViolation {
pub rule_id: &'static str,
pub path: PathBuf,
pub severity: Severity,
pub kind: CheckViolationKind,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CheckViolationKind {
WrongIdPrefix { id: EntityRef, expected: String },
DuplicateId {
id: EntityRef,
also_at: Vec<PathBuf>,
},
IdSlugMismatch {
id_suffix: String,
dir_prefix: String,
},
LinkTargetNotFound { target: EntityRef },
EventLogBroken { cause: EventLogIssue },
TagDescriptorViolation {
owner: EntityRef,
cause: TagViolation,
},
MissingBackPointer {
source: EntityRef,
forward: String,
back: String,
},
PrematureBackPointer {
source: EntityRef,
source_status: String,
back: String,
},
MissingForwardLink {
source: EntityRef,
forward: String,
back: String,
},
TagOrderViolated {
tag_key: String,
parent: EntityRef,
parent_value: String,
child: EntityRef,
child_value: String,
},
MultipleParents {
child: EntityRef,
parents: Vec<EntityRef>,
},
ParentOfCycle { cycle: Vec<EntityRef> },
TerminalParentWithOpenChildren {
parent: EntityRef,
parent_status: String,
open_children: Vec<EntityRef>,
},
ScanIssue { detail: String },
BrokenCompanionLink { from: String, to: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EventLogIssue {
CreatedStatusUnknown {
status: String,
},
CreatedStatusMismatch {
status: String,
expected: String,
},
FirstActionNotCreated {
found: String,
},
StatusChangeFromUnknown {
from: String,
},
StatusChangeToUnknown {
to: String,
},
EventChainBroken {
from: String,
prev_status: String,
},
InvalidTransition {
from: String,
to: String,
},
TerminalStatusOutbound {
from: String,
},
FinalStatusMismatch {
last_to: String,
current_status: String,
},
}
#[cfg(test)]
pub fn render(kind: &CheckViolationKind) -> String {
match kind {
CheckViolationKind::WrongIdPrefix { id, expected } => {
format!("WrongIdPrefix id={id} expected={expected}")
}
CheckViolationKind::DuplicateId { id, also_at } => {
let paths = also_at
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(",");
format!("DuplicateId id={id} also_at={paths}")
}
CheckViolationKind::IdSlugMismatch {
id_suffix,
dir_prefix,
} => format!("IdSlugMismatch id_suffix={id_suffix} dir_prefix={dir_prefix}"),
CheckViolationKind::LinkTargetNotFound { target } => {
format!("LinkTargetNotFound target={target}")
}
CheckViolationKind::EventLogBroken { cause } => {
format!("EventLogBroken cause={cause:?}")
}
CheckViolationKind::TagDescriptorViolation { owner, cause } => {
format!("TagDescriptorViolation owner={owner} cause={cause:?}")
}
CheckViolationKind::MissingBackPointer {
source,
forward,
back,
} => format!("MissingBackPointer source={source} forward={forward} back={back}"),
CheckViolationKind::PrematureBackPointer {
source,
source_status,
back,
} => format!(
"PrematureBackPointer source={source} source_status={source_status} back={back}"
),
CheckViolationKind::MissingForwardLink {
source,
forward,
back,
} => format!("MissingForwardLink source={source} forward={forward} back={back}"),
CheckViolationKind::TagOrderViolated {
tag_key,
parent,
parent_value,
child,
child_value,
} => format!(
"TagOrderViolated tag_key={tag_key} parent={parent} parent_value={parent_value} \
child={child} child_value={child_value}"
),
CheckViolationKind::MultipleParents { child, parents } => {
let p = parents
.iter()
.map(|r| r.as_str())
.collect::<Vec<_>>()
.join(",");
format!("MultipleParents child={child} parents={p}")
}
CheckViolationKind::ParentOfCycle { cycle } => {
let c = cycle
.iter()
.map(|r| r.as_str())
.collect::<Vec<_>>()
.join(",");
format!("ParentOfCycle cycle={c}")
}
CheckViolationKind::TerminalParentWithOpenChildren {
parent,
parent_status,
open_children,
} => {
let oc = open_children
.iter()
.map(|r| r.as_str())
.collect::<Vec<_>>()
.join(",");
format!(
"TerminalParentWithOpenChildren parent={parent} parent_status={parent_status} \
open_children={oc}"
)
}
CheckViolationKind::ScanIssue { detail } => format!("ScanIssue detail={detail}"),
CheckViolationKind::BrokenCompanionLink { from, to } => {
format!("BrokenCompanionLink from={from} to={to}")
}
}
}
#[cfg(test)]
pub mod strategy {
use super::{CheckViolation, CheckViolationKind, EventLogIssue};
use crate::domain::model::check::violation::strategy::severity;
use proptest::prelude::*;
use std::path::PathBuf;
pub fn check_violation_kind() -> impl Strategy<Value = CheckViolationKind> {
prop_oneof![
".{0,40}".prop_map(|d| CheckViolationKind::ScanIssue { detail: d }),
("[a-z]{1,8}", "[a-z]{1,8}")
.prop_map(|(from, to)| { CheckViolationKind::BrokenCompanionLink { from, to } }),
]
}
#[allow(dead_code)] pub fn event_log_issue() -> impl Strategy<Value = EventLogIssue> {
prop_oneof![
"[a-z]{1,8}".prop_map(|found| EventLogIssue::FirstActionNotCreated { found }),
("[a-z]{1,8}", "[a-z]{1,8}")
.prop_map(|(from, to)| { EventLogIssue::InvalidTransition { from, to } }),
]
}
prop_compose! {
pub fn check_violation()(
severity in severity(),
kind in check_violation_kind(),
path in "[a-z]{1,8}",
) -> CheckViolation {
CheckViolation {
rule_id: "test/rule",
path: PathBuf::from(path),
severity,
kind,
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn render_includes_the_variant_name(kind in strategy::check_violation_kind()) {
let s = render(&kind);
prop_assert!(!s.is_empty());
}
}
}