use super::contract::{component_for, list_contains};
use crate::engine::RepositoryState;
use crate::operations::node_path;
use blazingly_json::{Value, json};
use std::collections::{BTreeMap, BTreeSet};
const COUPLING_KINDS: &[&str] = &["any", "runtime", "type-only"];
const RELATION_KINDS: &[&str] = &[
"contains",
"imports",
"calls",
"references",
"method",
"implements",
"re_exports",
"depends_on",
"inherits",
"publishes",
"consumes",
"binds",
"reads",
"writes",
"deploys",
"exposes",
"mounts",
"configures",
];
pub(super) fn dependency_violations(state: &RepositoryState, value: &Value) -> Vec<Value> {
let mut output = BTreeMap::<String, Value>::new();
for edge in state.graph().edges() {
let Some(source) = state.graph().node(edge.source.as_str()) else {
continue;
};
let Some(target) = state.graph().node(edge.target.as_str()) else {
continue;
};
let Some(source_file) = node_path(source) else {
continue;
};
let Some(target_file) = node_path(target) else {
continue;
};
let Some(from) = component_for(value, source_file) else {
continue;
};
let Some(to) = component_for(value, target_file) else {
continue;
};
if from == to {
continue;
}
for rule in matching_rules(value, from, to, edge) {
let identity = format!(
"{}|{}|{}|{}",
rule["id"].as_str().unwrap_or("rule"),
edge.source,
edge.target,
edge.kind.as_str()
);
let fingerprint = stable_hash(&identity);
output.entry(fingerprint.clone()).or_insert_with(|| {
json!({
"fingerprint": fingerprint,
"rule": rule,
"source": source,
"target": target,
"edge": edge
})
});
}
}
output.into_values().collect()
}
pub(super) fn validate(value: &Value) -> Result<(), String> {
let mut unsupported = BTreeSet::new();
for rule in value
.get("dependencyRules")
.and_then(Value::as_array)
.into_iter()
.flatten()
{
for kind in rule
.get("kinds")
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter_map(Value::as_str)
{
if !COUPLING_KINDS.contains(&kind) && !RELATION_KINDS.contains(&kind) {
unsupported.insert(kind.to_owned());
}
}
}
if unsupported.is_empty() {
return Ok(());
}
Err(format!(
"architecture contract uses dependency kinds this engine cannot evaluate: {}. \
Supported values are {} plus relation names such as imports, calls, inherits. \
Rules are rejected rather than skipped, because a rule that matches nothing \
would report a passing verification.",
unsupported.into_iter().collect::<Vec<_>>().join(", "),
COUPLING_KINDS.join(", ")
))
}
pub(super) fn stable_hash(value: &str) -> String {
let mut hash = 0xcbf2_9ce4_8422_2325_u64;
for byte in value.bytes() {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
format!("{hash:016x}")
}
fn rule_selects_edge(rule: &Value, edge: &weavatrix_graph::Edge) -> bool {
let coupling = match edge.attributes.get("coupling") {
Some(weavatrix_graph::AttributeValue::String(value)) => value.as_str(),
_ => "runtime",
};
list_contains(rule.get("kinds"), "any")
|| list_contains(rule.get("kinds"), coupling)
|| list_contains(rule.get("kinds"), edge.kind.as_str())
}
fn matching_rules<'contract>(
value: &'contract Value,
from: &str,
to: &str,
edge: &weavatrix_graph::Edge,
) -> Vec<&'contract Value> {
value
.get("dependencyRules")
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter(|rule| rule["action"] == "forbid")
.filter(|rule| list_contains(rule.get("from"), from))
.filter(|rule| list_contains(rule.get("to"), to))
.filter(|rule| rule_selects_edge(rule, edge))
.collect()
}