-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
346 additions
and
164 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,296 @@ | ||
use crate::contracts::{Contract, ContractViolation, ForbiddenImport}; | ||
use crate::{ImportsInfo, InternalImportsPathQuery, PackageItemToken, PackageItemTokens}; | ||
use anyhow::Result; | ||
use maplit::hashset; | ||
use std::collections::HashSet; | ||
|
||
#[derive(Debug, Clone)] | ||
pub struct LayeredArchitectureContract { | ||
layers: Vec<Layer>, | ||
} | ||
|
||
impl LayeredArchitectureContract { | ||
pub fn new(layers: &[Layer]) -> Self { | ||
LayeredArchitectureContract { | ||
layers: layers.to_vec(), | ||
} | ||
} | ||
|
||
pub fn layers(&self) -> &[Layer] { | ||
&self.layers | ||
} | ||
} | ||
|
||
impl Contract for LayeredArchitectureContract { | ||
fn find_violations(&self, imports_info: &ImportsInfo) -> Result<Vec<ContractViolation>> { | ||
let mut violations: Vec<ContractViolation> = Vec::new(); | ||
|
||
let forbidden_imports = get_forbidden_imports(&self.layers); | ||
for forbidden_import in forbidden_imports { | ||
let from: HashSet<PackageItemToken> = forbidden_import.from.into(); | ||
let from = from.extend_with_descendants(imports_info.package_info()); | ||
let to: HashSet<PackageItemToken> = forbidden_import.to.into(); | ||
let to = to.extend_with_descendants(imports_info.package_info()); | ||
let except_via = forbidden_import | ||
.except_via | ||
.clone() | ||
.extend_with_descendants(imports_info.package_info()); | ||
|
||
let path = imports_info.internal_imports().find_path( | ||
&InternalImportsPathQuery::new() | ||
.from(from) | ||
.to(to) | ||
.excluding_paths_via(except_via), | ||
)?; | ||
match path { | ||
Some(path) => { | ||
violations.push(ContractViolation::ForbiddenImport { | ||
forbidden_import, | ||
path, | ||
}); | ||
} | ||
None => {} | ||
} | ||
} | ||
|
||
Ok(violations) | ||
} | ||
} | ||
|
||
#[derive(Debug, Clone)] | ||
pub struct Layer { | ||
siblings: HashSet<PackageItemToken>, | ||
siblings_independent: bool, | ||
} | ||
|
||
impl Layer { | ||
pub fn new<T: IntoIterator<Item = PackageItemToken>>( | ||
siblings: T, | ||
siblings_independent: bool, | ||
) -> Self { | ||
Layer { | ||
siblings: siblings.into_iter().collect(), | ||
siblings_independent, | ||
} | ||
} | ||
|
||
pub fn siblings(&self) -> &HashSet<PackageItemToken> { | ||
&self.siblings | ||
} | ||
|
||
pub fn siblings_independent(&self) -> bool { | ||
self.siblings_independent | ||
} | ||
} | ||
|
||
fn get_forbidden_imports(layers: &[Layer]) -> Vec<ForbiddenImport> { | ||
let mut forbidden_imports = Vec::new(); | ||
|
||
for (idx, layer) in layers.iter().enumerate() { | ||
for higher_layer in layers[idx + 1..].iter() { | ||
for layer_sibling in layer.siblings.iter() { | ||
for higher_layer_sibling in higher_layer.siblings.iter() { | ||
forbidden_imports.push(ForbiddenImport::new( | ||
*layer_sibling, | ||
*higher_layer_sibling, | ||
hashset! {}, | ||
)); | ||
} | ||
} | ||
} | ||
|
||
if idx >= 2 { | ||
let directly_lower_layer = &layers[idx - 1]; | ||
for lower_layer in layers[..idx - 1].iter() { | ||
for layer_sibling in layer.siblings.iter() { | ||
for lower_layer_sibling in lower_layer.siblings.iter() { | ||
forbidden_imports.push(ForbiddenImport::new( | ||
*layer_sibling, | ||
*lower_layer_sibling, | ||
directly_lower_layer.siblings.clone(), | ||
)); | ||
} | ||
} | ||
} | ||
} | ||
|
||
if layer.siblings_independent { | ||
for layer_sibling1 in layer.siblings.iter() { | ||
for layer_sibling2 in layer.siblings.iter() { | ||
if layer_sibling1 == layer_sibling2 { | ||
continue; | ||
} | ||
forbidden_imports.push(ForbiddenImport::new( | ||
*layer_sibling1, | ||
*layer_sibling2, | ||
hashset! {}, | ||
)); | ||
} | ||
} | ||
} | ||
} | ||
|
||
forbidden_imports | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
use crate::{testpackage, PackageInfo, PackageToken, TestPackage}; | ||
use anyhow::Result; | ||
use pretty_assertions::assert_eq; | ||
use slotmap::SlotMap; | ||
|
||
#[test] | ||
fn test_get_forbidden_imports() -> Result<()> { | ||
let mut sm: SlotMap<PackageToken, String> = SlotMap::with_key(); | ||
let data: PackageItemToken = sm.insert("data".into()).into(); | ||
let domain1: PackageItemToken = sm.insert("domain1".into()).into(); | ||
let domain2: PackageItemToken = sm.insert("domain2".into()).into(); | ||
let application1: PackageItemToken = sm.insert("application1".into()).into(); | ||
let application2: PackageItemToken = sm.insert("application2".into()).into(); | ||
let interfaces: PackageItemToken = sm.insert("interfaces".into()).into(); | ||
|
||
let layers = vec![ | ||
Layer::new([data], true), | ||
Layer::new([domain1, domain2], true), | ||
Layer::new([application1, application2], false), | ||
Layer::new([interfaces], true), | ||
]; | ||
|
||
let forbidden_imports = get_forbidden_imports(&layers); | ||
|
||
let expected = vec![ | ||
// data may not import domain, application or interfaces | ||
ForbiddenImport::new(data, domain1, hashset! {}), | ||
ForbiddenImport::new(data, domain2, hashset! {}), | ||
ForbiddenImport::new(data, application1, hashset! {}), | ||
ForbiddenImport::new(data, application2, hashset! {}), | ||
ForbiddenImport::new(data, interfaces, hashset! {}), | ||
// domain may not import application or interfaces | ||
// (domain may import data) | ||
ForbiddenImport::new(domain1, application1, hashset! {}), | ||
ForbiddenImport::new(domain1, application2, hashset! {}), | ||
ForbiddenImport::new(domain1, interfaces, hashset! {}), | ||
ForbiddenImport::new(domain2, application1, hashset! {}), | ||
ForbiddenImport::new(domain2, application2, hashset! {}), | ||
ForbiddenImport::new(domain2, interfaces, hashset! {}), | ||
// domain1 and domain2 are independent siblings | ||
ForbiddenImport::new(domain1, domain2, hashset! {}), | ||
ForbiddenImport::new(domain2, domain1, hashset! {}), | ||
// application may not import interfaces | ||
// application may not import data, except via domain | ||
// (application may import domain) | ||
ForbiddenImport::new(application1, interfaces, hashset! {}), | ||
ForbiddenImport::new(application1, data, hashset! {domain1, domain2}), | ||
ForbiddenImport::new(application2, interfaces, hashset! {}), | ||
ForbiddenImport::new(application2, data, hashset! {domain1, domain2}), | ||
// interfaces may not import data or domain, except via application | ||
// (application may import application) | ||
ForbiddenImport::new(interfaces, data, hashset! {application1, application2}), | ||
ForbiddenImport::new(interfaces, domain1, hashset! {application1, application2}), | ||
ForbiddenImport::new(interfaces, domain2, hashset! {application1, application2}), | ||
]; | ||
|
||
assert_eq!(forbidden_imports.len(), expected.len(),); | ||
for forbidden_import in forbidden_imports.iter() { | ||
assert!(expected.contains(forbidden_import)); | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
#[test] | ||
fn test_layered_architecture_contract_ok() -> Result<()> { | ||
let testpackage = testpackage! { | ||
"__init__.py" => "", | ||
"data.py" => "", | ||
"domain.py" => " | ||
import testpackage.data | ||
", | ||
"application.py" => " | ||
import testpackage.domain | ||
", | ||
"interfaces.py" => " | ||
import testpackage.application | ||
" | ||
}; | ||
|
||
let package_info = PackageInfo::build(testpackage.path())?; | ||
let imports_info = ImportsInfo::build(package_info)?; | ||
|
||
let data = imports_info.package_info()._item("testpackage.data"); | ||
let domain = imports_info.package_info()._item("testpackage.domain"); | ||
let application = imports_info.package_info()._item("testpackage.application"); | ||
let interfaces = imports_info.package_info()._item("testpackage.interfaces"); | ||
|
||
let contract = LayeredArchitectureContract::new(&[ | ||
Layer::new([data], true), | ||
Layer::new([domain], true), | ||
Layer::new([application], true), | ||
Layer::new([interfaces], true), | ||
]); | ||
|
||
let violations = contract.find_violations(&imports_info)?; | ||
assert!(violations.is_empty()); | ||
|
||
Ok(()) | ||
} | ||
|
||
#[test] | ||
fn test_layered_architecture_contract_violated() -> Result<()> { | ||
let testpackage = testpackage! { | ||
"__init__.py" => "", | ||
"data.py" => "", | ||
"domain.py" => " | ||
import testpackage.data | ||
", | ||
"application.py" => " | ||
import testpackage.domain | ||
import testpackage.interfaces | ||
", | ||
"interfaces.py" => " | ||
import testpackage.application | ||
import testpackage.data | ||
" | ||
}; | ||
|
||
let package_info = PackageInfo::build(testpackage.path())?; | ||
let imports_info = ImportsInfo::build(package_info)?; | ||
|
||
let data = imports_info.package_info()._item("testpackage.data"); | ||
let domain = imports_info.package_info()._item("testpackage.domain"); | ||
let application = imports_info.package_info()._item("testpackage.application"); | ||
let interfaces = imports_info.package_info()._item("testpackage.interfaces"); | ||
|
||
let contract = LayeredArchitectureContract::new(&[ | ||
Layer::new([data], true), | ||
Layer::new([domain], true), | ||
Layer::new([application], true), | ||
Layer::new([interfaces], true), | ||
]); | ||
|
||
let violations = contract.find_violations(&imports_info)?; | ||
|
||
let expected_violations = vec![ | ||
ContractViolation::ForbiddenImport { | ||
forbidden_import: ForbiddenImport::new(application, interfaces, hashset! {}), | ||
path: vec![application, interfaces], | ||
}, | ||
ContractViolation::ForbiddenImport { | ||
forbidden_import: ForbiddenImport::new(interfaces, data, hashset! {application}), | ||
path: vec![interfaces, data], | ||
}, | ||
ContractViolation::ForbiddenImport { | ||
forbidden_import: ForbiddenImport::new(application, data, hashset! {domain}), | ||
path: vec![application, interfaces, data], | ||
}, | ||
]; | ||
assert_eq!(violations.len(), expected_violations.len()); | ||
for violation in violations.iter() { | ||
assert!(expected_violations.contains(violation)); | ||
} | ||
|
||
Ok(()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
use crate::{ImportsInfo, PackageItemToken}; | ||
use anyhow::Result; | ||
use std::collections::HashSet; | ||
|
||
pub mod layers; | ||
|
||
trait Contract { | ||
fn find_violations(&self, imports_info: &ImportsInfo) -> Result<Vec<ContractViolation>>; | ||
} | ||
|
||
#[derive(Debug, Clone, PartialEq)] | ||
pub enum ContractViolation { | ||
ForbiddenImport { | ||
forbidden_import: ForbiddenImport, | ||
path: Vec<PackageItemToken>, | ||
}, | ||
} | ||
|
||
#[derive(Debug, Clone, PartialEq)] | ||
pub struct ForbiddenImport { | ||
from: PackageItemToken, | ||
to: PackageItemToken, | ||
except_via: HashSet<PackageItemToken>, | ||
} | ||
|
||
impl ForbiddenImport { | ||
fn new( | ||
from: PackageItemToken, | ||
to: PackageItemToken, | ||
except_via: HashSet<PackageItemToken>, | ||
) -> Self { | ||
ForbiddenImport { | ||
from, | ||
to, | ||
except_via, | ||
} | ||
} | ||
|
||
pub fn from(&self) -> PackageItemToken { | ||
self.from | ||
} | ||
pub fn to(&self) -> PackageItemToken { | ||
self.to | ||
} | ||
pub fn except_via(&self) -> &HashSet<PackageItemToken> { | ||
&self.except_via | ||
} | ||
} |
2 changes: 0 additions & 2 deletions
2
...orts_info/queries/internal_imports/mod.rs → src/imports_info/queries/internal_imports.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,3 @@ | ||
mod layers; | ||
|
||
use std::collections::{HashMap, HashSet}; | ||
|
||
use anyhow::Result; | ||
|
Oops, something went wrong.