use serde_json::Value;
use uuid::Uuid;
use crate::binding::BindingContext;
use crate::inbound::InboundMessageMeta;
pub const META_KEY: &str = "_meta";
pub const NEXO_NAMESPACE: &str = "nexo";
pub const BINDING_KEY: &str = "binding";
pub const INBOUND_KEY: &str = "inbound";
pub fn build_meta_value(
agent_id: &str,
session_id: Option<Uuid>,
binding: Option<&BindingContext>,
inbound: Option<&InboundMessageMeta>,
) -> Value {
let mut meta = serde_json::Map::new();
meta.insert("agent_id".into(), Value::String(agent_id.to_string()));
meta.insert(
"session_id".into(),
session_id
.map(|u| Value::String(u.to_string()))
.unwrap_or(Value::Null),
);
if binding.is_some() || inbound.is_some() {
let mut nexo = serde_json::Map::new();
if let Some(b) = binding {
nexo.insert(
BINDING_KEY.into(),
serde_json::to_value(b).unwrap_or(Value::Null),
);
}
if let Some(i) = inbound {
nexo.insert(
INBOUND_KEY.into(),
serde_json::to_value(i).unwrap_or(Value::Null),
);
}
meta.insert(NEXO_NAMESPACE.into(), Value::Object(nexo));
}
Value::Object(meta)
}
pub fn parse_binding_from_meta(meta: &Value) -> Option<BindingContext> {
let nexo = meta.as_object()?.get(NEXO_NAMESPACE)?;
let binding = nexo.as_object()?.get(BINDING_KEY)?;
serde_json::from_value(binding.clone()).ok()
}
pub fn parse_inbound_from_meta(meta: &Value) -> Option<InboundMessageMeta> {
let nexo = meta.as_object()?.get(NEXO_NAMESPACE)?;
let inbound = nexo.as_object()?.get(INBOUND_KEY)?;
serde_json::from_value(inbound.clone()).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_with_binding_emits_dual_namespaces() {
let mut b = BindingContext::agent_only("ana");
b.session_id = Some(Uuid::nil());
b.channel = Some("whatsapp".into());
b.account_id = Some("personal".into());
b.binding_id = Some("whatsapp:personal".into());
let meta = build_meta_value("ana", Some(Uuid::nil()), Some(&b), None);
assert_eq!(meta["agent_id"], "ana");
assert!(meta["session_id"].is_string());
let nested = &meta["nexo"]["binding"];
assert_eq!(nested["agent_id"], "ana");
assert_eq!(nested["channel"], "whatsapp");
assert_eq!(nested["account_id"], "personal");
assert_eq!(nested["binding_id"], "whatsapp:personal");
}
#[test]
fn build_without_binding_emits_legacy_block_only() {
let meta = build_meta_value("delegation", None, None, None);
assert_eq!(meta["agent_id"], "delegation");
assert!(meta["session_id"].is_null());
assert!(meta.get(NEXO_NAMESPACE).is_none());
}
#[test]
fn parse_round_trips_with_build() {
let mut original = BindingContext::agent_only("carlos");
original.channel = Some("telegram".into());
original.account_id = Some("kate_tg".into());
original.binding_id = Some("telegram:kate_tg".into());
original.mcp_channel_source = Some("slack".into());
let meta = build_meta_value("carlos", Some(Uuid::from_u128(7)), Some(&original), None);
let back = parse_binding_from_meta(&meta).expect("binding parses");
assert_eq!(back, original);
}
#[test]
fn parse_returns_none_for_legacy_only_payload() {
let meta = build_meta_value("delegation", None, None, None);
assert!(parse_binding_from_meta(&meta).is_none());
}
#[test]
fn parse_returns_none_for_non_object_value() {
let v = serde_json::json!("not-an-object");
assert!(parse_binding_from_meta(&v).is_none());
}
#[test]
fn parse_returns_none_for_malformed_binding_block() {
let v = serde_json::json!({
"nexo": {
"binding": "not-an-object"
}
});
assert!(parse_binding_from_meta(&v).is_none());
}
#[test]
fn parse_tolerates_extra_keys_in_binding_block() {
let v = serde_json::json!({
"nexo": {
"binding": {
"agent_id": "ana",
"channel": "whatsapp",
"future_field": "ignored"
}
}
});
let ctx = parse_binding_from_meta(&v).expect("parses with extras");
assert_eq!(ctx.agent_id, "ana");
assert_eq!(ctx.channel.as_deref(), Some("whatsapp"));
}
#[test]
fn build_session_id_serialises_as_string() {
let sid = Uuid::from_u128(0x42);
let meta = build_meta_value("x", Some(sid), None, None);
assert_eq!(meta["session_id"], sid.to_string());
}
#[test]
fn build_with_inbound_emits_nested_inbound_block() {
let inbound = InboundMessageMeta::external_user("+5491100", "wa.ABCD");
let meta = build_meta_value("ana", None, None, Some(&inbound));
let nested = &meta["nexo"]["inbound"];
assert_eq!(nested["kind"], "external_user");
assert_eq!(nested["sender_id"], "+5491100");
assert_eq!(nested["msg_id"], "wa.ABCD");
assert!(meta["nexo"].as_object().unwrap().get("binding").is_none());
}
#[test]
fn parse_inbound_returns_none_when_block_absent() {
let meta = build_meta_value("delegation", None, None, None);
assert!(parse_inbound_from_meta(&meta).is_none());
}
#[test]
fn build_with_binding_and_inbound_emits_both_peer_buckets() {
let mut binding = BindingContext::agent_only("ana");
binding.channel = Some("whatsapp".into());
binding.account_id = Some("personal".into());
binding.binding_id = Some("whatsapp:personal".into());
let inbound = InboundMessageMeta::external_user("+5491100", "wa.ABCD");
let meta = build_meta_value("ana", Some(Uuid::nil()), Some(&binding), Some(&inbound));
assert_eq!(meta["nexo"]["binding"]["channel"], "whatsapp");
assert_eq!(meta["nexo"]["inbound"]["sender_id"], "+5491100");
let parsed_b = parse_binding_from_meta(&meta).expect("binding parses");
let parsed_i = parse_inbound_from_meta(&meta).expect("inbound parses");
assert_eq!(parsed_b.channel.as_deref(), Some("whatsapp"));
assert_eq!(parsed_i.sender_id.as_deref(), Some("+5491100"));
}
#[test]
fn parse_inbound_tolerates_extra_keys_in_inbound_block() {
let v = serde_json::json!({
"nexo": {
"inbound": {
"kind": "external_user",
"sender_id": "+5491100",
"future_field": "ignored"
}
}
});
let parsed = parse_inbound_from_meta(&v).expect("parses with extras");
assert_eq!(parsed.sender_id.as_deref(), Some("+5491100"));
}
}