1use std::{borrow::Cow, collections::HashMap};
4
5use parse_display_derive::{Display, FromStr};
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8#[cfg(feature = "slog")]
9use slog::{Record, Serializer, KV};
10use uuid::Uuid;
11
12use crate::{
13 id::ModelingCmdId,
14 ok_response::OkModelingCmdResponse,
15 shared::{EngineErrorCode, ExportFile},
16 ModelingCmd,
17};
18
19#[derive(Display, FromStr, Copy, Eq, PartialEq, Debug, JsonSchema, Deserialize, Serialize, Clone, Ord, PartialOrd)]
21#[serde(rename_all = "snake_case")]
22#[cfg_attr(not(feature = "unstable_exhaustive"), non_exhaustive)]
23pub enum ErrorCode {
24 InternalEngine,
26 InternalApi,
28 BadRequest,
32 AuthTokenMissing,
34 AuthTokenInvalid,
36 InvalidJson,
38 InvalidBson,
40 WrongProtocol,
42 ConnectionProblem,
44 MessageTypeNotAccepted,
46 MessageTypeNotAcceptedForWebRTC,
49}
50
51impl From<EngineErrorCode> for ErrorCode {
54 fn from(value: EngineErrorCode) -> Self {
55 match value {
56 EngineErrorCode::InternalEngine => Self::InternalEngine,
57 EngineErrorCode::BadRequest => Self::BadRequest,
58 }
59 }
60}
61
62#[derive(Debug, Clone, Deserialize, Serialize)]
64#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
65#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
66pub struct ModelingCmdReq {
67 pub cmd: ModelingCmd,
69 pub cmd_id: ModelingCmdId,
71}
72
73#[allow(clippy::large_enum_variant)]
75#[derive(Serialize, Deserialize, Debug, Clone)]
76#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
77#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
78#[serde(tag = "type", rename_all = "snake_case")]
79#[cfg_attr(not(feature = "unstable_exhaustive"), non_exhaustive)]
80pub enum WebSocketRequest {
81 TrickleIce {
84 candidate: Box<RtcIceCandidateInit>,
86 },
87 SdpOffer {
89 offer: Box<RtcSessionDescription>,
91 },
92 ModelingCmdReq(ModelingCmdReq),
94 ModelingCmdBatchReq(ModelingBatch),
96 Ping {},
98
99 MetricsResponse {
101 metrics: Box<ClientMetrics>,
103 },
104
105 Debug {},
107
108 Headers {
110 headers: HashMap<String, String>,
112 },
113
114 #[cfg(feature = "exec-kcl")]
116 ExecKclProject {
117 request_id: Uuid,
119 project: crate::exec_kcl::KclProject,
121 },
122}
123
124#[derive(Serialize, Deserialize, Debug, Clone)]
126#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
127#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
128#[serde(rename_all = "snake_case")]
129pub struct ModelingBatch {
130 pub requests: Vec<ModelingCmdReq>,
132 pub batch_id: ModelingCmdId,
136 #[serde(default)]
139 pub responses: bool,
140}
141
142impl std::default::Default for ModelingBatch {
143 fn default() -> Self {
145 Self {
146 requests: Default::default(),
147 batch_id: Uuid::new_v4().into(),
148 responses: false,
149 }
150 }
151}
152
153impl ModelingBatch {
154 pub fn push(&mut self, req: ModelingCmdReq) {
156 self.requests.push(req);
157 }
158
159 pub fn is_empty(&self) -> bool {
161 self.requests.is_empty()
162 }
163}
164
165#[derive(serde::Serialize, serde::Deserialize, Debug, JsonSchema, Clone, PartialEq)]
169pub struct IceServer {
170 pub urls: Vec<String>,
174 pub credential: Option<String>,
176 pub username: Option<String>,
178}
179
180#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
182#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
183#[serde(tag = "type", content = "data", rename_all = "snake_case")]
184#[cfg_attr(not(feature = "unstable_exhaustive"), non_exhaustive)]
185pub enum OkWebSocketResponseData {
186 IceServerInfo {
188 ice_servers: Vec<IceServer>,
190 },
191 TrickleIce {
194 candidate: Box<RtcIceCandidateInit>,
196 },
197 SdpAnswer {
199 answer: Box<RtcSessionDescription>,
201 },
202 Modeling {
204 modeling_response: OkModelingCmdResponse,
206 },
207 ModelingBatch {
209 responses: HashMap<ModelingCmdId, BatchResponse>,
212 },
213 Export {
215 files: Vec<RawFile>,
217 },
218
219 MetricsRequest {},
221
222 ModelingSessionData {
224 session: ModelingSessionData,
226 },
227
228 Pong {},
230
231 Debug {
233 name: String,
235 },
236
237 #[cfg(feature = "exec-kcl")]
239 ExecKclProject {
240 result: Result<crate::exec_kcl::ExecKclProjectOk, crate::exec_kcl::ExecKclProjectErr>,
242 },
243}
244
245#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
247#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
248#[serde(rename_all = "snake_case")]
249pub struct SuccessWebSocketResponse {
250 pub success: bool,
252 pub request_id: Option<Uuid>,
256 pub resp: OkWebSocketResponseData,
259}
260
261#[derive(JsonSchema, Debug, Serialize, Deserialize, Clone, PartialEq)]
263#[serde(rename_all = "snake_case")]
264pub struct FailureWebSocketResponse {
265 pub success: bool,
267 pub request_id: Option<Uuid>,
271 pub errors: Vec<ApiError>,
273}
274
275#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
278#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
279#[serde(rename_all = "snake_case", untagged)]
280pub enum WebSocketResponse {
281 Success(SuccessWebSocketResponse),
283 Failure(FailureWebSocketResponse),
285}
286
287#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
290#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
291#[serde(rename_all = "snake_case", untagged)]
292pub enum BatchResponse {
293 Success {
295 response: OkModelingCmdResponse,
297 },
298 Failure {
300 errors: Vec<ApiError>,
302 },
303}
304
305impl WebSocketResponse {
306 pub fn success(request_id: Option<Uuid>, resp: OkWebSocketResponseData) -> Self {
308 Self::Success(SuccessWebSocketResponse {
309 success: true,
310 request_id,
311 resp,
312 })
313 }
314
315 pub fn failure(request_id: Option<Uuid>, errors: Vec<ApiError>) -> Self {
317 Self::Failure(FailureWebSocketResponse {
318 success: false,
319 request_id,
320 errors,
321 })
322 }
323
324 pub fn is_success(&self) -> bool {
326 matches!(self, Self::Success(_))
327 }
328
329 pub fn is_failure(&self) -> bool {
331 matches!(self, Self::Failure(_))
332 }
333
334 pub fn request_id(&self) -> Option<Uuid> {
336 match self {
337 WebSocketResponse::Success(x) => x.request_id,
338 WebSocketResponse::Failure(x) => x.request_id,
339 }
340 }
341}
342
343#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, PartialEq)]
346#[cfg_attr(
347 feature = "python",
348 pyo3::pyclass(from_py_object),
349 pyo3_stub_gen::derive::gen_stub_pyclass
350)]
351pub struct RawFile {
352 pub name: String,
354 #[serde(
356 serialize_with = "serde_bytes::serialize",
357 deserialize_with = "serde_bytes::deserialize"
358 )]
359 pub contents: Vec<u8>,
360}
361
362#[cfg(feature = "python")]
363#[pyo3_stub_gen::derive::gen_stub_pymethods]
364#[pyo3::pymethods]
365impl RawFile {
366 #[getter]
367 fn contents(&self) -> Vec<u8> {
368 self.contents.clone()
369 }
370
371 #[getter]
372 fn name(&self) -> String {
373 self.name.clone()
374 }
375}
376
377impl From<ExportFile> for RawFile {
378 fn from(f: ExportFile) -> Self {
379 Self {
380 name: f.name,
381 contents: f.contents.0,
382 }
383 }
384}
385
386#[derive(Debug, Serialize, Deserialize, JsonSchema)]
388pub struct LoggableApiError {
389 pub error: ApiError,
391 pub msg_internal: Option<Cow<'static, str>>,
393}
394
395#[cfg(feature = "slog")]
396impl KV for LoggableApiError {
397 fn serialize(&self, _rec: &Record, serializer: &mut dyn Serializer) -> slog::Result {
398 use slog::Key;
399 if let Some(ref msg_internal) = self.msg_internal {
400 serializer.emit_str(Key::from("msg_internal"), msg_internal)?;
401 }
402 serializer.emit_str(Key::from("msg_external"), &self.error.message)?;
403 serializer.emit_str(Key::from("error_code"), &self.error.error_code.to_string())
404 }
405}
406
407#[derive(Debug, Serialize, Deserialize, JsonSchema, Eq, PartialEq, Clone)]
409pub struct ApiError {
410 pub error_code: ErrorCode,
412 pub message: String,
414}
415
416impl ApiError {
417 pub fn no_internal_message(self) -> LoggableApiError {
419 LoggableApiError {
420 error: self,
421 msg_internal: None,
422 }
423 }
424 pub fn with_message(self, msg_internal: Cow<'static, str>) -> LoggableApiError {
426 LoggableApiError {
427 error: self,
428 msg_internal: Some(msg_internal),
429 }
430 }
431
432 pub fn should_log_internal_message(&self) -> bool {
434 use ErrorCode as Code;
435 match self.error_code {
436 Code::InternalEngine | Code::InternalApi => true,
438 Code::MessageTypeNotAcceptedForWebRTC
440 | Code::MessageTypeNotAccepted
441 | Code::BadRequest
442 | Code::WrongProtocol
443 | Code::AuthTokenMissing
444 | Code::AuthTokenInvalid
445 | Code::InvalidBson
446 | Code::InvalidJson => false,
447 Code::ConnectionProblem => cfg!(debug_assertions),
449 }
450 }
451}
452
453#[derive(Debug, Serialize, Deserialize, JsonSchema)]
456#[serde(rename_all = "snake_case", rename = "SnakeCaseResult")]
457pub enum SnakeCaseResult<T, E> {
458 Ok(T),
460 Err(E),
462}
463
464impl<T, E> From<SnakeCaseResult<T, E>> for Result<T, E> {
465 fn from(value: SnakeCaseResult<T, E>) -> Self {
466 match value {
467 SnakeCaseResult::Ok(x) => Self::Ok(x),
468 SnakeCaseResult::Err(x) => Self::Err(x),
469 }
470 }
471}
472
473impl<T, E> From<Result<T, E>> for SnakeCaseResult<T, E> {
474 fn from(value: Result<T, E>) -> Self {
475 match value {
476 Ok(x) => Self::Ok(x),
477 Err(x) => Self::Err(x),
478 }
479 }
480}
481
482#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
484#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
485pub struct ClientMetrics {
486 pub rtc_frames_dropped: Option<u32>,
491
492 pub rtc_frames_decoded: Option<u64>,
497
498 pub rtc_frames_received: Option<u64>,
503
504 pub rtc_frames_per_second: Option<u8>, pub rtc_freeze_count: Option<u32>,
516
517 pub rtc_jitter_sec: Option<f64>,
527
528 pub rtc_keyframes_decoded: Option<u32>,
538
539 pub rtc_total_freezes_duration_sec: Option<f32>,
543
544 pub rtc_frame_height: Option<u32>,
548
549 pub rtc_frame_width: Option<u32>,
553
554 pub rtc_packets_lost: Option<u32>,
558
559 pub rtc_pli_count: Option<u32>,
563
564 pub rtc_pause_count: Option<u32>,
568
569 pub rtc_total_pauses_duration_sec: Option<f32>,
573
574 pub rtc_stun_rtt_sec: Option<f32>,
582}
583
584#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
586pub struct RtcIceCandidate {
587 pub stats_id: String,
589 pub foundation: String,
591 pub priority: u32,
593 pub address: String,
595 pub protocol: RtcIceProtocol,
597 pub port: u16,
599 pub typ: RtcIceCandidateType,
601 pub component: u16,
603 pub related_address: String,
605 pub related_port: u16,
607 pub tcp_type: String,
609}
610
611#[cfg(feature = "webrtc")]
612impl From<webrtc::ice_transport::ice_candidate::RTCIceCandidate> for RtcIceCandidate {
613 fn from(candidate: webrtc::ice_transport::ice_candidate::RTCIceCandidate) -> Self {
614 Self {
615 stats_id: candidate.stats_id,
616 foundation: candidate.foundation,
617 priority: candidate.priority,
618 address: candidate.address,
619 protocol: candidate.protocol.into(),
620 port: candidate.port,
621 typ: candidate.typ.into(),
622 component: candidate.component,
623 related_address: candidate.related_address,
624 related_port: candidate.related_port,
625 tcp_type: candidate.tcp_type,
626 }
627 }
628}
629
630#[cfg(feature = "webrtc")]
631impl From<RtcIceCandidate> for webrtc::ice_transport::ice_candidate::RTCIceCandidate {
632 fn from(candidate: RtcIceCandidate) -> Self {
633 Self {
634 stats_id: candidate.stats_id,
635 foundation: candidate.foundation,
636 priority: candidate.priority,
637 address: candidate.address,
638 protocol: candidate.protocol.into(),
639 port: candidate.port,
640 typ: candidate.typ.into(),
641 component: candidate.component,
642 related_address: candidate.related_address,
643 related_port: candidate.related_port,
644 tcp_type: candidate.tcp_type,
645 }
646 }
647}
648
649#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
651#[serde(rename_all = "snake_case")]
652pub enum RtcIceCandidateType {
653 #[default]
655 Unspecified,
656
657 Host,
663
664 Srflx,
671
672 Prflx,
677
678 Relay,
682}
683
684#[cfg(feature = "webrtc")]
685impl From<webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType> for RtcIceCandidateType {
686 fn from(candidate_type: webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType) -> Self {
687 match candidate_type {
688 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Host => RtcIceCandidateType::Host,
689 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Srflx => RtcIceCandidateType::Srflx,
690 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Prflx => RtcIceCandidateType::Prflx,
691 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Relay => RtcIceCandidateType::Relay,
692 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Unspecified => {
693 RtcIceCandidateType::Unspecified
694 }
695 }
696 }
697}
698
699#[cfg(feature = "webrtc")]
700impl From<RtcIceCandidateType> for webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType {
701 fn from(candidate_type: RtcIceCandidateType) -> Self {
702 match candidate_type {
703 RtcIceCandidateType::Host => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Host,
704 RtcIceCandidateType::Srflx => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Srflx,
705 RtcIceCandidateType::Prflx => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Prflx,
706 RtcIceCandidateType::Relay => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Relay,
707 RtcIceCandidateType::Unspecified => {
708 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Unspecified
709 }
710 }
711 }
712}
713
714#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
717#[serde(rename_all = "snake_case")]
718pub enum RtcIceProtocol {
719 #[default]
721 Unspecified,
722
723 Udp,
725
726 Tcp,
728}
729
730#[cfg(feature = "webrtc")]
731impl From<webrtc::ice_transport::ice_protocol::RTCIceProtocol> for RtcIceProtocol {
732 fn from(protocol: webrtc::ice_transport::ice_protocol::RTCIceProtocol) -> Self {
733 match protocol {
734 webrtc::ice_transport::ice_protocol::RTCIceProtocol::Udp => RtcIceProtocol::Udp,
735 webrtc::ice_transport::ice_protocol::RTCIceProtocol::Tcp => RtcIceProtocol::Tcp,
736 webrtc::ice_transport::ice_protocol::RTCIceProtocol::Unspecified => RtcIceProtocol::Unspecified,
737 }
738 }
739}
740
741#[cfg(feature = "webrtc")]
742impl From<RtcIceProtocol> for webrtc::ice_transport::ice_protocol::RTCIceProtocol {
743 fn from(protocol: RtcIceProtocol) -> Self {
744 match protocol {
745 RtcIceProtocol::Udp => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Udp,
746 RtcIceProtocol::Tcp => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Tcp,
747 RtcIceProtocol::Unspecified => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Unspecified,
748 }
749 }
750}
751
752#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
754#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
755#[serde(rename_all = "camelCase")]
756pub struct RtcIceCandidateInit {
758 pub candidate: String,
760 pub sdp_mid: Option<String>,
763 #[serde(rename = "sdpMLineIndex")]
766 pub sdp_mline_index: Option<u16>,
767 pub username_fragment: Option<String>,
770}
771
772#[cfg(feature = "webrtc")]
773impl From<webrtc::ice_transport::ice_candidate::RTCIceCandidateInit> for RtcIceCandidateInit {
774 fn from(candidate: webrtc::ice_transport::ice_candidate::RTCIceCandidateInit) -> Self {
775 Self {
776 candidate: candidate.candidate,
777 sdp_mid: candidate.sdp_mid,
778 sdp_mline_index: candidate.sdp_mline_index,
779 username_fragment: candidate.username_fragment,
780 }
781 }
782}
783
784#[cfg(feature = "webrtc")]
785impl From<RtcIceCandidateInit> for webrtc::ice_transport::ice_candidate::RTCIceCandidateInit {
786 fn from(candidate: RtcIceCandidateInit) -> Self {
787 Self {
788 candidate: candidate.candidate,
789 sdp_mid: candidate.sdp_mid,
790 sdp_mline_index: candidate.sdp_mline_index,
791 username_fragment: candidate.username_fragment,
792 }
793 }
794}
795
796#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
798#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
799pub struct RtcSessionDescription {
800 #[serde(rename = "type")]
802 pub sdp_type: RtcSdpType,
803
804 pub sdp: String,
806}
807
808#[cfg(feature = "webrtc")]
809impl From<webrtc::peer_connection::sdp::session_description::RTCSessionDescription> for RtcSessionDescription {
810 fn from(desc: webrtc::peer_connection::sdp::session_description::RTCSessionDescription) -> Self {
811 Self {
812 sdp_type: desc.sdp_type.into(),
813 sdp: desc.sdp,
814 }
815 }
816}
817
818#[cfg(feature = "webrtc")]
819impl TryFrom<RtcSessionDescription> for webrtc::peer_connection::sdp::session_description::RTCSessionDescription {
820 type Error = anyhow::Error;
821
822 fn try_from(desc: RtcSessionDescription) -> Result<Self, Self::Error> {
823 let result = match desc.sdp_type {
824 RtcSdpType::Offer => {
825 webrtc::peer_connection::sdp::session_description::RTCSessionDescription::offer(desc.sdp)?
826 }
827 RtcSdpType::Pranswer => {
828 webrtc::peer_connection::sdp::session_description::RTCSessionDescription::pranswer(desc.sdp)?
829 }
830 RtcSdpType::Answer => {
831 webrtc::peer_connection::sdp::session_description::RTCSessionDescription::answer(desc.sdp)?
832 }
833 RtcSdpType::Rollback => anyhow::bail!("Rollback is not supported"),
834 RtcSdpType::Unspecified => anyhow::bail!("Unspecified is not supported"),
835 };
836
837 Ok(result)
838 }
839}
840
841#[derive(Default, Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, JsonSchema)]
843#[serde(rename_all = "snake_case")]
844#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
845pub enum RtcSdpType {
846 #[default]
848 Unspecified = 0,
849
850 Offer,
852
853 Pranswer,
858
859 Answer,
864
865 Rollback,
871}
872
873#[cfg(feature = "webrtc")]
874impl From<webrtc::peer_connection::sdp::sdp_type::RTCSdpType> for RtcSdpType {
875 fn from(sdp_type: webrtc::peer_connection::sdp::sdp_type::RTCSdpType) -> Self {
876 match sdp_type {
877 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Offer => Self::Offer,
878 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Pranswer => Self::Pranswer,
879 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Answer => Self::Answer,
880 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Rollback => Self::Rollback,
881 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Unspecified => Self::Unspecified,
882 }
883 }
884}
885
886#[cfg(feature = "webrtc")]
887impl From<RtcSdpType> for webrtc::peer_connection::sdp::sdp_type::RTCSdpType {
888 fn from(sdp_type: RtcSdpType) -> Self {
889 match sdp_type {
890 RtcSdpType::Offer => Self::Offer,
891 RtcSdpType::Pranswer => Self::Pranswer,
892 RtcSdpType::Answer => Self::Answer,
893 RtcSdpType::Rollback => Self::Rollback,
894 RtcSdpType::Unspecified => Self::Unspecified,
895 }
896 }
897}
898#[derive(JsonSchema, Debug, Serialize, Deserialize, Clone, PartialEq)]
900#[serde(rename_all = "snake_case")]
901pub struct ModelingSessionData {
902 pub api_call_id: String,
905}
906
907#[cfg(test)]
908mod tests {
909 use super::*;
910 use crate::output;
911
912 const REQ_ID: Uuid = uuid::uuid!("cc30d5e2-482b-4498-b5d2-6131c30a50a4");
913
914 #[test]
915 fn serialize_websocket_modeling_ok() {
916 let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
917 success: true,
918 request_id: Some(REQ_ID),
919 resp: OkWebSocketResponseData::Modeling {
920 modeling_response: OkModelingCmdResponse::CurveGetControlPoints(output::CurveGetControlPoints {
921 control_points: vec![],
922 }),
923 },
924 });
925 let expected = serde_json::json!({
926 "success": true,
927 "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
928 "resp": {
929 "type": "modeling",
930 "data": {
931 "modeling_response": {
932 "type": "curve_get_control_points",
933 "data": { "control_points": [] }
934 }
935 }
936 }
937 });
938 assert_json_eq(actual, expected);
939 }
940
941 #[test]
942 fn serialize_websocket_webrtc_ok() {
943 let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
944 success: true,
945 request_id: Some(REQ_ID),
946 resp: OkWebSocketResponseData::IceServerInfo { ice_servers: vec![] },
947 });
948 let expected = serde_json::json!({
949 "success": true,
950 "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
951 "resp": {
952 "type": "ice_server_info",
953 "data": {
954 "ice_servers": []
955 }
956 }
957 });
958 assert_json_eq(actual, expected);
959 }
960
961 #[test]
962 fn serialize_websocket_export_ok() {
963 let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
964 success: true,
965 request_id: Some(REQ_ID),
966 resp: OkWebSocketResponseData::Export { files: vec![] },
967 });
968 let expected = serde_json::json!({
969 "success": true,
970 "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
971 "resp": {
972 "type": "export",
973 "data": {"files": [] }
974 }
975 });
976 assert_json_eq(actual, expected);
977 }
978
979 #[test]
980 fn serialize_websocket_err() {
981 let actual = WebSocketResponse::Failure(FailureWebSocketResponse {
982 success: false,
983 request_id: Some(REQ_ID),
984 errors: vec![ApiError {
985 error_code: ErrorCode::InternalApi,
986 message: "you fucked up!".to_owned(),
987 }],
988 });
989 let expected = serde_json::json!({
990 "success": false,
991 "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
992 "errors": [
993 {
994 "error_code": "internal_api",
995 "message": "you fucked up!"
996 }
997 ],
998 });
999 assert_json_eq(actual, expected);
1000 }
1001
1002 #[test]
1003 fn serialize_websocket_metrics() {
1004 let actual = WebSocketRequest::MetricsResponse {
1005 metrics: Box::new(ClientMetrics {
1006 rtc_frames_dropped: Some(1),
1007 rtc_frames_decoded: Some(2),
1008 rtc_frames_per_second: Some(3),
1009 rtc_frames_received: Some(4),
1010 rtc_freeze_count: Some(5),
1011 rtc_jitter_sec: Some(6.7),
1012 rtc_keyframes_decoded: Some(8),
1013 rtc_total_freezes_duration_sec: Some(9.1),
1014 rtc_frame_height: Some(100),
1015 rtc_frame_width: Some(100),
1016 rtc_packets_lost: Some(0),
1017 rtc_pli_count: Some(0),
1018 rtc_pause_count: Some(0),
1019 rtc_total_pauses_duration_sec: Some(0.0),
1020 rtc_stun_rtt_sec: Some(0.005),
1021 }),
1022 };
1023 let expected = serde_json::json!({
1024 "type": "metrics_response",
1025 "metrics": {
1026 "rtc_frames_dropped": 1,
1027 "rtc_frames_decoded": 2,
1028 "rtc_frames_per_second": 3,
1029 "rtc_frames_received": 4,
1030 "rtc_freeze_count": 5,
1031 "rtc_jitter_sec": 6.7,
1032 "rtc_keyframes_decoded": 8,
1033 "rtc_total_freezes_duration_sec": 9.1,
1034 "rtc_frame_height": 100,
1035 "rtc_frame_width": 100,
1036 "rtc_packets_lost": 0,
1037 "rtc_pli_count": 0,
1038 "rtc_pause_count": 0,
1039 "rtc_total_pauses_duration_sec": 0.0,
1040 "rtc_stun_rtt_sec": 0.005,
1041 },
1042 });
1043 assert_json_eq(actual, expected);
1044 }
1045
1046 fn assert_json_eq<T: Serialize>(actual: T, expected: serde_json::Value) {
1047 let json_str = serde_json::to_string(&actual).unwrap();
1048 let actual: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1049 assert_eq!(actual, expected, "got\n{actual:#}\n, expected\n{expected:#}\n");
1050 }
1051}