use crate::{Attribution, ExternalIdentity, TurnMessage};
pub trait EdgeAdapter {
type Native;
type Inbound;
fn namespace(&self) -> &'static str;
fn conversation_id(&self, native: &Self::Native) -> String;
fn to_turn_input(&self, inbound: &Self::Inbound) -> Vec<TurnMessage>;
fn caller(&self, _inbound: &Self::Inbound) -> Option<ExternalIdentity> {
None
}
}
pub fn build_attribution<E: EdgeAdapter>(
edge: &E,
trigger: Option<&E::Inbound>,
observed: &[E::Inbound],
) -> Attribution {
fn same(a: &ExternalIdentity, b: &ExternalIdentity) -> bool {
a.provider == b.provider && a.scope == b.scope && a.external_id == b.external_id
}
let caller = trigger.and_then(|t| edge.caller(t));
let mut participants: Vec<ExternalIdentity> = Vec::new();
for unit in observed {
let Some(identity) = edge.caller(unit) else {
continue;
};
let duplicate = caller.as_ref().is_some_and(|c| same(c, &identity))
|| participants.iter().any(|p| same(p, &identity));
if !duplicate {
participants.push(identity);
}
}
Attribution {
caller,
participants,
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use super::*;
use crate::{attributed_message, hashed_conversation_id};
struct ExampleEdge {
namespace_uuid: uuid::Uuid,
}
struct Thread {
team: String,
channel: String,
thread_ts: String,
}
struct Line {
speaker: String,
text: String,
}
impl EdgeAdapter for ExampleEdge {
type Native = Thread;
type Inbound = Line;
fn namespace(&self) -> &'static str {
"example"
}
fn conversation_id(&self, native: &Thread) -> String {
hashed_conversation_id(
self.namespace_uuid,
&[&native.team, &native.channel, &native.thread_ts],
)
}
fn to_turn_input(&self, inbound: &Line) -> Vec<TurnMessage> {
if inbound.text.trim().is_empty() {
return Vec::new();
}
vec![attributed_message(&inbound.speaker, &inbound.text)]
}
}
#[test]
fn conversation_id_is_stable_per_native_unit() {
let edge = ExampleEdge {
namespace_uuid: uuid::Uuid::from_u128(0x42),
};
let t = Thread {
team: "T1".to_owned(),
channel: "C1".to_owned(),
thread_ts: "169.0".to_owned(),
};
assert_eq!(edge.conversation_id(&t), edge.conversation_id(&t));
assert_eq!(edge.namespace(), "example");
}
#[test]
fn ingress_drops_empty_and_attributes_speakers() {
let edge = ExampleEdge {
namespace_uuid: uuid::Uuid::from_u128(0x42),
};
assert!(
edge.to_turn_input(&Line {
speaker: "Alice".to_owned(),
text: " ".to_owned(),
})
.is_empty()
);
assert_eq!(
edge.to_turn_input(&Line {
speaker: "Alice".to_owned(),
text: "hi".to_owned(),
}),
vec![attributed_message("Alice", "hi")]
);
}
}