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 Reconnect {},
248}
249
250#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
252#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
253#[serde(rename_all = "snake_case")]
254pub struct SuccessWebSocketResponse {
255 pub success: bool,
257 pub request_id: Option<Uuid>,
261 pub resp: OkWebSocketResponseData,
264}
265
266#[derive(JsonSchema, Debug, Serialize, Deserialize, Clone, PartialEq)]
268#[serde(rename_all = "snake_case")]
269pub struct FailureWebSocketResponse {
270 pub success: bool,
272 pub request_id: Option<Uuid>,
276 pub errors: Vec<ApiError>,
278}
279
280#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
283#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
284#[serde(rename_all = "snake_case", untagged)]
285pub enum WebSocketResponse {
286 Success(SuccessWebSocketResponse),
288 Failure(FailureWebSocketResponse),
290}
291
292#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
295#[cfg_attr(feature = "derive-jsonschema-on-enums", derive(schemars::JsonSchema))]
296#[serde(rename_all = "snake_case", untagged)]
297pub enum BatchResponse {
298 Success {
300 response: OkModelingCmdResponse,
302 },
303 Failure {
305 errors: Vec<ApiError>,
307 },
308}
309
310impl WebSocketResponse {
311 pub fn success(request_id: Option<Uuid>, resp: OkWebSocketResponseData) -> Self {
313 Self::Success(SuccessWebSocketResponse {
314 success: true,
315 request_id,
316 resp,
317 })
318 }
319
320 pub fn failure(request_id: Option<Uuid>, errors: Vec<ApiError>) -> Self {
322 Self::Failure(FailureWebSocketResponse {
323 success: false,
324 request_id,
325 errors,
326 })
327 }
328
329 pub fn is_success(&self) -> bool {
331 matches!(self, Self::Success(_))
332 }
333
334 pub fn is_failure(&self) -> bool {
336 matches!(self, Self::Failure(_))
337 }
338
339 pub fn request_id(&self) -> Option<Uuid> {
341 match self {
342 WebSocketResponse::Success(x) => x.request_id,
343 WebSocketResponse::Failure(x) => x.request_id,
344 }
345 }
346}
347
348#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, PartialEq)]
352#[cfg_attr(
353 feature = "python",
354 pyo3::pyclass(from_py_object),
355 pyo3_stub_gen::derive::gen_stub_pyclass
356)]
357pub struct RawFile {
358 pub name: String,
360 #[serde(
362 serialize_with = "serde_bytes::serialize",
363 deserialize_with = "serde_bytes::deserialize"
364 )]
365 pub contents: Vec<u8>,
366}
367
368#[cfg(feature = "python")]
369#[pyo3_stub_gen::derive::gen_stub_pymethods]
370#[pyo3::pymethods]
371impl RawFile {
372 #[getter]
373 fn contents(&self) -> Vec<u8> {
374 self.contents.clone()
375 }
376
377 #[getter]
378 fn name(&self) -> String {
379 self.name.clone()
380 }
381}
382
383impl From<ExportFile> for RawFile {
384 fn from(f: ExportFile) -> Self {
385 Self {
386 name: f.name,
387 contents: f.contents.0,
388 }
389 }
390}
391
392#[derive(Debug, Serialize, Deserialize, JsonSchema)]
394pub struct LoggableApiError {
395 pub error: ApiError,
397 pub msg_internal: Option<Cow<'static, str>>,
399}
400
401#[cfg(feature = "slog")]
402impl KV for LoggableApiError {
403 fn serialize(&self, _rec: &Record, serializer: &mut dyn Serializer) -> slog::Result {
404 use slog::Key;
405 if let Some(ref msg_internal) = self.msg_internal {
406 serializer.emit_str(Key::from("msg_internal"), msg_internal)?;
407 }
408 serializer.emit_str(Key::from("msg_external"), &self.error.message)?;
409 serializer.emit_str(Key::from("error_code"), &self.error.error_code.to_string())
410 }
411}
412
413#[derive(Debug, Serialize, Deserialize, JsonSchema, Eq, PartialEq, Clone)]
415pub struct ApiError {
416 pub error_code: ErrorCode,
418 pub message: String,
420}
421
422impl ApiError {
423 pub fn no_internal_message(self) -> LoggableApiError {
425 LoggableApiError {
426 error: self,
427 msg_internal: None,
428 }
429 }
430 pub fn with_message(self, msg_internal: Cow<'static, str>) -> LoggableApiError {
432 LoggableApiError {
433 error: self,
434 msg_internal: Some(msg_internal),
435 }
436 }
437
438 pub fn should_log_internal_message(&self) -> bool {
440 use ErrorCode as Code;
441 match self.error_code {
442 Code::InternalEngine | Code::InternalApi => true,
444 Code::MessageTypeNotAcceptedForWebRTC
446 | Code::MessageTypeNotAccepted
447 | Code::BadRequest
448 | Code::WrongProtocol
449 | Code::AuthTokenMissing
450 | Code::AuthTokenInvalid
451 | Code::InvalidBson
452 | Code::InvalidJson => false,
453 Code::ConnectionProblem => cfg!(debug_assertions),
455 }
456 }
457}
458
459#[derive(Debug, Serialize, Deserialize, JsonSchema)]
462#[serde(rename_all = "snake_case", rename = "SnakeCaseResult")]
463pub enum SnakeCaseResult<T, E> {
464 Ok(T),
466 Err(E),
468}
469
470impl<T, E> From<SnakeCaseResult<T, E>> for Result<T, E> {
471 fn from(value: SnakeCaseResult<T, E>) -> Self {
472 match value {
473 SnakeCaseResult::Ok(x) => Self::Ok(x),
474 SnakeCaseResult::Err(x) => Self::Err(x),
475 }
476 }
477}
478
479impl<T, E> From<Result<T, E>> for SnakeCaseResult<T, E> {
480 fn from(value: Result<T, E>) -> Self {
481 match value {
482 Ok(x) => Self::Ok(x),
483 Err(x) => Self::Err(x),
484 }
485 }
486}
487
488#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
490#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
491pub struct ClientMetrics {
492 pub rtc_frames_dropped: Option<u32>,
497
498 pub rtc_frames_decoded: Option<u64>,
503
504 pub rtc_frames_received: Option<u64>,
509
510 pub rtc_frames_per_second: Option<u8>, pub rtc_freeze_count: Option<u32>,
522
523 pub rtc_jitter_sec: Option<f64>,
533
534 pub rtc_keyframes_decoded: Option<u32>,
544
545 pub rtc_total_freezes_duration_sec: Option<f32>,
549
550 pub rtc_frame_height: Option<u32>,
554
555 pub rtc_frame_width: Option<u32>,
559
560 pub rtc_packets_lost: Option<u32>,
564
565 pub rtc_pli_count: Option<u32>,
569
570 pub rtc_pause_count: Option<u32>,
574
575 pub rtc_total_pauses_duration_sec: Option<f32>,
579
580 pub rtc_stun_rtt_sec: Option<f32>,
588}
589
590#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
592pub struct RtcIceCandidate {
593 pub stats_id: String,
595 pub foundation: String,
597 pub priority: u32,
599 pub address: String,
601 pub protocol: RtcIceProtocol,
603 pub port: u16,
605 pub typ: RtcIceCandidateType,
607 pub component: u16,
609 pub related_address: String,
611 pub related_port: u16,
613 pub tcp_type: String,
615}
616
617#[cfg(feature = "webrtc")]
618impl From<webrtc::ice_transport::ice_candidate::RTCIceCandidate> for RtcIceCandidate {
619 fn from(candidate: webrtc::ice_transport::ice_candidate::RTCIceCandidate) -> Self {
620 Self {
621 stats_id: candidate.stats_id,
622 foundation: candidate.foundation,
623 priority: candidate.priority,
624 address: candidate.address,
625 protocol: candidate.protocol.into(),
626 port: candidate.port,
627 typ: candidate.typ.into(),
628 component: candidate.component,
629 related_address: candidate.related_address,
630 related_port: candidate.related_port,
631 tcp_type: candidate.tcp_type,
632 }
633 }
634}
635
636#[cfg(feature = "webrtc")]
637impl From<RtcIceCandidate> for webrtc::ice_transport::ice_candidate::RTCIceCandidate {
638 fn from(candidate: RtcIceCandidate) -> Self {
639 Self {
640 stats_id: candidate.stats_id,
641 foundation: candidate.foundation,
642 priority: candidate.priority,
643 address: candidate.address,
644 protocol: candidate.protocol.into(),
645 port: candidate.port,
646 typ: candidate.typ.into(),
647 component: candidate.component,
648 related_address: candidate.related_address,
649 related_port: candidate.related_port,
650 tcp_type: candidate.tcp_type,
651 }
652 }
653}
654
655#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
657#[serde(rename_all = "snake_case")]
658pub enum RtcIceCandidateType {
659 #[default]
661 Unspecified,
662
663 Host,
669
670 Srflx,
677
678 Prflx,
683
684 Relay,
688}
689
690#[cfg(feature = "webrtc")]
691impl From<webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType> for RtcIceCandidateType {
692 fn from(candidate_type: webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType) -> Self {
693 match candidate_type {
694 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Host => RtcIceCandidateType::Host,
695 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Srflx => RtcIceCandidateType::Srflx,
696 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Prflx => RtcIceCandidateType::Prflx,
697 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Relay => RtcIceCandidateType::Relay,
698 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Unspecified => {
699 RtcIceCandidateType::Unspecified
700 }
701 }
702 }
703}
704
705#[cfg(feature = "webrtc")]
706impl From<RtcIceCandidateType> for webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType {
707 fn from(candidate_type: RtcIceCandidateType) -> Self {
708 match candidate_type {
709 RtcIceCandidateType::Host => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Host,
710 RtcIceCandidateType::Srflx => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Srflx,
711 RtcIceCandidateType::Prflx => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Prflx,
712 RtcIceCandidateType::Relay => webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Relay,
713 RtcIceCandidateType::Unspecified => {
714 webrtc::ice_transport::ice_candidate_type::RTCIceCandidateType::Unspecified
715 }
716 }
717 }
718}
719
720#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
723#[serde(rename_all = "snake_case")]
724pub enum RtcIceProtocol {
725 #[default]
727 Unspecified,
728
729 Udp,
731
732 Tcp,
734}
735
736#[cfg(feature = "webrtc")]
737impl From<webrtc::ice_transport::ice_protocol::RTCIceProtocol> for RtcIceProtocol {
738 fn from(protocol: webrtc::ice_transport::ice_protocol::RTCIceProtocol) -> Self {
739 match protocol {
740 webrtc::ice_transport::ice_protocol::RTCIceProtocol::Udp => RtcIceProtocol::Udp,
741 webrtc::ice_transport::ice_protocol::RTCIceProtocol::Tcp => RtcIceProtocol::Tcp,
742 webrtc::ice_transport::ice_protocol::RTCIceProtocol::Unspecified => RtcIceProtocol::Unspecified,
743 }
744 }
745}
746
747#[cfg(feature = "webrtc")]
748impl From<RtcIceProtocol> for webrtc::ice_transport::ice_protocol::RTCIceProtocol {
749 fn from(protocol: RtcIceProtocol) -> Self {
750 match protocol {
751 RtcIceProtocol::Udp => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Udp,
752 RtcIceProtocol::Tcp => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Tcp,
753 RtcIceProtocol::Unspecified => webrtc::ice_transport::ice_protocol::RTCIceProtocol::Unspecified,
754 }
755 }
756}
757
758#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
760#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
761#[serde(rename_all = "camelCase")]
762pub struct RtcIceCandidateInit {
764 pub candidate: String,
766 pub sdp_mid: Option<String>,
769 #[serde(rename = "sdpMLineIndex")]
772 pub sdp_mline_index: Option<u16>,
773 pub username_fragment: Option<String>,
776}
777
778#[cfg(feature = "webrtc")]
779impl From<webrtc::ice_transport::ice_candidate::RTCIceCandidateInit> for RtcIceCandidateInit {
780 fn from(candidate: webrtc::ice_transport::ice_candidate::RTCIceCandidateInit) -> Self {
781 Self {
782 candidate: candidate.candidate,
783 sdp_mid: candidate.sdp_mid,
784 sdp_mline_index: candidate.sdp_mline_index,
785 username_fragment: candidate.username_fragment,
786 }
787 }
788}
789
790#[cfg(feature = "webrtc")]
791impl From<RtcIceCandidateInit> for webrtc::ice_transport::ice_candidate::RTCIceCandidateInit {
792 fn from(candidate: RtcIceCandidateInit) -> Self {
793 Self {
794 candidate: candidate.candidate,
795 sdp_mid: candidate.sdp_mid,
796 sdp_mline_index: candidate.sdp_mline_index,
797 username_fragment: candidate.username_fragment,
798 }
799 }
800}
801
802#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
804#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
805pub struct RtcSessionDescription {
806 #[serde(rename = "type")]
808 pub sdp_type: RtcSdpType,
809
810 pub sdp: String,
812}
813
814#[cfg(feature = "webrtc")]
815impl From<webrtc::peer_connection::sdp::session_description::RTCSessionDescription> for RtcSessionDescription {
816 fn from(desc: webrtc::peer_connection::sdp::session_description::RTCSessionDescription) -> Self {
817 Self {
818 sdp_type: desc.sdp_type.into(),
819 sdp: desc.sdp,
820 }
821 }
822}
823
824#[cfg(feature = "webrtc")]
825impl TryFrom<RtcSessionDescription> for webrtc::peer_connection::sdp::session_description::RTCSessionDescription {
826 type Error = anyhow::Error;
827
828 fn try_from(desc: RtcSessionDescription) -> Result<Self, Self::Error> {
829 let result = match desc.sdp_type {
830 RtcSdpType::Offer => {
831 webrtc::peer_connection::sdp::session_description::RTCSessionDescription::offer(desc.sdp)?
832 }
833 RtcSdpType::Pranswer => {
834 webrtc::peer_connection::sdp::session_description::RTCSessionDescription::pranswer(desc.sdp)?
835 }
836 RtcSdpType::Answer => {
837 webrtc::peer_connection::sdp::session_description::RTCSessionDescription::answer(desc.sdp)?
838 }
839 RtcSdpType::Rollback => anyhow::bail!("Rollback is not supported"),
840 RtcSdpType::Unspecified => anyhow::bail!("Unspecified is not supported"),
841 };
842
843 Ok(result)
844 }
845}
846
847#[derive(Default, Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, JsonSchema)]
849#[serde(rename_all = "snake_case")]
850#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
851pub enum RtcSdpType {
852 #[default]
854 Unspecified = 0,
855
856 Offer,
858
859 Pranswer,
864
865 Answer,
870
871 Rollback,
877}
878
879#[cfg(feature = "webrtc")]
880impl From<webrtc::peer_connection::sdp::sdp_type::RTCSdpType> for RtcSdpType {
881 fn from(sdp_type: webrtc::peer_connection::sdp::sdp_type::RTCSdpType) -> Self {
882 match sdp_type {
883 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Offer => Self::Offer,
884 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Pranswer => Self::Pranswer,
885 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Answer => Self::Answer,
886 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Rollback => Self::Rollback,
887 webrtc::peer_connection::sdp::sdp_type::RTCSdpType::Unspecified => Self::Unspecified,
888 }
889 }
890}
891
892#[cfg(feature = "webrtc")]
893impl From<RtcSdpType> for webrtc::peer_connection::sdp::sdp_type::RTCSdpType {
894 fn from(sdp_type: RtcSdpType) -> Self {
895 match sdp_type {
896 RtcSdpType::Offer => Self::Offer,
897 RtcSdpType::Pranswer => Self::Pranswer,
898 RtcSdpType::Answer => Self::Answer,
899 RtcSdpType::Rollback => Self::Rollback,
900 RtcSdpType::Unspecified => Self::Unspecified,
901 }
902 }
903}
904#[derive(JsonSchema, Debug, Serialize, Deserialize, Clone, PartialEq)]
906#[serde(rename_all = "snake_case")]
907pub struct ModelingSessionData {
908 pub api_call_id: String,
911}
912
913#[cfg(test)]
914mod tests {
915 use super::*;
916 use crate::output;
917
918 const REQ_ID: Uuid = uuid::uuid!("cc30d5e2-482b-4498-b5d2-6131c30a50a4");
919
920 #[test]
921 fn reconnect_response_round_trip() {
922 let response = WebSocketResponse::success(None, OkWebSocketResponseData::Reconnect {});
923 let expected = serde_json::json!({
924 "success": true,
925 "request_id": null,
926 "resp": {
927 "type": "reconnect",
928 "data": {}
929 }
930 });
931 assert_eq!(serde_json::to_value(&response).unwrap(), expected);
932 let decoded: WebSocketResponse = serde_json::from_value(expected).unwrap();
933 assert_eq!(decoded, response);
934 }
935
936 #[test]
937 fn serialize_websocket_modeling_ok() {
938 let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
939 success: true,
940 request_id: Some(REQ_ID),
941 resp: OkWebSocketResponseData::Modeling {
942 modeling_response: OkModelingCmdResponse::CurveGetControlPoints(output::CurveGetControlPoints {
943 control_points: vec![],
944 }),
945 },
946 });
947 let expected = serde_json::json!({
948 "success": true,
949 "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
950 "resp": {
951 "type": "modeling",
952 "data": {
953 "modeling_response": {
954 "type": "curve_get_control_points",
955 "data": { "control_points": [] }
956 }
957 }
958 }
959 });
960 assert_json_eq(actual, expected);
961 }
962
963 #[test]
964 fn serialize_websocket_webrtc_ok() {
965 let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
966 success: true,
967 request_id: Some(REQ_ID),
968 resp: OkWebSocketResponseData::IceServerInfo { ice_servers: vec![] },
969 });
970 let expected = serde_json::json!({
971 "success": true,
972 "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
973 "resp": {
974 "type": "ice_server_info",
975 "data": {
976 "ice_servers": []
977 }
978 }
979 });
980 assert_json_eq(actual, expected);
981 }
982
983 #[test]
984 fn serialize_websocket_export_ok() {
985 let actual = WebSocketResponse::Success(SuccessWebSocketResponse {
986 success: true,
987 request_id: Some(REQ_ID),
988 resp: OkWebSocketResponseData::Export { files: vec![] },
989 });
990 let expected = serde_json::json!({
991 "success": true,
992 "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
993 "resp": {
994 "type": "export",
995 "data": {"files": [] }
996 }
997 });
998 assert_json_eq(actual, expected);
999 }
1000
1001 #[test]
1002 fn serialize_websocket_err() {
1003 let actual = WebSocketResponse::Failure(FailureWebSocketResponse {
1004 success: false,
1005 request_id: Some(REQ_ID),
1006 errors: vec![ApiError {
1007 error_code: ErrorCode::InternalApi,
1008 message: "you fucked up!".to_owned(),
1009 }],
1010 });
1011 let expected = serde_json::json!({
1012 "success": false,
1013 "request_id": "cc30d5e2-482b-4498-b5d2-6131c30a50a4",
1014 "errors": [
1015 {
1016 "error_code": "internal_api",
1017 "message": "you fucked up!"
1018 }
1019 ],
1020 });
1021 assert_json_eq(actual, expected);
1022 }
1023
1024 #[test]
1025 fn serialize_websocket_metrics() {
1026 let actual = WebSocketRequest::MetricsResponse {
1027 metrics: Box::new(ClientMetrics {
1028 rtc_frames_dropped: Some(1),
1029 rtc_frames_decoded: Some(2),
1030 rtc_frames_per_second: Some(3),
1031 rtc_frames_received: Some(4),
1032 rtc_freeze_count: Some(5),
1033 rtc_jitter_sec: Some(6.7),
1034 rtc_keyframes_decoded: Some(8),
1035 rtc_total_freezes_duration_sec: Some(9.1),
1036 rtc_frame_height: Some(100),
1037 rtc_frame_width: Some(100),
1038 rtc_packets_lost: Some(0),
1039 rtc_pli_count: Some(0),
1040 rtc_pause_count: Some(0),
1041 rtc_total_pauses_duration_sec: Some(0.0),
1042 rtc_stun_rtt_sec: Some(0.005),
1043 }),
1044 };
1045 let expected = serde_json::json!({
1046 "type": "metrics_response",
1047 "metrics": {
1048 "rtc_frames_dropped": 1,
1049 "rtc_frames_decoded": 2,
1050 "rtc_frames_per_second": 3,
1051 "rtc_frames_received": 4,
1052 "rtc_freeze_count": 5,
1053 "rtc_jitter_sec": 6.7,
1054 "rtc_keyframes_decoded": 8,
1055 "rtc_total_freezes_duration_sec": 9.1,
1056 "rtc_frame_height": 100,
1057 "rtc_frame_width": 100,
1058 "rtc_packets_lost": 0,
1059 "rtc_pli_count": 0,
1060 "rtc_pause_count": 0,
1061 "rtc_total_pauses_duration_sec": 0.0,
1062 "rtc_stun_rtt_sec": 0.005,
1063 },
1064 });
1065 assert_json_eq(actual, expected);
1066 }
1067
1068 fn assert_json_eq<T: Serialize>(actual: T, expected: serde_json::Value) {
1069 let json_str = serde_json::to_string(&actual).unwrap();
1070 let actual: serde_json::Value = serde_json::from_str(&json_str).unwrap();
1071 assert_eq!(actual, expected, "got\n{actual:#}\n, expected\n{expected:#}\n");
1072 }
1073}