use std::collections::BTreeSet;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::discovery::{DetectedProvider, DiscoveryResult};
#[derive(Clone, Debug, Default, Serialize)]
pub struct BundleDiff {
pub packs_added: Vec<DetectedProvider>,
pub packs_removed: Vec<DetectedProvider>,
pub packs_changed: Vec<DetectedProvider>,
pub providers_added: Vec<String>,
pub providers_removed: Vec<String>,
pub tenants_added: Vec<String>,
pub tenants_removed: Vec<String>,
}
impl BundleDiff {
pub fn is_empty(&self) -> bool {
self.packs_added.is_empty()
&& self.packs_removed.is_empty()
&& self.packs_changed.is_empty()
&& self.providers_added.is_empty()
&& self.providers_removed.is_empty()
&& self.tenants_added.is_empty()
&& self.tenants_removed.is_empty()
}
pub fn change_count(&self) -> usize {
self.packs_added.len()
+ self.packs_removed.len()
+ self.packs_changed.len()
+ self.providers_added.len()
+ self.providers_removed.len()
+ self.tenants_added.len()
+ self.tenants_removed.len()
}
}
#[derive(Clone, Debug, Serialize)]
pub struct ReloadPlan {
pub bundle: PathBuf,
pub diff: BundleDiff,
pub actions: Vec<ReloadAction>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ReloadAction {
LoadComponent { provider_id: String, path: PathBuf },
UnloadComponent { provider_id: String },
ReloadComponent { provider_id: String, path: PathBuf },
UpdateRoutes,
RunResolver,
SeedSecrets { provider_id: String },
}
pub fn diff_discoveries(prev: &DiscoveryResult, curr: &DiscoveryResult) -> BundleDiff {
let prev_ids: BTreeSet<&str> = prev
.providers
.iter()
.map(|p| p.provider_id.as_str())
.collect();
let curr_ids: BTreeSet<&str> = curr
.providers
.iter()
.map(|p| p.provider_id.as_str())
.collect();
let added_ids: BTreeSet<&&str> = curr_ids.difference(&prev_ids).collect();
let removed_ids: BTreeSet<&&str> = prev_ids.difference(&curr_ids).collect();
let packs_added: Vec<DetectedProvider> = curr
.providers
.iter()
.filter(|p| added_ids.contains(&&p.provider_id.as_str()))
.cloned()
.collect();
let packs_removed: Vec<DetectedProvider> = prev
.providers
.iter()
.filter(|p| removed_ids.contains(&&p.provider_id.as_str()))
.cloned()
.collect();
let packs_changed: Vec<DetectedProvider> = curr
.providers
.iter()
.filter(|cp| {
if added_ids.contains(&&cp.provider_id.as_str()) {
return false;
}
prev.providers
.iter()
.any(|pp| pp.provider_id == cp.provider_id && pp.pack_path != cp.pack_path)
})
.cloned()
.collect();
BundleDiff {
packs_added,
packs_removed,
packs_changed,
providers_added: Vec::new(),
providers_removed: Vec::new(),
tenants_added: Vec::new(),
tenants_removed: Vec::new(),
}
}
pub fn plan_reload(bundle: &std::path::Path, diff: &BundleDiff) -> ReloadPlan {
let mut actions = Vec::new();
for pack in &diff.packs_added {
actions.push(ReloadAction::LoadComponent {
provider_id: pack.provider_id.clone(),
path: pack.pack_path.clone(),
});
actions.push(ReloadAction::SeedSecrets {
provider_id: pack.provider_id.clone(),
});
}
for pack in &diff.packs_removed {
actions.push(ReloadAction::UnloadComponent {
provider_id: pack.provider_id.clone(),
});
}
for pack in &diff.packs_changed {
actions.push(ReloadAction::ReloadComponent {
provider_id: pack.provider_id.clone(),
path: pack.pack_path.clone(),
});
}
if !diff.packs_added.is_empty()
|| !diff.packs_removed.is_empty()
|| !diff.packs_changed.is_empty()
{
actions.push(ReloadAction::UpdateRoutes);
}
if !diff.tenants_added.is_empty() || !diff.tenants_removed.is_empty() {
actions.push(ReloadAction::RunResolver);
}
ReloadPlan {
bundle: bundle.to_path_buf(),
diff: diff.clone(),
actions,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::discovery::{DetectedDomains, DetectedPackKind, ProviderIdSource};
fn make_provider(id: &str, path: &str) -> DetectedProvider {
DetectedProvider {
provider_id: id.to_string(),
display_name: None,
domain: "messaging".to_string(),
pack_path: PathBuf::from(path),
id_source: ProviderIdSource::Manifest,
kind: DetectedPackKind::Provider,
}
}
fn make_discovery(providers: Vec<DetectedProvider>) -> DiscoveryResult {
DiscoveryResult {
domains: DetectedDomains {
messaging: true,
events: false,
oauth: false,
state: false,
secrets: false,
},
providers,
app_packs: Vec::new(),
}
}
#[test]
fn empty_diff_when_same() {
let disc = make_discovery(vec![make_provider("telegram", "/a/telegram.gtpack")]);
let diff = diff_discoveries(&disc, &disc);
assert!(diff.is_empty());
assert_eq!(diff.change_count(), 0);
}
#[test]
fn detects_added_packs() {
let prev = make_discovery(vec![make_provider("telegram", "/a/telegram.gtpack")]);
let curr = make_discovery(vec![
make_provider("telegram", "/a/telegram.gtpack"),
make_provider("slack", "/a/slack.gtpack"),
]);
let diff = diff_discoveries(&prev, &curr);
assert_eq!(diff.packs_added.len(), 1);
assert_eq!(diff.packs_added[0].provider_id, "slack");
assert!(diff.packs_removed.is_empty());
}
#[test]
fn detects_removed_packs() {
let prev = make_discovery(vec![
make_provider("telegram", "/a/telegram.gtpack"),
make_provider("slack", "/a/slack.gtpack"),
]);
let curr = make_discovery(vec![make_provider("telegram", "/a/telegram.gtpack")]);
let diff = diff_discoveries(&prev, &curr);
assert!(diff.packs_added.is_empty());
assert_eq!(diff.packs_removed.len(), 1);
assert_eq!(diff.packs_removed[0].provider_id, "slack");
}
#[test]
fn detects_changed_packs() {
let prev = make_discovery(vec![make_provider("telegram", "/a/v1/telegram.gtpack")]);
let curr = make_discovery(vec![make_provider("telegram", "/a/v2/telegram.gtpack")]);
let diff = diff_discoveries(&prev, &curr);
assert!(diff.packs_added.is_empty());
assert!(diff.packs_removed.is_empty());
assert_eq!(diff.packs_changed.len(), 1);
}
#[test]
fn plan_reload_generates_actions() {
let diff = BundleDiff {
packs_added: vec![make_provider("slack", "/a/slack.gtpack")],
packs_removed: vec![make_provider("teams", "/a/teams.gtpack")],
packs_changed: vec![make_provider("telegram", "/a/telegram.gtpack")],
..Default::default()
};
let plan = plan_reload(std::path::Path::new("/bundle"), &diff);
assert_eq!(plan.actions.len(), 5);
}
#[test]
fn empty_diff_no_actions() {
let diff = BundleDiff::default();
let plan = plan_reload(std::path::Path::new("/bundle"), &diff);
assert!(plan.actions.is_empty());
}
}