tap_msg/message/
connection.rs

1//! Connection types for TAP messages.
2//!
3//! This module defines the structure of connection messages and related types
4//! used in the Transaction Authorization Protocol (TAP).
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use crate::error::{Error, Result};
10use crate::message::tap_message_trait::{TapMessage as TapMessageTrait, TapMessageBody};
11use crate::TapMessage;
12
13/// Transaction limits for connection constraints.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct TransactionLimits {
16    /// Maximum amount for a transaction.
17    pub max_amount: Option<String>,
18
19    /// Maximum total amount for all transactions.
20    pub max_total_amount: Option<String>,
21
22    /// Maximum number of transactions allowed.
23    pub max_transactions: Option<u64>,
24}
25
26/// Connection constraints for the Connect message.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct ConnectionConstraints {
29    /// Limit on transaction amount.
30    pub transaction_limits: Option<TransactionLimits>,
31}
32
33/// Connect message body (TAIP-2).
34#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
35#[tap(
36    message_type = "https://tap.rsvp/schema/1.0#Connect",
37    initiator,
38    authorizable
39)]
40pub struct Connect {
41    /// Transaction ID.
42    #[tap(transaction_id)]
43    pub transaction_id: String,
44
45    /// Agent DID.
46    pub agent_id: String,
47
48    /// The entity this connection is for.
49    #[serde(rename = "for")]
50    pub for_: String,
51
52    /// The role of the agent (optional).
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub role: Option<String>,
55
56    /// Connection constraints (optional).
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub constraints: Option<ConnectionConstraints>,
59}
60
61impl Connect {
62    /// Create a new Connect message.
63    pub fn new(transaction_id: &str, agent_id: &str, for_id: &str, role: Option<&str>) -> Self {
64        Self {
65            transaction_id: transaction_id.to_string(),
66            agent_id: agent_id.to_string(),
67            for_: for_id.to_string(),
68            role: role.map(|s| s.to_string()),
69            constraints: None,
70        }
71    }
72
73    /// Add constraints to the Connect message.
74    pub fn with_constraints(mut self, constraints: ConnectionConstraints) -> Self {
75        self.constraints = Some(constraints);
76        self
77    }
78}
79
80impl Connect {
81    /// Custom validation for Connect messages
82    pub fn validate_connect(&self) -> Result<()> {
83        if self.transaction_id.is_empty() {
84            return Err(Error::Validation("transaction_id is required".to_string()));
85        }
86        if self.agent_id.is_empty() {
87            return Err(Error::Validation("agent_id is required".to_string()));
88        }
89        if self.for_.is_empty() {
90            return Err(Error::Validation("for is required".to_string()));
91        }
92        Ok(())
93    }
94
95    /// Validation method that will be called by TapMessageBody trait
96    pub fn validate(&self) -> Result<()> {
97        self.validate_connect()
98    }
99}
100
101/// Out of Band invitation for TAP connections.
102#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
103#[tap(message_type = "https://tap.rsvp/schema/1.0#OutOfBand")]
104pub struct OutOfBand {
105    /// The goal code for this invitation.
106    pub goal_code: String,
107
108    /// The goal for this invitation.
109    pub goal: String,
110
111    /// The public DID or endpoint URL for the inviter.
112    pub service: String,
113
114    /// Accept media types.
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub accept: Option<Vec<String>>,
117
118    /// Handshake protocols supported.
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub handshake_protocols: Option<Vec<String>>,
121
122    /// Additional metadata.
123    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
124    pub metadata: HashMap<String, serde_json::Value>,
125}
126
127impl OutOfBand {
128    /// Create a new OutOfBand message.
129    pub fn new(goal_code: String, goal: String, service: String) -> Self {
130        Self {
131            goal_code,
132            goal,
133            service,
134            accept: None,
135            handshake_protocols: None,
136            metadata: HashMap::new(),
137        }
138    }
139}
140
141/// Authorization Required message body.
142#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
143#[tap(message_type = "https://tap.rsvp/schema/1.0#AuthorizationRequired")]
144pub struct AuthorizationRequired {
145    /// Authorization URL.
146    pub url: String,
147
148    /// Additional metadata.
149    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
150    pub metadata: HashMap<String, serde_json::Value>,
151}
152
153impl AuthorizationRequired {
154    /// Create a new AuthorizationRequired message.
155    pub fn new(url: String, expires: String) -> Self {
156        let mut metadata = HashMap::new();
157        metadata.insert("expires".to_string(), serde_json::Value::String(expires));
158
159        Self { url, metadata }
160    }
161
162    /// Add metadata to the message.
163    pub fn add_metadata(mut self, key: &str, value: serde_json::Value) -> Self {
164        self.metadata.insert(key.to_string(), value);
165        self
166    }
167}
168
169impl OutOfBand {
170    /// Custom validation for OutOfBand messages
171    pub fn validate_out_of_band(&self) -> Result<()> {
172        if self.goal_code.is_empty() {
173            return Err(Error::Validation("Goal code is required".to_string()));
174        }
175
176        if self.service.is_empty() {
177            return Err(Error::Validation("Service is required".to_string()));
178        }
179
180        Ok(())
181    }
182
183    /// Validation method that will be called by TapMessageBody trait
184    pub fn validate(&self) -> Result<()> {
185        self.validate_out_of_band()
186    }
187}
188
189impl AuthorizationRequired {
190    /// Custom validation for AuthorizationRequired messages
191    pub fn validate_authorization_required(&self) -> Result<()> {
192        if self.url.is_empty() {
193            return Err(Error::Validation(
194                "Authorization URL is required".to_string(),
195            ));
196        }
197
198        // Validate expiry date if present
199        if let Some(expires) = self.metadata.get("expires") {
200            if let Some(expires_str) = expires.as_str() {
201                // Simple format check
202                if !expires_str.contains('T') || !expires_str.contains(':') {
203                    return Err(Error::Validation(
204                        "Invalid expiry date format. Expected ISO8601/RFC3339 format".to_string(),
205                    ));
206                }
207            }
208        }
209
210        Ok(())
211    }
212
213    /// Validation method that will be called by TapMessageBody trait
214    pub fn validate(&self) -> Result<()> {
215        self.validate_authorization_required()
216    }
217}