1use anyhow::Context;
2use std::{env, path::PathBuf, process::ExitCode};
3
4pub mod dependency_graph;
5pub mod dependency_rule;
6pub mod metadata;
7
8#[derive(Debug, Clone)]
9pub enum ReturnStatus {
10 NoViolation,
11 Violation,
12}
13impl ReturnStatus {
14 pub fn to_return_code(&self) -> ExitCode {
15 match self {
16 ReturnStatus::NoViolation => ExitCode::SUCCESS,
17 ReturnStatus::Violation => ExitCode::FAILURE,
18 }
19 }
20}
21
22pub struct HandlerConfig {
23 pub graph_build_configs: dependency_graph::DependencyGraphBuildConfigs,
24 pub metadata_configs: metadata::CollectMetadataConfig,
25 pub tree_config: dependency_graph::tree::TreePrintConfig,
26 pub rules_path: Option<PathBuf>,
27}
28
29pub fn handler(config: HandlerConfig) -> anyhow::Result<ReturnStatus> {
30 tracing::info!("collecting cargo metadata");
31 let metadata = metadata::collect_metadata(config.metadata_configs.clone())?;
32
33 tracing::info!("building dependency graph");
34 let graph = dependency_graph::build_dependency_graph(&metadata, config.graph_build_configs)?;
35
36 let rules_path = match config.rules_path {
37 Some(path) => path,
38 None => {
39 let manifest_path = match config.metadata_configs.manifest_path {
40 Some(path) => PathBuf::from(path),
41 None => env::current_dir()?.join("Cargo.toml"),
42 };
43 let rules_dir = manifest_path
44 .parent()
45 .ok_or_else(|| anyhow::anyhow!("manifest path has no parent directory"))?;
46 rules_dir.join("dependency_rules.toml")
47 }
48 };
49 tracing::info!(path = ?rules_path, "loading dependency rules");
50 let rules = dependency_rule::DependencyRules::from_file(&rules_path).with_context(|| {
51 format!(
52 "failed to load dependency rules from '{}'",
53 rules_path.display()
54 )
55 })?;
56
57 tracing::info!("checking violations");
58 let report = dependency_graph::violation::check_violations(&graph, &rules);
59
60 tracing::info!("printing dependency tree");
61 dependency_graph::tree::print(
62 &mut std::io::stdout(),
63 &graph,
64 &metadata,
65 &report,
66 config.tree_config,
67 )?;
68
69 if report.has_violations() {
70 Ok(ReturnStatus::Violation)
71 } else {
72 Ok(ReturnStatus::NoViolation)
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79 use crate::{
80 dependency_graph::{DependencyGraphBuildConfigs, tree::TreePrintConfig},
81 metadata::CollectMetadataConfig,
82 };
83 use anyhow::Result;
84
85 fn handler_config(manifest_path: &str) -> HandlerConfig {
86 HandlerConfig {
87 graph_build_configs: DependencyGraphBuildConfigs::default(),
88 metadata_configs: CollectMetadataConfig {
89 manifest_path: Some(manifest_path.to_string()),
90 ..CollectMetadataConfig::default()
91 },
92 tree_config: TreePrintConfig::default(),
93 rules_path: None,
94 }
95 }
96
97 #[test]
98 #[ignore]
99 fn test_main() -> Result<()> {
100 let config = handler_config("tests/demo_crates/clean-arch/Cargo.toml");
101 let _ = handler(config)?;
102 Ok(())
103 }
104
105 #[test]
106 fn test_handler_success() -> Result<()> {
107 let config = handler_config("tests/demo_crates/clean-arch/Cargo.toml");
108 let result = handler(config)?;
109 assert_eq!(result.to_return_code(), ExitCode::SUCCESS);
110 Ok(())
111 }
112
113 #[test]
114 fn test_handler_failure() -> Result<()> {
115 let config = handler_config("tests/demo_crates/tangled-clean-arch/Cargo.toml");
116 let result = handler(config)?;
117 assert_eq!(result.to_return_code(), ExitCode::FAILURE);
118 Ok(())
119 }
120}