use std::collections::BTreeSet;
use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum FlowEntry {
Route(String),
Endpoint(String),
Operation(String),
Event(String),
PublicApi(String),
}
impl FlowEntry {
#[must_use]
pub fn surface(&self) -> &str {
match self {
Self::Route(value)
| Self::Endpoint(value)
| Self::Operation(value)
| Self::Event(value)
| Self::PublicApi(value) => value,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ImpactedFlow {
pub id: String,
pub revision: String,
pub entry: FlowEntry,
pub graph_nodes: Vec<String>,
pub graph_edges: Vec<String>,
pub public_surfaces: Vec<String>,
pub requirements: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct FlowFingerprint {
pub entry_surface: String,
pub capability: Option<String>,
pub observable_contract: Vec<String>,
pub structural_digest: String,
}
#[must_use]
pub fn fingerprint(flow: &ImpactedFlow, capability: Option<String>) -> FlowFingerprint {
let mut contract: Vec<String> = flow.public_surfaces.clone();
contract.extend(flow.requirements.clone());
contract.sort();
contract.dedup();
let mut nodes = flow.graph_nodes.clone();
nodes.sort();
FlowFingerprint {
entry_surface: flow.entry.surface().to_owned(),
capability,
observable_contract: contract,
structural_digest: nodes.join("|"),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum FlowState {
Unchanged,
Modified,
Rewired,
Split,
Merged,
Added,
Removed,
Unmatched,
}
impl FlowState {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Unchanged => "unchanged",
Self::Modified => "modified",
Self::Rewired => "rewired",
Self::Split => "split",
Self::Merged => "merged",
Self::Added => "added",
Self::Removed => "removed",
Self::Unmatched => "unmatched",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct FlowMatch {
pub state: FlowState,
pub base: Option<String>,
pub head: Option<String>,
pub matched_on: &'static str,
pub surface: String,
}
#[must_use]
pub fn match_flows(base: &[ImpactedFlow], head: &[ImpactedFlow]) -> Vec<FlowMatch> {
let mut out = Vec::new();
let mut used_head: BTreeSet<usize> = BTreeSet::new();
for base_flow in base {
let candidates: Vec<usize> = head
.iter()
.enumerate()
.filter(|(index, _)| !used_head.contains(index))
.filter(|(_, head_flow)| head_flow.entry.surface() == base_flow.entry.surface())
.map(|(index, _)| index)
.collect();
if candidates.len() > 1 {
for index in &candidates {
used_head.insert(*index);
out.push(FlowMatch {
state: FlowState::Split,
base: Some(base_flow.id.clone()),
head: Some(head[*index].id.clone()),
matched_on: "entry_surface",
surface: base_flow.entry.surface().to_owned(),
});
}
continue;
}
if let Some(index) = candidates.first().copied() {
used_head.insert(index);
out.push(pair(base_flow, &head[index], "entry_surface"));
continue;
}
let by_requirement = head.iter().enumerate().find(|(index, head_flow)| {
!used_head.contains(index)
&& head_flow
.requirements
.iter()
.any(|item| base_flow.requirements.contains(item))
});
if let Some((index, head_flow)) = by_requirement {
used_head.insert(index);
out.push(pair(base_flow, head_flow, "requirement"));
continue;
}
let by_nodes = head.iter().enumerate().find(|(index, head_flow)| {
!used_head.contains(index) && node_overlap(base_flow, head_flow) >= 2
});
if let Some((index, head_flow)) = by_nodes {
used_head.insert(index);
out.push(pair(base_flow, head_flow, "graph_neighbourhood"));
continue;
}
out.push(FlowMatch {
state: FlowState::Removed,
base: Some(base_flow.id.clone()),
head: None,
matched_on: "none",
surface: base_flow.entry.surface().to_owned(),
});
}
for (index, head_flow) in head.iter().enumerate() {
if used_head.contains(&index) {
continue;
}
out.push(FlowMatch {
state: FlowState::Added,
base: None,
head: Some(head_flow.id.clone()),
matched_on: "none",
surface: head_flow.entry.surface().to_owned(),
});
}
let mut merged = Vec::new();
for item in &out {
if let Some(head_id) = &item.head
&& out
.iter()
.filter(|other| other.head.as_ref() == Some(head_id) && other.base.is_some())
.count()
> 1
{
merged.push(head_id.clone());
}
}
for item in &mut out {
if let Some(head_id) = &item.head
&& merged.contains(head_id)
&& item.base.is_some()
{
item.state = FlowState::Merged;
}
}
out.sort_by(|left, right| {
left.surface
.cmp(&right.surface)
.then_with(|| left.base.cmp(&right.base))
.then_with(|| left.head.cmp(&right.head))
});
out
}
fn pair(base: &ImpactedFlow, head: &ImpactedFlow, matched_on: &'static str) -> FlowMatch {
let same_nodes = sorted(&base.graph_nodes) == sorted(&head.graph_nodes);
let same_edges = sorted(&base.graph_edges) == sorted(&head.graph_edges);
let state = match (same_nodes, same_edges) {
(true, true) => FlowState::Unchanged,
(true, false) => FlowState::Rewired,
_ => FlowState::Modified,
};
FlowMatch {
state,
base: Some(base.id.clone()),
head: Some(head.id.clone()),
matched_on,
surface: base.entry.surface().to_owned(),
}
}
fn node_overlap(left: &ImpactedFlow, right: &ImpactedFlow) -> usize {
let right_nodes: BTreeSet<&String> = right.graph_nodes.iter().collect();
left.graph_nodes
.iter()
.filter(|item| right_nodes.contains(item))
.count()
}
fn sorted(values: &[String]) -> Vec<&String> {
let mut out: Vec<&String> = values.iter().collect();
out.sort();
out
}