medea_control_api_proto/grpc/convert/
api.rs

1//! Conversions between [`ControlApi`] types and the ones generated from
2//! `api.proto` [gRPC] spec.
3//!
4//! [`ControlApi`]: crate::ControlApi
5//! [gRPC]: https://grpc.io
6
7use std::time::Duration;
8
9use crate::{
10    Element, Endpoint, Fid, Member, Ping, Pong, Room,
11    control::{ParseFidError, Request},
12    endpoint::{
13        self, web_rtc_play,
14        web_rtc_play::WebRtcPlay,
15        web_rtc_publish::{
16            self, AudioSettings, P2pMode, Policy, VideoSettings, WebRtcPublish,
17        },
18    },
19    grpc::{CallbackUrlParseError, api as proto, convert::ProtobufError},
20    member::{self, Credentials},
21    room,
22};
23
24impl TryFrom<(Fid, Element)> for proto::Element {
25    type Error = ProtobufError;
26
27    fn try_from((fid, spec): (Fid, Element)) -> Result<Self, Self::Error> {
28        use proto::element::El;
29
30        let el = match (fid, spec) {
31            (Fid::Room { id }, Element::Room(room)) => {
32                Ok(El::Room(Room { id, spec: room.spec }.into()))
33            }
34            (Fid::Member { id, .. }, Element::Member(member)) => {
35                Ok(El::Member(Member { id, spec: member.spec }.into()))
36            }
37            (Fid::Endpoint { id, .. }, Element::Endpoint(endpoint)) => {
38                Ok(Endpoint { id, spec: endpoint.spec }.into())
39            }
40            (id @ Fid::Room { .. }, _) => Err(ProtobufError::ExpectedElement(
41                "Room",
42                id.to_string().into(),
43            )),
44            (id @ Fid::Member { .. }, _) => Err(
45                ProtobufError::ExpectedElement("Member", id.to_string().into()),
46            ),
47
48            (id @ Fid::Endpoint { .. }, _) => {
49                Err(ProtobufError::ExpectedElement(
50                    "Endpoint",
51                    id.to_string().into(),
52                ))
53            }
54        }?;
55
56        Ok(Self { el: Some(el) })
57    }
58}
59
60impl TryFrom<proto::CreateRequest> for Request {
61    type Error = ProtobufError;
62
63    fn try_from(req: proto::CreateRequest) -> Result<Self, Self::Error> {
64        let parent_fid = req.parent_fid;
65        let el = req.el.ok_or_else(|| {
66            ProtobufError::NoElementForId(parent_fid.as_str().into())
67        })?;
68
69        if parent_fid.is_empty() {
70            return Room::try_from(el)
71                .map(|room| Self::Room { id: room.id, spec: room.spec });
72        }
73
74        match parent_fid.parse::<Fid>()? {
75            Fid::Room { id: room_id } => {
76                Member::try_from(el).map(|member| Self::Member {
77                    id: member.id,
78                    room_id,
79                    spec: Box::new(member.spec),
80                })
81            }
82            Fid::Member { id: member_id, room_id } => Endpoint::try_from(el)
83                .map(|endpoint| Self::Endpoint {
84                    id: endpoint.id,
85                    room_id,
86                    member_id,
87                    spec: endpoint.spec,
88                }),
89            Fid::Endpoint { .. } => Err(ProtobufError::ParseFidErr(
90                ParseFidError::TooManyPaths(parent_fid.into()),
91            )),
92        }
93    }
94}
95
96impl TryFrom<proto::ApplyRequest> for Request {
97    type Error = ProtobufError;
98
99    fn try_from(req: proto::ApplyRequest) -> Result<Self, Self::Error> {
100        let parent_fid = req.parent_fid;
101        let el = req.el.ok_or_else(|| {
102            ProtobufError::NoElementForId(parent_fid.as_str().into())
103        })?;
104
105        match parent_fid.parse::<Fid>()? {
106            Fid::Room { .. } => Room::try_from(el)
107                .map(|room| Self::Room { id: room.id, spec: room.spec }),
108            Fid::Member { room_id, .. } => {
109                Member::try_from(el).map(|member| Self::Member {
110                    id: member.id,
111                    room_id,
112                    spec: Box::new(member.spec),
113                })
114            }
115            Fid::Endpoint { .. } => Err(ProtobufError::Unimplemented),
116        }
117    }
118}
119
120impl From<Request> for proto::CreateRequest {
121    fn from(req: Request) -> Self {
122        let (parent_fid, el) = match req {
123            Request::Room { id, spec } => {
124                (String::new(), Room { id, spec }.into())
125            }
126            Request::Member { id, room_id, spec } => (
127                Fid::Room { id: room_id }.to_string(),
128                Member { id, spec: *spec }.into(),
129            ),
130            Request::Endpoint { id, room_id, member_id, spec } => (
131                Fid::Member { id: member_id, room_id }.to_string(),
132                Endpoint { id, spec }.into(),
133            ),
134        };
135
136        Self { parent_fid, el: Some(el) }
137    }
138}
139
140impl From<Request> for proto::ApplyRequest {
141    fn from(req: Request) -> Self {
142        let (parent_fid, el) = match req {
143            Request::Room { id, spec } => {
144                (Fid::Room { id: id.clone() }, Room { id, spec }.into())
145            }
146            Request::Member { id, room_id, spec } => (
147                Fid::Member { id: id.clone(), room_id },
148                Member { id, spec: *spec }.into(),
149            ),
150            Request::Endpoint { id, room_id, member_id, spec } => (
151                Fid::Endpoint { id: id.clone(), room_id, member_id },
152                Endpoint { id, spec }.into(),
153            ),
154        };
155
156        Self { parent_fid: parent_fid.to_string(), el: Some(el) }
157    }
158}
159
160impl TryFrom<proto::Element> for Element {
161    type Error = ProtobufError;
162
163    fn try_from(el: proto::Element) -> Result<Self, Self::Error> {
164        use proto::element::El;
165
166        Ok(match el.el.ok_or(ProtobufError::NoElement)? {
167            El::Member(member) => {
168                Self::Member(Box::new(Member::try_from(member)?))
169            }
170            El::Room(room) => Self::Room(Room::try_from(room)?),
171            El::WebrtcPlay(play) => {
172                Self::Endpoint(WebRtcPlay::try_from(play)?.into())
173            }
174            El::WebrtcPub(publish) => {
175                Self::Endpoint(WebRtcPublish::try_from(publish)?.into())
176            }
177        })
178    }
179}
180
181impl TryFrom<proto::create_request::El> for Room {
182    type Error = ProtobufError;
183
184    fn try_from(val: proto::create_request::El) -> Result<Self, Self::Error> {
185        use proto::create_request::El;
186
187        match val {
188            El::Room(room) => room.try_into(),
189            El::Member(proto::Member { id, .. })
190            | El::WebrtcPub(proto::WebRtcPublishEndpoint { id, .. })
191            | El::WebrtcPlay(proto::WebRtcPlayEndpoint { id, .. }) => {
192                Err(ProtobufError::ExpectedElement("Room", id.into()))
193            }
194        }
195    }
196}
197
198impl TryFrom<proto::apply_request::El> for Room {
199    type Error = ProtobufError;
200
201    fn try_from(val: proto::apply_request::El) -> Result<Self, Self::Error> {
202        use proto::apply_request::El;
203
204        match val {
205            El::Room(room) => room.try_into(),
206            El::Member(proto::Member { id, .. })
207            | El::WebrtcPub(proto::WebRtcPublishEndpoint { id, .. })
208            | El::WebrtcPlay(proto::WebRtcPlayEndpoint { id, .. }) => {
209                Err(ProtobufError::ExpectedElement("Room", id.into()))
210            }
211        }
212    }
213}
214
215impl TryFrom<proto::Room> for Room {
216    type Error = ProtobufError;
217
218    fn try_from(room: proto::Room) -> Result<Self, Self::Error> {
219        Ok(Self {
220            id: room.id.into(),
221            spec: room::Spec {
222                pipeline: room
223                    .pipeline
224                    .into_iter()
225                    .map(|(id, room_el)| {
226                        room_el.el.map_or(
227                            Err(ProtobufError::NoElementForId(id.into())),
228                            |el| {
229                                Member::try_from(el)
230                                    .map(|m| (m.id, m.spec.into()))
231                            },
232                        )
233                    })
234                    .collect::<Result<_, _>>()?,
235            },
236        })
237    }
238}
239
240impl From<Room> for proto::create_request::El {
241    fn from(room: Room) -> Self {
242        Self::Room(room.into())
243    }
244}
245
246impl From<Room> for proto::apply_request::El {
247    fn from(room: Room) -> Self {
248        Self::Room(room.into())
249    }
250}
251
252impl From<Room> for proto::Room {
253    fn from(room: Room) -> Self {
254        Self {
255            id: room.id.into(),
256            pipeline: room
257                .spec
258                .pipeline
259                .into_iter()
260                .map(|(id, el)| match el {
261                    room::PipelineSpec::Member(spec) => {
262                        (id.clone().into(), Member { id, spec }.into())
263                    }
264                })
265                .collect(),
266        }
267    }
268}
269
270impl TryFrom<proto::create_request::El> for Member {
271    type Error = ProtobufError;
272
273    fn try_from(val: proto::create_request::El) -> Result<Self, Self::Error> {
274        use proto::create_request::El;
275
276        match val {
277            El::Member(member) => member.try_into(),
278            El::Room(proto::Room { id, .. })
279            | El::WebrtcPub(proto::WebRtcPublishEndpoint { id, .. })
280            | El::WebrtcPlay(proto::WebRtcPlayEndpoint { id, .. }) => {
281                Err(ProtobufError::ExpectedElement("Member", id.into()))
282            }
283        }
284    }
285}
286
287impl TryFrom<proto::apply_request::El> for Member {
288    type Error = ProtobufError;
289
290    fn try_from(val: proto::apply_request::El) -> Result<Self, Self::Error> {
291        use proto::apply_request::El;
292
293        match val {
294            El::Member(member) => member.try_into(),
295            El::Room(proto::Room { id, .. })
296            | El::WebrtcPub(proto::WebRtcPublishEndpoint { id, .. })
297            | El::WebrtcPlay(proto::WebRtcPlayEndpoint { id, .. }) => {
298                Err(ProtobufError::ExpectedElement("Member", id.into()))
299            }
300        }
301    }
302}
303
304impl TryFrom<proto::room::element::El> for Member {
305    type Error = ProtobufError;
306
307    fn try_from(val: proto::room::element::El) -> Result<Self, Self::Error> {
308        use proto::room::element::El;
309
310        match val {
311            El::Member(member) => member.try_into(),
312            El::WebrtcPub(proto::WebRtcPublishEndpoint { id, .. })
313            | El::WebrtcPlay(proto::WebRtcPlayEndpoint { id, .. }) => {
314                Err(ProtobufError::ExpectedElement("Member", id.into()))
315            }
316        }
317    }
318}
319
320impl TryFrom<proto::Member> for Member {
321    type Error = ProtobufError;
322
323    fn try_from(member: proto::Member) -> Result<Self, Self::Error> {
324        /// Tries to parse [`Duration`] and maps its error into a
325        /// [`TryFromProtobufError`].
326        fn parse_duration<T: TryInto<Duration>>(
327            duration: Option<T>,
328            member_id: &str,
329            field: &'static str,
330        ) -> Result<Option<Duration>, ProtobufError> {
331            #[expect(clippy::map_err_ignore, reason = "not useful")]
332            duration.map(TryInto::try_into).transpose().map_err(|_| {
333                ProtobufError::InvalidDuration(member_id.into(), field)
334            })
335        }
336
337        let idle_timeout =
338            parse_duration(member.idle_timeout, &member.id, "idle_timeout")?;
339        let reconnect_timeout = parse_duration(
340            member.reconnect_timeout,
341            &member.id,
342            "reconnect_timeout",
343        )?;
344        let ping_interval =
345            parse_duration(member.ping_interval, &member.id, "ping_interval")?;
346
347        Ok(Self {
348            id: member.id.into(),
349            spec: member::Spec {
350                pipeline: member
351                    .pipeline
352                    .into_iter()
353                    .map(|(id, member_el)| {
354                        member_el.el.map_or_else(
355                            || Err(ProtobufError::NoElementForId(id.into())),
356                            |el| Endpoint::try_from(el).map(|e| (e.id, e.spec)),
357                        )
358                    })
359                    .collect::<Result<_, _>>()?,
360                credentials: member.credentials.map(Into::into),
361                on_join: (!member.on_join.is_empty())
362                    .then(|| member.on_join.parse())
363                    .transpose()
364                    .map_err(CallbackUrlParseError::from)?,
365                on_leave: (!member.on_leave.is_empty())
366                    .then(|| member.on_leave.parse())
367                    .transpose()
368                    .map_err(CallbackUrlParseError::from)?,
369                idle_timeout,
370                reconnect_timeout,
371                ping_interval,
372            },
373        })
374    }
375}
376
377impl From<Member> for proto::Member {
378    fn from(member: Member) -> Self {
379        #[expect( // intentional
380            clippy::unwrap_used,
381            reason = " `Duration` values are not expected to be large"
382        )]
383        Self {
384            id: member.id.into(),
385            on_join: member
386                .spec
387                .on_join
388                .as_ref()
389                .map_or_else(String::default, ToString::to_string),
390            on_leave: member
391                .spec
392                .on_leave
393                .as_ref()
394                .map_or_else(String::default, ToString::to_string),
395            idle_timeout: member
396                .spec
397                .idle_timeout
398                .map(|d| d.try_into().unwrap()),
399            reconnect_timeout: member
400                .spec
401                .reconnect_timeout
402                .map(|d| d.try_into().unwrap()),
403            ping_interval: member
404                .spec
405                .ping_interval
406                .map(|d| d.try_into().unwrap()),
407            pipeline: member
408                .spec
409                .pipeline
410                .into_iter()
411                .map(|(id, spec)| {
412                    (id.clone().into(), Endpoint { id, spec }.into())
413                })
414                .collect(),
415            credentials: member.spec.credentials.map(Into::into),
416        }
417    }
418}
419
420impl From<Member> for proto::create_request::El {
421    fn from(member: Member) -> Self {
422        Self::Member(member.into())
423    }
424}
425
426impl From<Member> for proto::apply_request::El {
427    fn from(member: Member) -> Self {
428        Self::Member(member.into())
429    }
430}
431
432impl From<Member> for proto::room::Element {
433    fn from(member: Member) -> Self {
434        Self { el: Some(proto::room::element::El::Member(member.into())) }
435    }
436}
437
438impl From<proto::member::Credentials> for Credentials {
439    fn from(val: proto::member::Credentials) -> Self {
440        use proto::member::Credentials as Creds;
441
442        match val {
443            Creds::Hash(hash) => Self::Hash(hash.into()),
444            Creds::Plain(plain) => Self::Plain(plain.into()),
445        }
446    }
447}
448
449impl From<Credentials> for proto::member::Credentials {
450    fn from(creds: Credentials) -> Self {
451        match creds {
452            Credentials::Hash(hash) => Self::Hash(hash.into()),
453            Credentials::Plain(plain) => Self::Plain(plain.expose_str().into()),
454        }
455    }
456}
457
458impl TryFrom<proto::create_request::El> for Endpoint {
459    type Error = ProtobufError;
460
461    fn try_from(val: proto::create_request::El) -> Result<Self, Self::Error> {
462        use proto::create_request::El;
463
464        match val {
465            El::WebrtcPlay(play) => WebRtcPlay::try_from(play).map(Self::from),
466            El::WebrtcPub(publish) => {
467                WebRtcPublish::try_from(publish).map(Self::from)
468            }
469            El::Room(proto::Room { id, .. })
470            | El::Member(proto::Member { id, .. }) => {
471                Err(ProtobufError::ExpectedElement("Endpoint", id.into()))
472            }
473        }
474    }
475}
476
477impl TryFrom<proto::member::element::El> for Endpoint {
478    type Error = ProtobufError;
479
480    fn try_from(val: proto::member::element::El) -> Result<Self, Self::Error> {
481        use proto::member::element::El;
482
483        match val {
484            El::WebrtcPub(e) => WebRtcPublish::try_from(e).map(Self::from),
485            El::WebrtcPlay(e) => WebRtcPlay::try_from(e).map(Self::from),
486        }
487    }
488}
489
490impl From<Endpoint> for proto::member::Element {
491    fn from(endpoint: Endpoint) -> Self {
492        use proto::member::element::El;
493
494        Self {
495            el: Some(match endpoint.spec {
496                endpoint::Spec::WebRtcPublishEndpoint(spec) => El::WebrtcPub(
497                    WebRtcPublish {
498                        id: String::from(endpoint.id).into(),
499                        spec,
500                    }
501                    .into(),
502                ),
503                endpoint::Spec::WebRtcPlayEndpoint(spec) => El::WebrtcPlay(
504                    WebRtcPlay { id: String::from(endpoint.id).into(), spec }
505                        .into(),
506                ),
507            }),
508        }
509    }
510}
511
512impl From<Endpoint> for proto::element::El {
513    fn from(endpoint: Endpoint) -> Self {
514        match endpoint.spec {
515            endpoint::Spec::WebRtcPublishEndpoint(spec) => Self::WebrtcPub(
516                WebRtcPublish { id: String::from(endpoint.id).into(), spec }
517                    .into(),
518            ),
519            endpoint::Spec::WebRtcPlayEndpoint(spec) => Self::WebrtcPlay(
520                WebRtcPlay { id: String::from(endpoint.id).into(), spec }
521                    .into(),
522            ),
523        }
524    }
525}
526
527impl From<Endpoint> for proto::create_request::El {
528    fn from(endpoint: Endpoint) -> Self {
529        match endpoint.spec {
530            endpoint::Spec::WebRtcPublishEndpoint(spec) => Self::WebrtcPub(
531                WebRtcPublish { id: String::from(endpoint.id).into(), spec }
532                    .into(),
533            ),
534            endpoint::Spec::WebRtcPlayEndpoint(spec) => Self::WebrtcPlay(
535                WebRtcPlay { id: String::from(endpoint.id).into(), spec }
536                    .into(),
537            ),
538        }
539    }
540}
541
542impl From<Endpoint> for proto::apply_request::El {
543    fn from(endpoint: Endpoint) -> Self {
544        match endpoint.spec {
545            endpoint::Spec::WebRtcPublishEndpoint(spec) => Self::WebrtcPub(
546                WebRtcPublish { id: String::from(endpoint.id).into(), spec }
547                    .into(),
548            ),
549            endpoint::Spec::WebRtcPlayEndpoint(spec) => Self::WebrtcPlay(
550                WebRtcPlay { id: String::from(endpoint.id).into(), spec }
551                    .into(),
552            ),
553        }
554    }
555}
556
557impl TryFrom<proto::WebRtcPlayEndpoint> for WebRtcPlay {
558    type Error = ProtobufError;
559
560    fn try_from(val: proto::WebRtcPlayEndpoint) -> Result<Self, Self::Error> {
561        Ok(Self {
562            id: val.id.into(),
563            spec: web_rtc_play::Spec {
564                src: val.src.parse()?,
565                force_relay: val.force_relay,
566            },
567        })
568    }
569}
570
571impl From<WebRtcPlay> for proto::WebRtcPlayEndpoint {
572    fn from(play: WebRtcPlay) -> Self {
573        Self {
574            id: play.id.into(),
575            src: play.spec.src.to_string(),
576            on_start: String::new(),
577            on_stop: String::new(),
578            force_relay: play.spec.force_relay,
579        }
580    }
581}
582
583impl TryFrom<proto::WebRtcPublishEndpoint> for WebRtcPublish {
584    type Error = ProtobufError;
585
586    fn try_from(
587        val: proto::WebRtcPublishEndpoint,
588    ) -> Result<Self, Self::Error> {
589        use proto::web_rtc_publish_endpoint::P2p;
590
591        Ok(Self {
592            id: val.id.into(),
593            spec: web_rtc_publish::Spec {
594                p2p: P2p::try_from(val.p2p).unwrap_or_default().into(),
595                audio_settings: val
596                    .audio_settings
597                    .map(AudioSettings::from)
598                    .unwrap_or_default(),
599                video_settings: val
600                    .video_settings
601                    .map(VideoSettings::from)
602                    .unwrap_or_default(),
603                force_relay: val.force_relay,
604            },
605        })
606    }
607}
608
609impl From<WebRtcPublish> for proto::WebRtcPublishEndpoint {
610    fn from(publish: WebRtcPublish) -> Self {
611        use proto::web_rtc_publish_endpoint::P2p;
612
613        Self {
614            id: publish.id.into(),
615            p2p: P2p::from(publish.spec.p2p).into(),
616            on_start: String::new(),
617            on_stop: String::new(),
618            force_relay: publish.spec.force_relay,
619            audio_settings: Some(publish.spec.audio_settings.into()),
620            video_settings: Some(publish.spec.video_settings.into()),
621        }
622    }
623}
624
625impl From<proto::web_rtc_publish_endpoint::AudioSettings> for AudioSettings {
626    fn from(val: proto::web_rtc_publish_endpoint::AudioSettings) -> Self {
627        use proto::web_rtc_publish_endpoint::PublishPolicy;
628
629        Self {
630            publish_policy: PublishPolicy::try_from(val.publish_policy)
631                .unwrap_or_default()
632                .into(),
633        }
634    }
635}
636
637impl From<AudioSettings> for proto::web_rtc_publish_endpoint::AudioSettings {
638    fn from(settings: AudioSettings) -> Self {
639        use proto::web_rtc_publish_endpoint::PublishPolicy;
640
641        Self {
642            publish_policy: PublishPolicy::from(settings.publish_policy).into(),
643        }
644    }
645}
646
647impl From<proto::web_rtc_publish_endpoint::VideoSettings> for VideoSettings {
648    fn from(val: proto::web_rtc_publish_endpoint::VideoSettings) -> Self {
649        use proto::web_rtc_publish_endpoint::PublishPolicy;
650
651        Self {
652            publish_policy: PublishPolicy::try_from(val.publish_policy)
653                .unwrap_or_default()
654                .into(),
655        }
656    }
657}
658
659impl From<VideoSettings> for proto::web_rtc_publish_endpoint::VideoSettings {
660    fn from(settings: VideoSettings) -> Self {
661        use proto::web_rtc_publish_endpoint::PublishPolicy;
662
663        Self {
664            publish_policy: PublishPolicy::from(settings.publish_policy).into(),
665        }
666    }
667}
668
669impl From<proto::web_rtc_publish_endpoint::PublishPolicy> for Policy {
670    fn from(val: proto::web_rtc_publish_endpoint::PublishPolicy) -> Self {
671        use proto::web_rtc_publish_endpoint::PublishPolicy as Proto;
672
673        match val {
674            Proto::Optional => Self::Optional,
675            Proto::Required => Self::Required,
676            Proto::Disabled => Self::Disabled,
677        }
678    }
679}
680
681impl From<Policy> for proto::web_rtc_publish_endpoint::PublishPolicy {
682    fn from(val: Policy) -> Self {
683        match val {
684            Policy::Optional => Self::Optional,
685            Policy::Required => Self::Required,
686            Policy::Disabled => Self::Disabled,
687        }
688    }
689}
690
691impl From<proto::web_rtc_publish_endpoint::P2p> for P2pMode {
692    fn from(val: proto::web_rtc_publish_endpoint::P2p) -> Self {
693        use proto::web_rtc_publish_endpoint::P2p;
694
695        match val {
696            P2p::Always => Self::Always,
697            P2p::IfPossible => Self::IfPossible,
698            P2p::Never => Self::Never,
699        }
700    }
701}
702
703impl From<P2pMode> for proto::web_rtc_publish_endpoint::P2p {
704    fn from(val: P2pMode) -> Self {
705        match val {
706            P2pMode::Always => Self::Always,
707            P2pMode::IfPossible => Self::IfPossible,
708            P2pMode::Never => Self::Never,
709        }
710    }
711}
712
713impl From<proto::Ping> for Ping {
714    fn from(val: proto::Ping) -> Self {
715        Self(val.nonce)
716    }
717}
718
719impl From<Ping> for proto::Ping {
720    fn from(val: Ping) -> Self {
721        Self { nonce: val.0 }
722    }
723}
724
725impl From<proto::Pong> for Pong {
726    fn from(val: proto::Pong) -> Self {
727        Self(val.nonce)
728    }
729}
730
731impl From<Pong> for proto::Pong {
732    fn from(val: Pong) -> Self {
733        Self { nonce: val.0 }
734    }
735}