-
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
7 changed files
with
360 additions
and
168 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
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,295 @@ | ||
use crate::contracts::{Contract, ContractViolation, ForbiddenImport}; | ||
use crate::{ImportsInfo, InternalImportsPathQuery, PackageItemToken, PackageItemTokens}; | ||
use anyhow::Result; | ||
use itertools::Itertools; | ||
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() { | ||
// Lower layers should not import higher layers. | ||
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! {}, | ||
)); | ||
} | ||
} | ||
} | ||
|
||
// Higher layers should not import lower layers, except via the layer immediately below. | ||
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(), | ||
)); | ||
} | ||
} | ||
} | ||
} | ||
|
||
// Independent siblings should not import each other. | ||
if layer.siblings_independent { | ||
for permutation in layer.siblings.iter().permutations(2) { | ||
forbidden_imports.push(ForbiddenImport::new( | ||
*permutation[0], | ||
*permutation[1], | ||
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(()) | ||
} | ||
} |
Oops, something went wrong.