use std::fmt;
use crate::common::assertion::EmptyTestViolation;
use crate::files::assertion::{
CustomFileViolation, CycleViolation, ExternalModuleDependencyViolation,
FileDependencyViolation, FilePatternViolation,
};
use crate::layers::assertion::LayerDependencyViolation;
use crate::metrics::assertion::{
CustomMetricViolation, MetricPredicateViolation, MetricThresholdViolation, MetricZoneViolation,
};
use crate::slices::assertion::SliceDependencyViolation;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum ViolationKind {
EmptyTest,
Cycle,
FilePattern,
FileDependency,
ExternalModuleDependency,
CustomFile,
LayerDependency,
SliceDependency,
MetricZone,
CustomMetric,
MetricThreshold,
MetricPredicate,
}
impl ViolationKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::EmptyTest => "empty-test",
Self::Cycle => "cycle",
Self::FilePattern => "file-pattern",
Self::FileDependency => "file-dependency",
Self::ExternalModuleDependency => "external-module-dependency",
Self::CustomFile => "custom-file",
Self::LayerDependency => "layer-dependency",
Self::SliceDependency => "slice-dependency",
Self::MetricZone => "metric-zone",
Self::CustomMetric => "custom-metric",
Self::MetricThreshold => "metric-threshold",
Self::MetricPredicate => "metric-predicate",
}
}
}
impl fmt::Display for ViolationKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Violation {
EmptyTest(EmptyTestViolation),
Cycle(CycleViolation),
FilePattern(FilePatternViolation),
FileDependency(FileDependencyViolation),
ExternalModuleDependency(ExternalModuleDependencyViolation),
CustomFile(CustomFileViolation),
LayerDependency(LayerDependencyViolation),
SliceDependency(SliceDependencyViolation),
MetricZone(MetricZoneViolation),
CustomMetric(CustomMetricViolation),
MetricThreshold(MetricThresholdViolation),
MetricPredicate(MetricPredicateViolation),
}
impl Violation {
#[must_use]
pub const fn kind(&self) -> ViolationKind {
match self {
Self::EmptyTest(_) => ViolationKind::EmptyTest,
Self::Cycle(_) => ViolationKind::Cycle,
Self::FilePattern(_) => ViolationKind::FilePattern,
Self::FileDependency(_) => ViolationKind::FileDependency,
Self::ExternalModuleDependency(_) => ViolationKind::ExternalModuleDependency,
Self::CustomFile(_) => ViolationKind::CustomFile,
Self::LayerDependency(_) => ViolationKind::LayerDependency,
Self::SliceDependency(_) => ViolationKind::SliceDependency,
Self::MetricZone(_) => ViolationKind::MetricZone,
Self::CustomMetric(_) => ViolationKind::CustomMetric,
Self::MetricThreshold(_) => ViolationKind::MetricThreshold,
Self::MetricPredicate(_) => ViolationKind::MetricPredicate,
}
}
#[must_use]
pub const fn as_empty_test(&self) -> Option<&EmptyTestViolation> {
match self {
Self::EmptyTest(violation) => Some(violation),
Self::Cycle(_)
| Self::FilePattern(_)
| Self::FileDependency(_)
| Self::ExternalModuleDependency(_)
| Self::CustomFile(_)
| Self::LayerDependency(_)
| Self::SliceDependency(_)
| Self::MetricZone(_)
| Self::CustomMetric(_)
| Self::MetricThreshold(_)
| Self::MetricPredicate(_) => None,
}
}
#[must_use]
pub const fn as_cycle(&self) -> Option<&CycleViolation> {
match self {
Self::Cycle(violation) => Some(violation),
Self::EmptyTest(_)
| Self::FilePattern(_)
| Self::FileDependency(_)
| Self::ExternalModuleDependency(_)
| Self::CustomFile(_)
| Self::LayerDependency(_)
| Self::SliceDependency(_)
| Self::MetricZone(_)
| Self::CustomMetric(_)
| Self::MetricThreshold(_)
| Self::MetricPredicate(_) => None,
}
}
#[must_use]
pub const fn as_file_pattern(&self) -> Option<&FilePatternViolation> {
match self {
Self::FilePattern(violation) => Some(violation),
Self::EmptyTest(_)
| Self::Cycle(_)
| Self::FileDependency(_)
| Self::ExternalModuleDependency(_)
| Self::CustomFile(_)
| Self::LayerDependency(_)
| Self::SliceDependency(_)
| Self::MetricZone(_)
| Self::CustomMetric(_)
| Self::MetricThreshold(_)
| Self::MetricPredicate(_) => None,
}
}
#[must_use]
pub const fn as_file_dependency(&self) -> Option<&FileDependencyViolation> {
match self {
Self::FileDependency(violation) => Some(violation),
Self::EmptyTest(_)
| Self::Cycle(_)
| Self::FilePattern(_)
| Self::ExternalModuleDependency(_)
| Self::CustomFile(_)
| Self::LayerDependency(_)
| Self::SliceDependency(_)
| Self::MetricZone(_)
| Self::CustomMetric(_)
| Self::MetricThreshold(_)
| Self::MetricPredicate(_) => None,
}
}
#[must_use]
pub const fn as_external_module_dependency(
&self,
) -> Option<&ExternalModuleDependencyViolation> {
match self {
Self::ExternalModuleDependency(violation) => Some(violation),
Self::EmptyTest(_)
| Self::Cycle(_)
| Self::FilePattern(_)
| Self::FileDependency(_)
| Self::CustomFile(_)
| Self::LayerDependency(_)
| Self::SliceDependency(_)
| Self::MetricZone(_)
| Self::CustomMetric(_)
| Self::MetricThreshold(_)
| Self::MetricPredicate(_) => None,
}
}
#[must_use]
pub const fn as_custom_file(&self) -> Option<&CustomFileViolation> {
match self {
Self::CustomFile(violation) => Some(violation),
Self::EmptyTest(_)
| Self::Cycle(_)
| Self::FilePattern(_)
| Self::FileDependency(_)
| Self::ExternalModuleDependency(_)
| Self::LayerDependency(_)
| Self::SliceDependency(_)
| Self::MetricZone(_)
| Self::CustomMetric(_)
| Self::MetricThreshold(_)
| Self::MetricPredicate(_) => None,
}
}
#[must_use]
pub const fn as_layer_dependency(&self) -> Option<&LayerDependencyViolation> {
match self {
Self::LayerDependency(violation) => Some(violation),
Self::EmptyTest(_)
| Self::Cycle(_)
| Self::FilePattern(_)
| Self::FileDependency(_)
| Self::ExternalModuleDependency(_)
| Self::CustomFile(_)
| Self::SliceDependency(_)
| Self::MetricZone(_)
| Self::CustomMetric(_)
| Self::MetricThreshold(_)
| Self::MetricPredicate(_) => None,
}
}
#[must_use]
pub const fn as_slice_dependency(&self) -> Option<&SliceDependencyViolation> {
match self {
Self::SliceDependency(violation) => Some(violation),
Self::EmptyTest(_)
| Self::Cycle(_)
| Self::FilePattern(_)
| Self::FileDependency(_)
| Self::ExternalModuleDependency(_)
| Self::CustomFile(_)
| Self::LayerDependency(_)
| Self::MetricZone(_)
| Self::CustomMetric(_)
| Self::MetricThreshold(_)
| Self::MetricPredicate(_) => None,
}
}
#[must_use]
pub const fn as_metric_zone(&self) -> Option<&MetricZoneViolation> {
match self {
Self::MetricZone(violation) => Some(violation),
Self::EmptyTest(_)
| Self::Cycle(_)
| Self::FilePattern(_)
| Self::FileDependency(_)
| Self::ExternalModuleDependency(_)
| Self::CustomFile(_)
| Self::LayerDependency(_)
| Self::SliceDependency(_)
| Self::CustomMetric(_)
| Self::MetricThreshold(_)
| Self::MetricPredicate(_) => None,
}
}
#[must_use]
pub const fn as_custom_metric(&self) -> Option<&CustomMetricViolation> {
match self {
Self::CustomMetric(violation) => Some(violation),
Self::EmptyTest(_)
| Self::Cycle(_)
| Self::FilePattern(_)
| Self::FileDependency(_)
| Self::ExternalModuleDependency(_)
| Self::CustomFile(_)
| Self::LayerDependency(_)
| Self::SliceDependency(_)
| Self::MetricZone(_)
| Self::MetricThreshold(_)
| Self::MetricPredicate(_) => None,
}
}
#[must_use]
pub const fn as_metric_threshold(&self) -> Option<&MetricThresholdViolation> {
match self {
Self::MetricThreshold(violation) => Some(violation),
Self::EmptyTest(_)
| Self::Cycle(_)
| Self::FilePattern(_)
| Self::FileDependency(_)
| Self::ExternalModuleDependency(_)
| Self::CustomFile(_)
| Self::LayerDependency(_)
| Self::SliceDependency(_)
| Self::MetricZone(_)
| Self::CustomMetric(_)
| Self::MetricPredicate(_) => None,
}
}
#[must_use]
pub const fn as_metric_predicate(&self) -> Option<&MetricPredicateViolation> {
match self {
Self::MetricPredicate(violation) => Some(violation),
Self::EmptyTest(_)
| Self::Cycle(_)
| Self::FilePattern(_)
| Self::FileDependency(_)
| Self::ExternalModuleDependency(_)
| Self::CustomFile(_)
| Self::LayerDependency(_)
| Self::SliceDependency(_)
| Self::MetricZone(_)
| Self::CustomMetric(_)
| Self::MetricThreshold(_) => None,
}
}
}
impl From<EmptyTestViolation> for Violation {
fn from(violation: EmptyTestViolation) -> Self {
Self::EmptyTest(violation)
}
}
impl From<CycleViolation> for Violation {
fn from(violation: CycleViolation) -> Self {
Self::Cycle(violation)
}
}
impl From<FilePatternViolation> for Violation {
fn from(violation: FilePatternViolation) -> Self {
Self::FilePattern(violation)
}
}
impl From<FileDependencyViolation> for Violation {
fn from(violation: FileDependencyViolation) -> Self {
Self::FileDependency(violation)
}
}
impl From<ExternalModuleDependencyViolation> for Violation {
fn from(violation: ExternalModuleDependencyViolation) -> Self {
Self::ExternalModuleDependency(violation)
}
}
impl From<CustomFileViolation> for Violation {
fn from(violation: CustomFileViolation) -> Self {
Self::CustomFile(violation)
}
}
impl From<LayerDependencyViolation> for Violation {
fn from(violation: LayerDependencyViolation) -> Self {
Self::LayerDependency(violation)
}
}
impl From<SliceDependencyViolation> for Violation {
fn from(violation: SliceDependencyViolation) -> Self {
Self::SliceDependency(violation)
}
}
impl From<MetricZoneViolation> for Violation {
fn from(violation: MetricZoneViolation) -> Self {
Self::MetricZone(violation)
}
}
impl From<CustomMetricViolation> for Violation {
fn from(violation: CustomMetricViolation) -> Self {
Self::CustomMetric(violation)
}
}
impl From<MetricThresholdViolation> for Violation {
fn from(violation: MetricThresholdViolation) -> Self {
Self::MetricThreshold(violation)
}
}
impl From<MetricPredicateViolation> for Violation {
fn from(violation: MetricPredicateViolation) -> Self {
Self::MetricPredicate(violation)
}
}
#[cfg(test)]
mod tests {
use super::{Violation, ViolationKind};
use crate::{
common::{
Edge, EmptyTestViolation, Graph, ImportKind, ProjectedEdge, RegexFactory,
project_to_nodes,
},
files::{
CustomFileViolation, CycleViolation, ExternalModuleDependencyViolation,
FileDependencyViolation, FileInfo, FilePatternViolation,
},
layers::{LayerDependencyRule, LayerDependencyViolation},
slices::{SliceDependencyRule, SliceDependencyViolation},
};
#[test]
fn empty_test_has_a_stable_kind() {
let violation = Violation::from(EmptyTestViolation::new("files", []));
assert_eq!(violation.kind(), ViolationKind::EmptyTest);
assert_eq!(violation.kind().as_str(), "empty-test");
assert_eq!(violation.kind().to_string(), "empty-test");
}
#[test]
fn exposes_typed_data_without_formatting_it() {
let violation = Violation::from(EmptyTestViolation::new("slices", []));
let empty = violation
.as_empty_test()
.expect("fixture should be an empty-test violation");
assert_eq!(empty.subject, "slices");
assert!(empty.selectors.is_empty());
}
#[test]
fn cycle_has_a_stable_kind_and_typed_accessor() {
let violation = Violation::from(CycleViolation::new(Vec::<ProjectedEdge>::new()));
assert_eq!(violation.kind(), ViolationKind::Cycle);
assert_eq!(violation.kind().as_str(), "cycle");
assert!(violation.as_cycle().is_some());
assert!(violation.as_empty_test().is_none());
assert!(violation.as_file_pattern().is_none());
assert!(violation.as_file_dependency().is_none());
assert!(violation.as_external_module_dependency().is_none());
assert!(violation.as_custom_file().is_none());
}
#[test]
fn file_pattern_has_a_stable_kind_and_typed_accessor() {
let filter = RegexFactory::default()
.filename_matcher("*.rs")
.expect("fixture pattern should compile");
let node = project_to_nodes(&Graph::from_edges([Edge::self_edge("src/lib.rs")]))
.into_iter()
.next()
.expect("fixture graph should project one node");
let violation = Violation::from(FilePatternViolation::new(filter, node, false));
assert_eq!(violation.kind(), ViolationKind::FilePattern);
assert_eq!(violation.kind().as_str(), "file-pattern");
assert!(violation.as_file_pattern().is_some());
assert!(violation.as_cycle().is_none());
assert!(violation.as_empty_test().is_none());
assert!(violation.as_file_dependency().is_none());
assert!(violation.as_external_module_dependency().is_none());
assert!(violation.as_custom_file().is_none());
}
#[test]
fn file_dependency_has_a_stable_kind_and_typed_accessor() {
let raw = Edge::new("src/api.rs", "src/db.rs", false, [ImportKind::Use]);
let edge = ProjectedEdge::new("src/api.rs", "src/db.rs", [raw]);
let violation = Violation::from(FileDependencyViolation::new(edge, true));
assert_eq!(violation.kind(), ViolationKind::FileDependency);
assert_eq!(violation.kind().as_str(), "file-dependency");
assert!(violation.as_file_dependency().is_some());
assert!(violation.as_file_pattern().is_none());
assert!(violation.as_cycle().is_none());
assert!(violation.as_empty_test().is_none());
assert!(violation.as_external_module_dependency().is_none());
assert!(violation.as_custom_file().is_none());
}
#[test]
fn external_module_dependency_has_a_stable_kind_and_typed_accessor() {
let raw = Edge::new("src/api.rs", "tokio", true, [ImportKind::MacroReference]);
let edge = ProjectedEdge::new("src/api.rs", "tokio", [raw]);
let violation = Violation::from(ExternalModuleDependencyViolation::new(edge, false));
assert_eq!(violation.kind(), ViolationKind::ExternalModuleDependency);
assert_eq!(violation.kind().as_str(), "external-module-dependency");
assert!(violation.as_external_module_dependency().is_some());
assert!(violation.as_file_dependency().is_none());
assert!(violation.as_file_pattern().is_none());
assert!(violation.as_cycle().is_none());
assert!(violation.as_empty_test().is_none());
assert!(violation.as_custom_file().is_none());
}
#[test]
fn custom_file_has_a_stable_kind_and_typed_accessor() {
let info = FileInfo::new("src/api.rs", "pub fn api() {}\n");
let violation = Violation::from(CustomFileViolation::new(
info,
"contain no public functions",
true,
));
assert_eq!(violation.kind(), ViolationKind::CustomFile);
assert_eq!(violation.kind().as_str(), "custom-file");
assert!(violation.as_custom_file().is_some());
assert!(violation.as_external_module_dependency().is_none());
assert!(violation.as_file_dependency().is_none());
assert!(violation.as_file_pattern().is_none());
assert!(violation.as_cycle().is_none());
assert!(violation.as_empty_test().is_none());
assert!(violation.as_layer_dependency().is_none());
}
#[test]
fn layer_dependency_has_a_stable_kind_and_typed_accessor() {
let raw = Edge::new("src/api.rs", "src/db.rs", false, [ImportKind::Use]);
let dependency = ProjectedEdge::new("src/api.rs", "src/db.rs", [raw]);
let violation = Violation::from(LayerDependencyViolation::new(
dependency,
"api",
"database",
LayerDependencyRule::MayOnlyDependOnLayers,
));
assert_eq!(violation.kind(), ViolationKind::LayerDependency);
assert_eq!(violation.kind().as_str(), "layer-dependency");
assert!(violation.as_layer_dependency().is_some());
assert!(violation.as_custom_file().is_none());
assert!(violation.as_external_module_dependency().is_none());
assert!(violation.as_file_dependency().is_none());
assert!(violation.as_file_pattern().is_none());
assert!(violation.as_cycle().is_none());
assert!(violation.as_empty_test().is_none());
}
#[test]
fn slice_dependency_has_a_stable_kind_and_typed_accessor() {
let raw = Edge::new("src/api.rs", "src/db.rs", false, [ImportKind::Use]);
let dependency = ProjectedEdge::new("api", "database", [raw]);
let violation = Violation::from(SliceDependencyViolation::new(
dependency,
"api",
"database",
SliceDependencyRule::ContainDependency,
true,
));
assert_eq!(violation.kind(), ViolationKind::SliceDependency);
assert_eq!(violation.kind().as_str(), "slice-dependency");
assert!(violation.as_slice_dependency().is_some());
assert!(violation.as_layer_dependency().is_none());
assert!(violation.as_empty_test().is_none());
}
}