1use std::fmt;
2use std::str::FromStr;
3
4use super::common::{ExecutionInfo, State};
5use super::media::Media;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10#[non_exhaustive]
11pub struct Endpoint {
12 #[cfg_attr(feature = "serde", serde(rename = "@entity"))]
15 pub entity: String,
16 #[cfg_attr(
18 feature = "serde",
19 serde(rename = "@state", default, skip_serializing_if = "Option::is_none")
20 )]
21 pub state: Option<State>,
22 #[cfg_attr(
24 feature = "serde",
25 serde(
26 rename = "display-text",
27 default,
28 skip_serializing_if = "Option::is_none"
29 )
30 )]
31 pub display_text: Option<String>,
32 #[cfg_attr(
34 feature = "serde",
35 serde(default, skip_serializing_if = "Option::is_none")
36 )]
37 pub referred: Option<ExecutionInfo>,
38 #[cfg_attr(
40 feature = "serde",
41 serde(default, skip_serializing_if = "Option::is_none")
42 )]
43 pub status: Option<EndpointStatus>,
44 #[cfg_attr(
46 feature = "serde",
47 serde(
48 rename = "joining-method",
49 default,
50 skip_serializing_if = "Option::is_none"
51 )
52 )]
53 pub joining_method: Option<JoiningMethod>,
54 #[cfg_attr(
56 feature = "serde",
57 serde(
58 rename = "joining-info",
59 default,
60 skip_serializing_if = "Option::is_none"
61 )
62 )]
63 pub joining_info: Option<ExecutionInfo>,
64 #[cfg_attr(
66 feature = "serde",
67 serde(
68 rename = "disconnection-method",
69 default,
70 skip_serializing_if = "Option::is_none"
71 )
72 )]
73 pub disconnection_method: Option<DisconnectionMethod>,
74 #[cfg_attr(
76 feature = "serde",
77 serde(
78 rename = "disconnection-info",
79 default,
80 skip_serializing_if = "Option::is_none"
81 )
82 )]
83 pub disconnection_info: Option<ExecutionInfo>,
84 #[cfg_attr(
86 feature = "serde",
87 serde(rename = "media", default, skip_serializing_if = "Vec::is_empty")
88 )]
89 pub media: Vec<Media>,
90 #[cfg_attr(
92 feature = "serde",
93 serde(rename = "call-info", default, skip_serializing_if = "Option::is_none")
94 )]
95 pub call_info: Option<CallInfo>,
96}
97
98impl Endpoint {
99 pub fn new(entity: impl Into<String>) -> Self {
101 Self {
102 entity: entity.into(),
103 state: None,
104 display_text: None,
105 referred: None,
106 status: None,
107 joining_method: None,
108 joining_info: None,
109 disconnection_method: None,
110 disconnection_info: None,
111 media: Vec::new(),
112 call_info: None,
113 }
114 }
115
116 pub fn with_status(mut self, status: EndpointStatus) -> Self {
118 self.status = Some(status);
119 self
120 }
121
122 pub fn with_joining_method(mut self, method: JoiningMethod) -> Self {
124 self.joining_method = Some(method);
125 self
126 }
127
128 pub fn with_joining_info(mut self, info: ExecutionInfo) -> Self {
130 self.joining_info = Some(info);
131 self
132 }
133
134 pub fn with_media(mut self, media: Media) -> Self {
136 self.media
137 .push(media);
138 self
139 }
140
141 pub fn with_call_info(mut self, call_info: CallInfo) -> Self {
143 self.call_info = Some(call_info);
144 self
145 }
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
150#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
151#[non_exhaustive]
152pub enum EndpointStatus {
153 #[cfg_attr(feature = "serde", serde(rename = "pending"))]
155 Pending,
156 #[cfg_attr(feature = "serde", serde(rename = "dialing-out"))]
158 DialingOut,
159 #[cfg_attr(feature = "serde", serde(rename = "dialing-in"))]
161 DialingIn,
162 #[cfg_attr(feature = "serde", serde(rename = "alerting"))]
164 Alerting,
165 #[cfg_attr(feature = "serde", serde(rename = "on-hold"))]
167 OnHold,
168 #[cfg_attr(feature = "serde", serde(rename = "connected"))]
170 Connected,
171 #[cfg_attr(feature = "serde", serde(rename = "muted-via-focus"))]
173 MutedViaFocus,
174 #[cfg_attr(feature = "serde", serde(rename = "disconnecting"))]
176 Disconnecting,
177 #[cfg_attr(feature = "serde", serde(rename = "disconnected"))]
179 Disconnected,
180}
181
182impl fmt::Display for EndpointStatus {
183 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184 f.write_str(match self {
185 Self::Pending => "pending",
186 Self::DialingOut => "dialing-out",
187 Self::DialingIn => "dialing-in",
188 Self::Alerting => "alerting",
189 Self::OnHold => "on-hold",
190 Self::Connected => "connected",
191 Self::MutedViaFocus => "muted-via-focus",
192 Self::Disconnecting => "disconnecting",
193 Self::Disconnected => "disconnected",
194 })
195 }
196}
197
198#[derive(Debug, Clone)]
200pub struct ParseEndpointStatusError(pub String);
201
202impl fmt::Display for ParseEndpointStatusError {
203 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204 write!(f, "invalid endpoint status: {:?}", self.0)
205 }
206}
207
208impl std::error::Error for ParseEndpointStatusError {}
209
210impl FromStr for EndpointStatus {
211 type Err = ParseEndpointStatusError;
212
213 fn from_str(s: &str) -> Result<Self, Self::Err> {
214 match s {
215 "pending" => Ok(Self::Pending),
216 "dialing-out" => Ok(Self::DialingOut),
217 "dialing-in" => Ok(Self::DialingIn),
218 "alerting" => Ok(Self::Alerting),
219 "on-hold" => Ok(Self::OnHold),
220 "connected" => Ok(Self::Connected),
221 "muted-via-focus" => Ok(Self::MutedViaFocus),
222 "disconnecting" => Ok(Self::Disconnecting),
223 "disconnected" => Ok(Self::Disconnected),
224 other => Err(ParseEndpointStatusError(other.to_owned())),
225 }
226 }
227}
228
229#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
231#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
232#[non_exhaustive]
233pub enum JoiningMethod {
234 #[cfg_attr(feature = "serde", serde(rename = "dialed-in"))]
236 DialedIn,
237 #[cfg_attr(feature = "serde", serde(rename = "dialed-out"))]
239 DialedOut,
240 #[cfg_attr(feature = "serde", serde(rename = "focus-owner"))]
242 FocusOwner,
243}
244
245impl fmt::Display for JoiningMethod {
246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247 f.write_str(match self {
248 Self::DialedIn => "dialed-in",
249 Self::DialedOut => "dialed-out",
250 Self::FocusOwner => "focus-owner",
251 })
252 }
253}
254
255#[derive(Debug, Clone)]
257pub struct ParseJoiningMethodError(pub String);
258
259impl fmt::Display for ParseJoiningMethodError {
260 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261 write!(f, "invalid joining method: {:?}", self.0)
262 }
263}
264
265impl std::error::Error for ParseJoiningMethodError {}
266
267impl FromStr for JoiningMethod {
268 type Err = ParseJoiningMethodError;
269
270 fn from_str(s: &str) -> Result<Self, Self::Err> {
271 match s {
272 "dialed-in" => Ok(Self::DialedIn),
273 "dialed-out" => Ok(Self::DialedOut),
274 "focus-owner" => Ok(Self::FocusOwner),
275 other => Err(ParseJoiningMethodError(other.to_owned())),
276 }
277 }
278}
279
280#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
282#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
283#[non_exhaustive]
284pub enum DisconnectionMethod {
285 #[cfg_attr(feature = "serde", serde(rename = "departed"))]
287 Departed,
288 #[cfg_attr(feature = "serde", serde(rename = "booted"))]
290 Booted,
291 #[cfg_attr(feature = "serde", serde(rename = "failed"))]
293 Failed,
294 #[cfg_attr(feature = "serde", serde(rename = "busy"))]
296 Busy,
297}
298
299impl fmt::Display for DisconnectionMethod {
300 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301 f.write_str(match self {
302 Self::Departed => "departed",
303 Self::Booted => "booted",
304 Self::Failed => "failed",
305 Self::Busy => "busy",
306 })
307 }
308}
309
310#[derive(Debug, Clone)]
312pub struct ParseDisconnectionMethodError(pub String);
313
314impl fmt::Display for ParseDisconnectionMethodError {
315 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
316 write!(f, "invalid disconnection method: {:?}", self.0)
317 }
318}
319
320impl std::error::Error for ParseDisconnectionMethodError {}
321
322impl FromStr for DisconnectionMethod {
323 type Err = ParseDisconnectionMethodError;
324
325 fn from_str(s: &str) -> Result<Self, Self::Err> {
326 match s {
327 "departed" => Ok(Self::Departed),
328 "booted" => Ok(Self::Booted),
329 "failed" => Ok(Self::Failed),
330 "busy" => Ok(Self::Busy),
331 other => Err(ParseDisconnectionMethodError(other.to_owned())),
332 }
333 }
334}
335
336#[derive(Debug, Clone, PartialEq, Eq, Default)]
338#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
339#[non_exhaustive]
340pub struct CallInfo {
341 #[cfg_attr(
343 feature = "serde",
344 serde(default, skip_serializing_if = "Option::is_none")
345 )]
346 pub sip: Option<SipDialogId>,
347}
348
349impl CallInfo {
350 pub fn with_sip(sip: SipDialogId) -> Self {
352 Self { sip: Some(sip) }
353 }
354}
355
356#[derive(Debug, Clone, PartialEq, Eq, Default)]
359#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
360#[non_exhaustive]
361pub struct SipDialogId {
362 #[cfg_attr(
364 feature = "serde",
365 serde(
366 rename = "display-text",
367 default,
368 skip_serializing_if = "Option::is_none"
369 )
370 )]
371 pub display_text: Option<String>,
372 #[cfg_attr(
374 feature = "serde",
375 serde(rename = "call-id", default, skip_serializing_if = "Option::is_none")
376 )]
377 pub call_id: Option<String>,
378 #[cfg_attr(
380 feature = "serde",
381 serde(rename = "from-tag", default, skip_serializing_if = "Option::is_none")
382 )]
383 pub from_tag: Option<String>,
384 #[cfg_attr(
386 feature = "serde",
387 serde(rename = "to-tag", default, skip_serializing_if = "Option::is_none")
388 )]
389 pub to_tag: Option<String>,
390}
391
392impl SipDialogId {
393 pub fn new(
395 call_id: impl Into<String>,
396 from_tag: impl Into<String>,
397 to_tag: impl Into<String>,
398 ) -> Self {
399 Self {
400 display_text: None,
401 call_id: Some(call_id.into()),
402 from_tag: Some(from_tag.into()),
403 to_tag: Some(to_tag.into()),
404 }
405 }
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411
412 #[test]
413 fn endpoint_status_round_trip() {
414 for (s, expected) in [
415 ("pending", EndpointStatus::Pending),
416 ("dialing-out", EndpointStatus::DialingOut),
417 ("dialing-in", EndpointStatus::DialingIn),
418 ("alerting", EndpointStatus::Alerting),
419 ("on-hold", EndpointStatus::OnHold),
420 ("connected", EndpointStatus::Connected),
421 ("muted-via-focus", EndpointStatus::MutedViaFocus),
422 ("disconnecting", EndpointStatus::Disconnecting),
423 ("disconnected", EndpointStatus::Disconnected),
424 ] {
425 let parsed: EndpointStatus = s
426 .parse()
427 .unwrap();
428 assert_eq!(parsed, expected);
429 assert_eq!(parsed.to_string(), s);
430 }
431 }
432
433 #[test]
434 fn joining_method_round_trip() {
435 for (s, expected) in [
436 ("dialed-in", JoiningMethod::DialedIn),
437 ("dialed-out", JoiningMethod::DialedOut),
438 ("focus-owner", JoiningMethod::FocusOwner),
439 ] {
440 let parsed: JoiningMethod = s
441 .parse()
442 .unwrap();
443 assert_eq!(parsed, expected);
444 assert_eq!(parsed.to_string(), s);
445 }
446 }
447
448 #[test]
449 fn disconnection_method_round_trip() {
450 for (s, expected) in [
451 ("departed", DisconnectionMethod::Departed),
452 ("booted", DisconnectionMethod::Booted),
453 ("failed", DisconnectionMethod::Failed),
454 ("busy", DisconnectionMethod::Busy),
455 ] {
456 let parsed: DisconnectionMethod = s
457 .parse()
458 .unwrap();
459 assert_eq!(parsed, expected);
460 assert_eq!(parsed.to_string(), s);
461 }
462 }
463
464 #[test]
465 fn sip_dialog_id_new() {
466 let id = SipDialogId::new("call-123", "from-abc", "to-xyz");
467 assert_eq!(
468 id.call_id
469 .as_deref(),
470 Some("call-123")
471 );
472 assert_eq!(
473 id.from_tag
474 .as_deref(),
475 Some("from-abc")
476 );
477 assert_eq!(
478 id.to_tag
479 .as_deref(),
480 Some("to-xyz")
481 );
482 assert!(id
483 .display_text
484 .is_none());
485 }
486
487 #[test]
488 fn endpoint_builder() {
489 let ep = Endpoint::new("sip:alice@example.com")
490 .with_status(EndpointStatus::Connected)
491 .with_joining_method(JoiningMethod::DialedIn)
492 .with_media(super::super::media::Media::new("1").with_type("audio"));
493 assert_eq!(ep.entity, "sip:alice@example.com");
494 assert_eq!(ep.status, Some(EndpointStatus::Connected));
495 assert_eq!(
496 ep.media
497 .len(),
498 1
499 );
500 }
501}