tap_msg/message/
context.rs

1//! Message Context Pattern for TAP messages.
2//!
3//! This module provides a declarative way to handle message context,
4//! including participant extraction, routing hints, and transaction context.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Message context providing participants, routing hints, and transaction context
10pub trait MessageContext {
11    /// Extract all participant DIDs from the message
12    /// This replaces the old participants() method and works with Agent/Party types directly
13    fn participant_dids(&self) -> Vec<String>;
14
15    /// Get routing hints for message delivery
16    fn routing_hints(&self) -> RoutingHints {
17        RoutingHints::default()
18    }
19
20    /// Get transaction context if applicable
21    fn transaction_context(&self) -> Option<TransactionContext> {
22        None
23    }
24
25    /// Get transaction ID if available
26    fn transaction_id(&self) -> Option<String> {
27        self.transaction_context().map(|ctx| ctx.transaction_id)
28    }
29}
30
31/// Routing hints for message delivery
32#[derive(Debug, Clone, Default, Serialize, Deserialize)]
33pub struct RoutingHints {
34    /// Preferred delivery endpoints
35    pub preferred_endpoints: Vec<String>,
36
37    /// Priority routing (high, normal, low)
38    pub priority: Priority,
39
40    /// Whether to use encryption
41    pub require_encryption: bool,
42
43    /// Custom routing metadata
44    pub metadata: HashMap<String, serde_json::Value>,
45}
46
47/// Priority levels for message routing
48#[derive(Debug, Clone, Default, Serialize, Deserialize)]
49pub enum Priority {
50    High,
51    #[default]
52    Normal,
53    Low,
54}
55
56/// Transaction context for messages that are part of a transaction
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct TransactionContext {
59    /// The transaction ID
60    pub transaction_id: String,
61
62    /// Parent transaction ID if this is a sub-transaction
63    pub parent_transaction_id: Option<String>,
64
65    /// Transaction type (transfer, payment, etc.)
66    pub transaction_type: String,
67
68    /// Transaction metadata
69    pub metadata: HashMap<String, serde_json::Value>,
70}
71
72impl TransactionContext {
73    /// Create a new transaction context
74    pub fn new(transaction_id: String, transaction_type: String) -> Self {
75        Self {
76            transaction_id,
77            parent_transaction_id: None,
78            transaction_type,
79            metadata: HashMap::new(),
80        }
81    }
82
83    /// Set parent transaction ID
84    pub fn with_parent(mut self, parent_id: String) -> Self {
85        self.parent_transaction_id = Some(parent_id);
86        self
87    }
88
89    /// Add metadata
90    pub fn with_metadata(mut self, key: String, value: serde_json::Value) -> Self {
91        self.metadata.insert(key, value);
92        self
93    }
94}
95
96/// A helper trait for extracting participant DIDs using attributes
97pub trait ParticipantExtractor {
98    /// Extract participant DIDs marked with #[tap(participant)]
99    fn extract_single_participant_dids(&self) -> Vec<String>;
100
101    /// Extract participant DIDs from lists marked with #[tap(participant_list)]
102    fn extract_list_participant_dids(&self) -> Vec<String>;
103
104    /// Extract optional participant DIDs marked with #[tap(participant)]
105    fn extract_optional_participant_dids(&self) -> Vec<String>;
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::message::agent::TapParticipant;
112    use crate::message::{Agent, Party};
113
114    #[test]
115    fn test_routing_hints_default() {
116        let hints = RoutingHints::default();
117        assert!(hints.preferred_endpoints.is_empty());
118        assert!(matches!(hints.priority, Priority::Normal));
119        assert!(!hints.require_encryption);
120        assert!(hints.metadata.is_empty());
121    }
122
123    #[test]
124    fn test_transaction_context() {
125        let ctx = TransactionContext::new("tx-123".to_string(), "transfer".to_string())
126            .with_parent("parent-tx-456".to_string())
127            .with_metadata(
128                "key".to_string(),
129                serde_json::Value::String("value".to_string()),
130            );
131
132        assert_eq!(ctx.transaction_id, "tx-123");
133        assert_eq!(ctx.transaction_type, "transfer");
134        assert_eq!(ctx.parent_transaction_id, Some("parent-tx-456".to_string()));
135        assert_eq!(
136            ctx.metadata.get("key").unwrap(),
137            &serde_json::Value::String("value".to_string())
138        );
139    }
140
141    // Mock implementation for testing with Agent and Party types
142    struct TestMessage {
143        originator: Party,
144        beneficiary: Option<Party>,
145        agents: Vec<Agent>,
146        transaction_id: String,
147    }
148
149    impl MessageContext for TestMessage {
150        fn participant_dids(&self) -> Vec<String> {
151            let mut dids = vec![self.originator.id().to_string()];
152            if let Some(ref beneficiary) = self.beneficiary {
153                dids.push(beneficiary.id().to_string());
154            }
155            for agent in &self.agents {
156                dids.push(agent.id().to_string());
157            }
158            dids
159        }
160
161        fn transaction_context(&self) -> Option<TransactionContext> {
162            Some(TransactionContext::new(
163                self.transaction_id.clone(),
164                "test".to_string(),
165            ))
166        }
167    }
168
169    #[test]
170    fn test_message_context() {
171        let msg = TestMessage {
172            originator: Party::new("did:example:alice"),
173            beneficiary: Some(Party::new("did:example:bob")),
174            agents: vec![Agent::new(
175                "did:example:agent",
176                "Exchange",
177                "did:example:alice",
178            )],
179            transaction_id: "tx-123".to_string(),
180        };
181
182        let dids = msg.participant_dids();
183        assert_eq!(dids.len(), 3);
184        assert!(dids.contains(&"did:example:alice".to_string()));
185        assert!(dids.contains(&"did:example:bob".to_string()));
186        assert!(dids.contains(&"did:example:agent".to_string()));
187
188        assert_eq!(msg.transaction_id(), Some("tx-123".to_string()));
189    }
190}