1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2
3use crate::{
4 ActionDefinition, ActionResult, DeviceId, DeviceInfo, EventSequence, HelloResult, MediaFrame,
5 MediaStreamId, MediaStreamInfo, MediaStreamKind, Observation, PeerInfo, SessionExport,
6 SessionId, SessionInfo, SessionOutcome, TestEvent, UiSnapshot, Verdict,
7};
8
9pub const MAX_EVENTS_LIST_PAGE_SIZE: u32 = 1_000;
16
17#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
19#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
20#[serde(rename_all = "camelCase", deny_unknown_fields)]
21pub struct SystemDescribeResult {
22 pub connection: HelloResult,
23 pub client: PeerInfo,
24 pub device_id: Option<DeviceId>,
25 pub active_session_id: Option<SessionId>,
26}
27
28pub type DeviceConnectResult = DeviceInfo;
30
31#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
33#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
34#[serde(rename_all = "camelCase", deny_unknown_fields)]
35pub struct DeviceDisconnectResult {
36 pub disconnected: bool,
37}
38
39pub type DeviceCapabilitiesResult = Vec<ActionDefinition>;
41
42pub type DeviceObserveResult = Observation;
44
45pub type DeviceExecuteResult = ActionResult;
47
48#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
53#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
54#[serde(rename_all = "camelCase", deny_unknown_fields)]
55pub struct UiSnapshotGetParams {
56 pub observation_id: uuid::Uuid,
57}
58
59pub type UiSnapshotGetResult = UiSnapshot;
61
62#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
67#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
68#[serde(rename_all = "camelCase", deny_unknown_fields)]
69pub struct VerdictRecordParams {
70 pub verdict: Verdict,
71}
72
73#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
75#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
76#[serde(rename_all = "camelCase", deny_unknown_fields)]
77pub struct VerdictRecordResult {
78 pub event: TestEvent,
79}
80
81#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
88#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
89#[serde(rename_all = "camelCase", deny_unknown_fields)]
90pub struct MediaStreamStartParams {
91 pub stream_id: MediaStreamId,
92 pub kind: MediaStreamKind,
93}
94
95#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
97#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
98#[serde(rename_all = "camelCase", deny_unknown_fields)]
99pub struct MediaStreamStartResult {
100 pub stream: MediaStreamInfo,
101}
102
103#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
105#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
106#[serde(rename_all = "camelCase", deny_unknown_fields)]
107pub struct MediaStreamCaptureParams {
108 pub stream_id: MediaStreamId,
109 pub frame_index: EventSequence,
112 #[serde(
113 default,
114 serialize_with = "crate::wire_integer::serialize_optional_js_safe_u64",
115 deserialize_with = "crate::wire_integer::deserialize_optional_js_safe_u64",
116 skip_serializing_if = "Option::is_none"
117 )]
118 #[cfg_attr(
119 feature = "schema",
120 schemars(range(min = 0_u64, max = 9_007_199_254_740_991_u64))
121 )]
122 pub duration_ms: Option<u64>,
123}
124
125#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
127#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
128#[serde(rename_all = "camelCase", deny_unknown_fields)]
129pub struct MediaStreamCaptureResult {
130 pub frame: MediaFrame,
131}
132
133#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
135#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
136#[serde(rename_all = "camelCase", deny_unknown_fields)]
137pub struct MediaStreamEndParams {
138 pub stream_id: MediaStreamId,
139}
140
141#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
143#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
144#[serde(rename_all = "camelCase", deny_unknown_fields)]
145pub struct MediaStreamEndResult {
146 pub stream_id: MediaStreamId,
147 #[serde(
148 serialize_with = "crate::wire_integer::serialize_js_safe_u64",
149 deserialize_with = "crate::wire_integer::deserialize_js_safe_u64"
150 )]
151 #[cfg_attr(
152 feature = "schema",
153 schemars(range(min = 0_u64, max = 9_007_199_254_740_991_u64))
154 )]
155 pub frame_count: u64,
156}
157
158pub type SessionStartResult = SessionInfo;
160
161#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
163#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
164#[serde(rename_all = "camelCase", deny_unknown_fields)]
165pub struct SessionCurrentResult {
166 pub session_id: SessionId,
167}
168
169#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
171#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
172#[serde(rename_all = "camelCase", deny_unknown_fields)]
173pub struct SessionEndParams {
174 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub outcome: Option<SessionOutcome>,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
177 pub reason: Option<String>,
178}
179
180pub type SessionEndResult = SessionInfo;
182
183pub type SessionsListResult = Vec<SessionInfo>;
185
186#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
188#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
189#[serde(rename_all = "camelCase", deny_unknown_fields)]
190pub struct SessionTargetParams {
191 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub session_id: Option<SessionId>,
193}
194
195#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
201#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
202#[serde(rename_all = "camelCase", deny_unknown_fields)]
203pub struct SessionExportParams {
204 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub session_id: Option<SessionId>,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub after_sequence: Option<EventSequence>,
208 #[serde(
209 default,
210 skip_serializing_if = "Option::is_none",
211 serialize_with = "serialize_optional_event_page_limit",
212 deserialize_with = "deserialize_optional_event_page_limit"
213 )]
214 #[cfg_attr(feature = "schema", schemars(range(min = 1_u32, max = 1_000_u32)))]
215 pub limit: Option<u32>,
216}
217
218#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
223#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
224#[serde(rename_all = "camelCase", deny_unknown_fields)]
225pub struct SessionExportResult {
226 pub session: SessionInfo,
227 pub events: Vec<TestEvent>,
228 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub next_after_sequence: Option<EventSequence>,
230}
231
232impl From<SessionExport> for SessionExportResult {
233 fn from(export: SessionExport) -> Self {
234 Self {
235 session: export.session,
236 events: export.events,
237 next_after_sequence: None,
238 }
239 }
240}
241
242#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
244#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
245#[serde(rename_all = "camelCase", deny_unknown_fields)]
246pub struct EventsListParams {
247 #[serde(default, skip_serializing_if = "Option::is_none")]
248 pub session_id: Option<SessionId>,
249 #[serde(default, skip_serializing_if = "Option::is_none")]
250 pub after_sequence: Option<EventSequence>,
251 #[serde(
252 default,
253 skip_serializing_if = "Option::is_none",
254 serialize_with = "serialize_optional_event_page_limit",
255 deserialize_with = "deserialize_optional_event_page_limit"
256 )]
257 #[cfg_attr(feature = "schema", schemars(range(min = 1_u32, max = 1_000_u32)))]
258 pub limit: Option<u32>,
259}
260
261pub type EventsListResult = Vec<TestEvent>;
263
264fn serialize_optional_event_page_limit<S>(
265 value: &Option<u32>,
266 serializer: S,
267) -> Result<S::Ok, S::Error>
268where
269 S: Serializer,
270{
271 match value {
272 Some(value) if (1..=MAX_EVENTS_LIST_PAGE_SIZE).contains(value) => {
273 serializer.serialize_some(value)
274 }
275 Some(_) => Err(serde::ser::Error::custom(format!(
276 "event page limit must be between 1 and {MAX_EVENTS_LIST_PAGE_SIZE}"
277 ))),
278 None => serializer.serialize_none(),
279 }
280}
281
282fn deserialize_optional_event_page_limit<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
283where
284 D: Deserializer<'de>,
285{
286 let value = Option::<u32>::deserialize(deserializer)?;
287 match value {
288 Some(value) if !(1..=MAX_EVENTS_LIST_PAGE_SIZE).contains(&value) => {
289 Err(serde::de::Error::custom(format!(
290 "event page limit must be between 1 and {MAX_EVENTS_LIST_PAGE_SIZE}"
291 )))
292 }
293 value => Ok(value),
294 }
295}
296
297#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
299#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
300#[serde(rename_all = "camelCase", deny_unknown_fields)]
301pub struct EventsClearResult {
302 pub deleted: bool,
303 pub session_id: SessionId,
304}
305
306#[cfg(test)]
307mod tests {
308 use serde_json::json;
309 use uuid::Uuid;
310
311 use super::{
312 EventsClearResult, EventsListParams, MAX_EVENTS_LIST_PAGE_SIZE, MediaStreamCaptureParams,
313 MediaStreamEndParams, MediaStreamEndResult, MediaStreamStartParams, SessionEndParams,
314 SessionExportParams, SessionExportResult, SessionTargetParams,
315 };
316 use crate::{EventSequence, MediaStreamId, MediaStreamKind, SessionId, SessionOutcome};
317
318 #[test]
319 fn optional_method_params_are_strict_and_use_camel_case() {
320 let session_id = SessionId::from(Uuid::nil());
321 let end: SessionEndParams = serde_json::from_value(json!({
322 "outcome": "cancelled",
323 "reason": "stopped by client"
324 }))
325 .expect("session.end params");
326 assert_eq!(end.outcome, Some(SessionOutcome::Cancelled));
327 assert_eq!(
328 serde_json::to_value(SessionTargetParams {
329 session_id: Some(session_id.clone()),
330 })
331 .expect("session target params"),
332 json!({ "sessionId": Uuid::nil() })
333 );
334 assert_eq!(
335 serde_json::to_value(SessionExportParams {
336 session_id: Some(session_id.clone()),
337 after_sequence: EventSequence::new(4),
338 limit: Some(25),
339 })
340 .expect("session.export params"),
341 json!({ "sessionId": Uuid::nil(), "afterSequence": 4, "limit": 25 })
342 );
343 assert_eq!(
344 serde_json::to_value(SessionExportParams {
345 session_id: Some(session_id.clone()),
346 after_sequence: None,
347 limit: None,
348 })
349 .expect("legacy session.export params"),
350 json!({ "sessionId": Uuid::nil() })
351 );
352 assert_eq!(
353 serde_json::to_value(EventsListParams {
354 session_id: Some(session_id.clone()),
355 after_sequence: EventSequence::new(4),
356 limit: Some(25),
357 })
358 .expect("events.list params"),
359 json!({ "sessionId": Uuid::nil(), "afterSequence": 4, "limit": 25 })
360 );
361
362 assert!(serde_json::from_value::<SessionEndParams>(json!({ "unexpected": true })).is_err());
363 assert!(
364 serde_json::from_value::<SessionTargetParams>(json!({ "session_id": Uuid::nil() }))
365 .is_err()
366 );
367 assert!(serde_json::from_value::<EventsListParams>(json!({ "afterSequence": 0 })).is_err());
368 assert!(serde_json::from_value::<EventsListParams>(json!({ "limit": 0 })).is_err());
369 assert!(serde_json::from_value::<SessionExportParams>(json!({ "limit": 0 })).is_err());
370 assert!(
371 serde_json::from_value::<EventsListParams>(
372 json!({ "limit": MAX_EVENTS_LIST_PAGE_SIZE + 1 })
373 )
374 .is_err()
375 );
376 assert!(
377 serde_json::from_value::<SessionExportParams>(
378 json!({ "limit": MAX_EVENTS_LIST_PAGE_SIZE + 1 })
379 )
380 .is_err()
381 );
382
383 let stream_id = MediaStreamId::from(Uuid::nil());
384 assert_eq!(
385 serde_json::to_value(MediaStreamStartParams {
386 stream_id: stream_id.clone(),
387 kind: MediaStreamKind::Screenshot,
388 })
389 .expect("media.stream.start params"),
390 json!({ "streamId": Uuid::nil(), "kind": "screenshot" })
391 );
392 assert_eq!(
393 serde_json::to_value(MediaStreamCaptureParams {
394 stream_id: stream_id.clone(),
395 frame_index: EventSequence::FIRST,
396 duration_ms: Some(crate::MAX_SAFE_INTEGER),
397 })
398 .expect("media.stream.capture params"),
399 json!({
400 "streamId": Uuid::nil(),
401 "frameIndex": 1,
402 "durationMs": crate::MAX_SAFE_INTEGER
403 })
404 );
405 assert_eq!(
406 serde_json::to_value(MediaStreamEndParams {
407 stream_id: stream_id.clone(),
408 })
409 .expect("media.stream.end params"),
410 json!({ "streamId": Uuid::nil() })
411 );
412 assert!(
413 serde_json::from_value::<MediaStreamCaptureParams>(json!({
414 "streamId": Uuid::nil(),
415 "frameIndex": 1,
416 "durationMs": crate::MAX_SAFE_INTEGER + 1
417 }))
418 .is_err()
419 );
420 assert!(
421 serde_json::from_value::<MediaStreamStartParams>(json!({
422 "streamId": Uuid::nil(),
423 "kind": "screenshot",
424 "mediaType": "image/png"
425 }))
426 .is_err()
427 );
428 }
429
430 #[test]
431 fn method_results_are_strict_and_use_camel_case() {
432 let result = EventsClearResult {
433 deleted: true,
434 session_id: SessionId::from(Uuid::nil()),
435 };
436 assert_eq!(
437 serde_json::to_value(result).expect("events.clear result"),
438 json!({ "deleted": true, "sessionId": Uuid::nil() })
439 );
440 assert!(
441 serde_json::from_value::<EventsClearResult>(json!({
442 "deleted": true,
443 "sessionId": Uuid::nil(),
444 "unexpected": true
445 }))
446 .is_err()
447 );
448
449 let export = SessionExportResult {
450 session: crate::SessionInfo {
451 id: SessionId::from(Uuid::nil()),
452 state: crate::SessionState::Ended,
453 started_at_ms: 1,
454 ended_at_ms: Some(2),
455 event_count: EventSequence::new(2).expect("event count"),
456 last_sequence: EventSequence::new(2).expect("last sequence"),
457 },
458 events: Vec::new(),
459 next_after_sequence: None,
460 };
461 let value = serde_json::to_value(export).expect("legacy session.export result");
462 let object = value.as_object().expect("export object");
463 assert_eq!(object.len(), 2);
464 assert!(object.contains_key("session"));
465 assert!(object.contains_key("events"));
466 assert!(!object.contains_key("nextAfterSequence"));
467
468 let stream_id = MediaStreamId::from(Uuid::nil());
469 assert_eq!(
470 serde_json::to_value(MediaStreamEndResult {
471 stream_id: stream_id.clone(),
472 frame_count: 0,
473 })
474 .expect("zero-frame media.stream.end result"),
475 json!({ "streamId": Uuid::nil(), "frameCount": 0 })
476 );
477 assert!(
478 serde_json::to_value(MediaStreamEndResult {
479 stream_id,
480 frame_count: crate::MAX_SAFE_INTEGER + 1,
481 })
482 .is_err()
483 );
484 }
485}