Skip to content

Commit

Permalink
Exposes rules for to userland (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
pfazzi authored Jan 16, 2025
1 parent b202020 commit 824f090
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 27 deletions.
55 changes: 53 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use rust_arkitect::dsl::{ArchitecturalRules, Arkitect, Project};

#[test]
fn test_architectural_rules() {
let project = Project::from_relative_path(file!(), "./../");
let project = Project::new();

let rules = ArchitecturalRules::define()
.component("Domain")
Expand Down Expand Up @@ -70,7 +70,7 @@ You can define and test architectural rules:
```rust
#[test]
fn test_architecture_baseline() {
let project = Project::from_relative_path(file!(), "./../");
let project = Project::new();

let rules = ArchitecturalRules::define()
.component("Application")
Expand Down Expand Up @@ -130,6 +130,57 @@ Example Output:
[2024-12-30T12:17:08Z ERROR rust_arkitect::dsl] 🟥 Rule my_project::utils may not depend on any modules violated: forbidden dependencies to [my_project::infrastructure::redis::*] in file:///users/random/projects/acme_project/src/utils/refill.rs
```

# 🧙‍♂️ Custom Rules
Rust Arkitect allows you to create custom rules to test your project's architecture. These rules can be implemented by creating a struct and implementing the `Rule` trait for it. Below is an example of how to define and use a custom rule in a test:

```rust
use rust_arkitect::dsl::Arkitect;
use rust_arkitect::dsl::Project;
use rust_arkitect::rules::Rule;
use std::fmt::{Display, Formatter};

// Define a custom rule
struct TestRule;

impl TestRule {
fn new(_subject: &str, _dependencies: &[&str; 1]) -> TestRule {
Self {}
}
}

// Implement Display for the rule for better readability in logs
impl Display for TestRule {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "TestRule applied")
}
}

// Implement the Rule trait
impl Rule for TestRule {
fn apply(&self, _file: &str) -> Result<(), String> {
Ok(())
}

fn is_applicable(&self, _file: &str) -> bool {
true
}
}

#[test]
fn test_custom_rule_execution() {
// Define the project path
let project = Project::new();

// Create a new instance of the custom rule
let rule = Box::new(TestRule::new("my_crate", &["a:crate::a_module"]));

// Apply the rule to the project
let result = Arkitect::ensure_that(project).complies_with(vec![rule]);

// Assert that the rule passed
assert!(result.is_ok());
}
```
# 😇 Built with Its Own Rules

Rust Arkitect is built and tested using the same architectural rules it enforces. This ensures the tool remains consistent with the principles it promotes. You can explore the [architecture tests here](tests/test_architecture.rs) to see it in action.
Expand Down
10 changes: 5 additions & 5 deletions examples/sample_project/src/architecture_rules_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@ fn test_vertical_slices_architecture_rules() {

.finalize();

let project = Project::from_relative_path(file!(), "./../src");
let project = Project::from_relative_path(file!(), "./../");

let result = Arkitect::ensure_that(project).complies_with(rules);

assert_eq!(result, Ok(()))
assert!(result.is_ok());
}

#[test]
fn test_mvc_architecture_rules() {
Arkitect::init_logger();

let project = Project::from_relative_path(file!(), "./../src");
let project = Project::from_relative_path(file!(), "./../");

#[rustfmt::skip]
let rules = ArchitecturalRules::define()
Expand All @@ -50,14 +50,14 @@ fn test_mvc_architecture_rules() {

let result = Arkitect::ensure_that(project).complies_with(rules);

assert_eq!(result, Ok(()))
assert!(result.is_ok())
}

#[test]
fn test_three_tier_architecture() {
Arkitect::init_logger();

let project = Project::from_relative_path(file!(), "./../src");
let project = Project::from_relative_path(file!(), "./../");

#[rustfmt::skip]
let rules = ArchitecturalRules::define()
Expand Down
3 changes: 3 additions & 0 deletions examples/sample_project/src/contracts/external_services.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub fn service_call_one() {
println!("Service call one");
}
5 changes: 5 additions & 0 deletions examples/sample_project/src/contracts/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub mod external_services;

trait ContractOne{
fn do_stuff() -> String;
}
16 changes: 5 additions & 11 deletions examples/workspace_project/tests/src/architecture_rules_test.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
use rust_arkitect::dsl::{ArchitecturalRules, Arkitect, Project};

fn project() -> Project {
Project::from_absolute_path(
"/Users/patrickfazzi/Projects/rust_arkitect/examples/workspace_project",
)
}

#[test]
fn test_vertical_slices_architecture_rules() {
Arkitect::init_logger();
Expand All @@ -30,18 +24,18 @@ fn test_vertical_slices_architecture_rules() {

.finalize();

let project = project();
let project = Project::new();

let result = Arkitect::ensure_that(project).complies_with(rules);

assert_eq!(result, Ok(()))
assert!(result.is_ok());
}

#[test]
fn test_mvc_architecture_rules() {
Arkitect::init_logger();

let project = project();
let project = Project::new();

#[rustfmt::skip]
let rules = ArchitecturalRules::define()
Expand All @@ -60,14 +54,14 @@ fn test_mvc_architecture_rules() {

let result = Arkitect::ensure_that(project).complies_with(rules);

assert_eq!(result, Ok(()))
assert!(result.is_ok());
}

#[test]
fn test_three_tier_architecture() {
Arkitect::init_logger();

let project = project();
let project =Project::new();

#[rustfmt::skip]
let rules = ArchitecturalRules::define()
Expand Down
30 changes: 23 additions & 7 deletions src/dsl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,54 @@ use crate::rules::may_depend_on::MayDependOnRule;
use crate::rules::must_not_depend_on_anything::MustNotDependOnAnythingRule;
use crate::rules::rule::Rule;
use std::collections::HashMap;
use std::env;
use std::marker::PhantomData;
use std::path::Path;

pub struct Project {
pub(crate) absolute_path: String,
pub project_root: String,
}

impl Project {
pub fn from_absolute_path(absolute_path: &str) -> Project {
pub fn from_path(absolute_path: &str) -> Project {
Project {
absolute_path: absolute_path.to_string(),
project_root: absolute_path.to_string(),
}
}

pub fn new() -> Project {
let cargo_manifest_dir =
env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is not set");

Project {
project_root: cargo_manifest_dir,
}
}

/// Creates a Project from a path relative to the given file.
pub fn from_relative_path(current_file: &str, relative_path: &str) -> Project {
let current_dir = Path::new(current_file)
.parent()
.expect("Failed to get parent directory");

let derived_path = current_dir.join(relative_path);

let absolute_path = derived_path.canonicalize().unwrap_or_else(|e| {
panic!(
"Failed to resolve absolute path. Derived path: '{}', from current file: '{}' and relative path: '{}'. Error: {}",
derived_path.display(),
"Failed to resolve absolute path:\n\
- Current file: '{}'\n\
- Relative path: '{}'\n\
- Derived path (before resolving): '{}'\n\
Cause: {}",
current_file,
relative_path,
derived_path.display(),
e
)
});

Project {
absolute_path: absolute_path
project_root: absolute_path
.to_str()
.expect("Failed to convert path to string")
.to_string(),
Expand All @@ -58,7 +74,7 @@ impl Arkitect {

pub fn complies_with(&mut self, rules: Vec<Box<dyn Rule>>) -> Result<Vec<String>, Vec<String>> {
let violations =
Engine::new(self.project.absolute_path.as_str(), rules.as_slice()).get_violations();
Engine::new(self.project.project_root.as_str(), rules.as_slice()).get_violations();

if violations.len() <= self.baseline {
Ok(violations)
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
mod dependency_parsing;
pub mod dsl;
mod engine;
mod rules;
pub mod rules;
9 changes: 9 additions & 0 deletions src/rules/must_not_depend_on.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ pub struct MustNotDependOnRule {
pub(crate) forbidden_dependencies: Vec<String>,
}

impl MustNotDependOnRule {
pub fn new(subject: String, forbidden_dependencies: Vec<String>) -> Self {
Self {
subject,
forbidden_dependencies,
}
}
}

impl Display for MustNotDependOnRule {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let bold = Style::new().bold().fg(RGB(255, 165, 0));
Expand Down
2 changes: 1 addition & 1 deletion tests/test_architecture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use rust_arkitect::dsl::{ArchitecturalRules, Arkitect, Project};
fn test_architectural_rules() {
Arkitect::init_logger();

let project = Project::from_relative_path(file!(), "./../");
let project = Project::new();

#[rustfmt::skip]
let rules = ArchitecturalRules::define()
Expand Down
56 changes: 56 additions & 0 deletions tests/test_standalone_rules_usage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#![cfg(test)]

use rust_arkitect::dsl::Arkitect;
use rust_arkitect::dsl::Project;
use rust_arkitect::rules::must_not_depend_on::MustNotDependOnRule;
use rust_arkitect::rules::rule::Rule;
use std::fmt::{Display, Formatter};

struct TestRule;

impl TestRule {
fn new(_subject: &str, _dependencies: &[&str; 1]) -> TestRule {
Self {}
}
}

impl Display for TestRule {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "TestRule applied")
}
}

impl Rule for TestRule {
fn apply(&self, _file: &str) -> Result<(), String> {
Ok(())
}

fn is_applicable(&self, _file: &str) -> bool {
true
}
}

#[test]
fn test_custom_rule_execution() {
let project = Project::new();

let rule = Box::new(TestRule::new("my_crate", &["a:crate::a_module"]));

let result = Arkitect::ensure_that(project).complies_with(vec![rule]);

assert!(result.is_ok());
}

#[test]
fn test_may_depend_on_standalone() {
let project = Project::new();

let rule = Box::new(MustNotDependOnRule::new(
"conversion::domain".to_string(),
vec!["a:crate::a_module".to_string()],
));

let result = Arkitect::ensure_that(project).complies_with(vec![rule]);

assert!(result.is_ok());
}

0 comments on commit 824f090

Please sign in to comment.