1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use crate::error::{Error, Result};
10use crate::message::agent::TapParticipant;
11use crate::message::tap_message_trait::{TapMessage as TapMessageTrait, TapMessageBody};
12use crate::message::{Agent, Party};
13use crate::TapMessage;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ConnectAgent {
20 #[serde(rename = "@id")]
22 pub id: String,
23
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub name: Option<String>,
27
28 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
30 pub agent_type: Option<String>,
31
32 #[serde(rename = "serviceUrl", skip_serializing_if = "Option::is_none")]
34 pub service_url: Option<String>,
35
36 #[serde(flatten)]
38 pub metadata: HashMap<String, serde_json::Value>,
39}
40
41impl TapParticipant for ConnectAgent {
42 fn id(&self) -> &str {
43 &self.id
44 }
45}
46
47impl ConnectAgent {
48 pub fn new(id: &str) -> Self {
50 Self {
51 id: id.to_string(),
52 name: None,
53 agent_type: None,
54 service_url: None,
55 metadata: HashMap::new(),
56 }
57 }
58
59 pub fn to_agent(&self, for_party: &str) -> Agent {
61 let mut agent = Agent::new_without_role(&self.id, for_party);
62
63 if let Some(name) = &self.name {
65 agent
66 .metadata
67 .insert("name".to_string(), serde_json::Value::String(name.clone()));
68 }
69 if let Some(agent_type) = &self.agent_type {
70 agent.metadata.insert(
71 "type".to_string(),
72 serde_json::Value::String(agent_type.clone()),
73 );
74 }
75 if let Some(service_url) = &self.service_url {
76 agent.metadata.insert(
77 "serviceUrl".to_string(),
78 serde_json::Value::String(service_url.clone()),
79 );
80 }
81
82 for (k, v) in &self.metadata {
84 agent.metadata.insert(k.clone(), v.clone());
85 }
86
87 agent
88 }
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct TransactionLimits {
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub per_transaction: Option<String>,
97
98 #[serde(skip_serializing_if = "Option::is_none")]
100 pub daily: Option<String>,
101
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub currency: Option<String>,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct ConnectionConstraints {
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub purposes: Option<Vec<String>>,
113
114 #[serde(rename = "categoryPurposes", skip_serializing_if = "Option::is_none")]
116 pub category_purposes: Option<Vec<String>>,
117
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub limits: Option<TransactionLimits>,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
125#[tap(
126 message_type = "https://tap.rsvp/schema/1.0#Connect",
127 initiator,
128 authorizable
129)]
130pub struct Connect {
131 #[serde(skip)]
133 #[tap(transaction_id)]
134 pub transaction_id: Option<String>,
135
136 #[serde(skip_serializing_if = "Option::is_none")]
138 pub agent_id: Option<String>,
139
140 #[serde(skip_serializing_if = "Option::is_none")]
142 #[tap(participant)]
143 pub agent: Option<ConnectAgent>,
144
145 #[serde(skip_serializing_if = "Option::is_none")]
147 #[tap(participant)]
148 pub principal: Option<Party>,
149
150 #[serde(rename = "for", skip_serializing_if = "Option::is_none", default)]
152 pub for_: Option<String>,
153
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub role: Option<String>,
157
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub constraints: Option<ConnectionConstraints>,
161}
162
163impl Connect {
164 pub fn new(transaction_id: &str, agent_id: &str, for_id: &str, role: Option<&str>) -> Self {
166 Self {
167 transaction_id: Some(transaction_id.to_string()),
168 agent_id: Some(agent_id.to_string()),
169 agent: None,
170 principal: None,
171 for_: Some(for_id.to_string()),
172 role: role.map(|s| s.to_string()),
173 constraints: None,
174 }
175 }
176
177 pub fn new_with_agent_and_principal(
179 transaction_id: &str,
180 agent: ConnectAgent,
181 principal: Party,
182 ) -> Self {
183 Self {
184 transaction_id: Some(transaction_id.to_string()),
185 agent_id: None,
186 agent: Some(agent),
187 principal: Some(principal),
188 for_: None,
189 role: None,
190 constraints: None,
191 }
192 }
193
194 pub fn with_constraints(mut self, constraints: ConnectionConstraints) -> Self {
196 self.constraints = Some(constraints);
197 self
198 }
199}
200
201impl Connect {
202 pub fn validate_connect(&self) -> Result<()> {
204 if self.agent_id.is_none() && self.agent.is_none() {
209 return Err(Error::Validation(
210 "either agent_id or agent is required".to_string(),
211 ));
212 }
213
214 let for_empty = self.for_.as_ref().is_none_or(|s| s.is_empty());
216 if for_empty && self.principal.is_none() {
217 return Err(Error::Validation(
218 "either for or principal is required".to_string(),
219 ));
220 }
221
222 if self.constraints.is_none() {
224 return Err(Error::Validation(
225 "Connection request must include constraints".to_string(),
226 ));
227 }
228
229 Ok(())
230 }
231
232 pub fn validate(&self) -> Result<()> {
234 self.validate_connect()
235 }
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
240#[tap(message_type = "https://tap.rsvp/schema/1.0#OutOfBand")]
241pub struct OutOfBand {
242 pub goal_code: String,
244
245 pub goal: String,
247
248 pub service: String,
250
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub accept: Option<Vec<String>>,
254
255 #[serde(skip_serializing_if = "Option::is_none")]
257 pub handshake_protocols: Option<Vec<String>>,
258
259 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
261 pub metadata: HashMap<String, serde_json::Value>,
262}
263
264impl OutOfBand {
265 pub fn new(goal_code: String, goal: String, service: String) -> Self {
267 Self {
268 goal_code,
269 goal,
270 service,
271 accept: None,
272 handshake_protocols: None,
273 metadata: HashMap::new(),
274 }
275 }
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
280#[tap(message_type = "https://tap.rsvp/schema/1.0#AuthorizationRequired")]
281pub struct AuthorizationRequired {
282 #[serde(rename = "authorization_url")]
284 pub url: String,
285
286 #[serde(skip_serializing_if = "Option::is_none")]
288 pub agent_id: Option<String>,
289
290 #[serde(skip_serializing_if = "Option::is_none")]
292 pub expires: Option<String>,
293
294 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
296 pub metadata: HashMap<String, serde_json::Value>,
297}
298
299impl AuthorizationRequired {
300 pub fn new(url: String, expires: String) -> Self {
302 Self {
303 url,
304 agent_id: None,
305 expires: Some(expires),
306 metadata: HashMap::new(),
307 }
308 }
309
310 pub fn add_metadata(mut self, key: &str, value: serde_json::Value) -> Self {
312 self.metadata.insert(key.to_string(), value);
313 self
314 }
315}
316
317impl OutOfBand {
318 pub fn validate_out_of_band(&self) -> Result<()> {
320 if self.goal_code.is_empty() {
321 return Err(Error::Validation("Goal code is required".to_string()));
322 }
323
324 if self.service.is_empty() {
325 return Err(Error::Validation("Service is required".to_string()));
326 }
327
328 Ok(())
329 }
330
331 pub fn validate(&self) -> Result<()> {
333 self.validate_out_of_band()
334 }
335}
336
337impl AuthorizationRequired {
338 pub fn validate_authorization_required(&self) -> Result<()> {
340 if self.url.is_empty() {
341 return Err(Error::Validation(
342 "Authorization URL is required".to_string(),
343 ));
344 }
345
346 if let Some(expires) = &self.expires {
348 if !expires.contains('T') || !expires.contains(':') {
350 return Err(Error::Validation(
351 "Invalid expiry date format. Expected ISO8601/RFC3339 format".to_string(),
352 ));
353 }
354 }
355
356 Ok(())
357 }
358
359 pub fn validate(&self) -> Result<()> {
361 self.validate_authorization_required()
362 }
363}