use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use crate::Platform;
use anyhow::{Context, Result, bail};
use greentic_pack::messaging::{MessagingAdapterCapabilities, MessagingAdapterKind};
use packc::manifest::PackSpec;
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub struct AdapterDescriptor {
pub pack_id: String,
pub pack_version: String,
pub name: String,
pub kind: MessagingAdapterKind,
pub component: String,
pub default_flow: Option<String>,
pub custom_flow: Option<String>,
pub capabilities: Option<MessagingAdapterCapabilities>,
pub source: Option<PathBuf>,
}
impl AdapterDescriptor {
pub fn allows_ingress(&self) -> bool {
matches!(
self.kind,
MessagingAdapterKind::Ingress | MessagingAdapterKind::IngressEgress
)
}
pub fn allows_egress(&self) -> bool {
matches!(
self.kind,
MessagingAdapterKind::Egress | MessagingAdapterKind::IngressEgress
)
}
pub fn flow_path(&self) -> Option<&str> {
self.custom_flow.as_deref().or(self.default_flow.as_deref())
}
}
#[derive(Default, Clone, Debug)]
pub struct AdapterRegistry {
adapters: HashMap<String, AdapterDescriptor>,
}
impl AdapterRegistry {
pub fn load_from_paths(paths: &[PathBuf]) -> Result<Self> {
load_adapters_from_pack_files(paths)
}
pub fn register(&mut self, adapter: AdapterDescriptor) -> Result<()> {
if self.adapters.contains_key(&adapter.name) {
bail!("duplicate adapter registration for {}", adapter.name);
}
self.adapters.insert(adapter.name.clone(), adapter);
Ok(())
}
pub fn get(&self, name: &str) -> Option<&AdapterDescriptor> {
self.adapters.get(name)
}
pub fn all(&self) -> Vec<AdapterDescriptor> {
self.adapters.values().cloned().collect()
}
pub fn by_kind(&self, kind: MessagingAdapterKind) -> Vec<AdapterDescriptor> {
self.adapters
.values()
.filter(|a| a.kind == kind)
.cloned()
.collect()
}
pub fn names(&self) -> Vec<String> {
self.adapters.keys().cloned().collect()
}
pub fn is_empty(&self) -> bool {
self.adapters.is_empty()
}
}
pub fn load_adapters_from_pack_files(paths: &[PathBuf]) -> Result<AdapterRegistry> {
let mut registry = AdapterRegistry::default();
for path in paths {
let adapters = adapters_from_pack_file(path)
.with_context(|| format!("failed to load pack {}", path.display()))?;
for adapter in adapters {
registry
.register(adapter)
.with_context(|| format!("failed to register adapters from {}", path.display()))?;
}
}
Ok(registry)
}
pub fn adapters_from_pack_file(path: &Path) -> Result<Vec<AdapterDescriptor>> {
let raw = fs::read_to_string(path)
.with_context(|| format!("failed to read pack file {}", path.display()))?;
let spec: PackSpec = serde_yaml_bw::from_str(&raw)
.with_context(|| format!("{} is not a valid pack spec", path.display()))?;
validate_pack_spec(&spec)?;
extract_adapters(&spec, Some(path))
}
fn validate_pack_spec(spec: &PackSpec) -> Result<()> {
if spec.id.trim().is_empty() {
bail!("pack id must not be empty");
}
if spec.version.trim().is_empty() {
bail!("pack version must not be empty");
}
if let Some(messaging) = &spec.messaging {
messaging.validate()?;
}
Ok(())
}
fn extract_adapters(spec: &PackSpec, source: Option<&Path>) -> Result<Vec<AdapterDescriptor>> {
let mut out = Vec::new();
let messaging = match &spec.messaging {
Some(section) => section,
None => return Ok(out),
};
let adapters = match &messaging.adapters {
Some(list) => list,
None => return Ok(out),
};
let mut seen = std::collections::BTreeSet::new();
for adapter in adapters {
if !seen.insert(&adapter.name) {
bail!("duplicate messaging adapter name: {}", adapter.name);
}
out.push(AdapterDescriptor {
pack_id: spec.id.clone(),
pack_version: spec.version.clone(),
name: adapter.name.clone(),
kind: adapter.kind.clone(),
component: adapter.component.clone(),
default_flow: adapter.default_flow.clone(),
custom_flow: adapter.custom_flow.clone(),
capabilities: adapter.capabilities.clone(),
source: source.map(Path::to_path_buf),
});
}
Ok(out)
}
pub fn infer_platform_from_adapter_name(name: &str) -> Option<Platform> {
let lowered = name.to_ascii_lowercase();
if lowered.starts_with("slack") {
Some(Platform::Slack)
} else if lowered.starts_with("teams") {
Some(Platform::Teams)
} else if lowered.starts_with("webex") {
Some(Platform::Webex)
} else if lowered.starts_with("webchat") {
Some(Platform::WebChat)
} else if lowered.starts_with("whatsapp") {
Some(Platform::WhatsApp)
} else if lowered.starts_with("telegram") {
Some(Platform::Telegram)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn loads_slack_pack() {
let base = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../packs/messaging/slack.yaml");
let registry = load_adapters_from_pack_files(std::slice::from_ref(&base)).unwrap();
let adapter = registry.get("slack-main").expect("adapter registered");
assert_eq!(adapter.pack_id, "greentic-messaging-slack");
assert_eq!(adapter.kind, MessagingAdapterKind::IngressEgress);
assert_eq!(adapter.component, "slack-adapter@1.0.0");
assert_eq!(
adapter.default_flow.as_deref(),
Some("flows/messaging/slack/default.ygtc")
);
assert_eq!(adapter.source.as_ref(), Some(&base));
}
#[test]
fn by_kind_filters() {
let paths = vec![
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../packs/messaging/slack.yaml"),
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../packs/messaging/telegram.yaml"),
];
let registry = load_adapters_from_pack_files(&paths).unwrap();
let ingress = registry.by_kind(MessagingAdapterKind::Ingress);
assert!(ingress.iter().any(|a| a.name == "telegram-ingress"));
let egress = registry.by_kind(MessagingAdapterKind::Egress);
assert!(egress.iter().any(|a| a.name == "telegram-egress"));
let both = registry.by_kind(MessagingAdapterKind::IngressEgress);
assert!(both.iter().any(|a| a.name == "slack-main"));
}
#[test]
fn infers_platform_from_name_prefix() {
assert_eq!(
infer_platform_from_adapter_name("slack-main"),
Some(Platform::Slack)
);
assert_eq!(
infer_platform_from_adapter_name("telegram-ingress"),
Some(Platform::Telegram)
);
assert_eq!(infer_platform_from_adapter_name("unknown"), None);
}
}