Skip to main content

devicerail_protocol/
manual.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use uuid::Uuid;
4
5use crate::{DeviceId, EventSequence};
6
7/// Version of the portable manual Action recording document.
8pub const MANUAL_RECORDING_VERSION: u16 = 1;
9
10/// A bounded, Driver-neutral record of human-selected Actions.
11///
12/// Protected Action arguments are represented only by an opaque host-owned
13/// secret reference. The secret value is never part of this durable DTO.
14#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
15#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
16#[serde(rename_all = "camelCase", deny_unknown_fields)]
17pub struct ManualRecording {
18    pub format_version: u16,
19    pub recording_id: Uuid,
20    pub source_device_id: DeviceId,
21    #[cfg_attr(feature = "schema", schemars(length(min = 64, max = 64)))]
22    pub action_space_sha256: String,
23    #[serde(
24        serialize_with = "crate::wire_integer::serialize_js_safe_u64",
25        deserialize_with = "crate::wire_integer::deserialize_js_safe_u64"
26    )]
27    #[cfg_attr(
28        feature = "schema",
29        schemars(range(min = 0_u64, max = 9_007_199_254_740_991_u64))
30    )]
31    pub started_at_ms: u64,
32    #[serde(
33        serialize_with = "crate::wire_integer::serialize_js_safe_u64",
34        deserialize_with = "crate::wire_integer::deserialize_js_safe_u64"
35    )]
36    #[cfg_attr(
37        feature = "schema",
38        schemars(range(min = 0_u64, max = 9_007_199_254_740_991_u64))
39    )]
40    pub ended_at_ms: u64,
41    #[cfg_attr(feature = "schema", schemars(length(max = 10_000)))]
42    pub steps: Vec<ManualActionStep>,
43}
44
45#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
46#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
47#[serde(rename_all = "camelCase", deny_unknown_fields)]
48pub struct ManualActionStep {
49    pub sequence: EventSequence,
50    #[serde(
51        serialize_with = "crate::wire_integer::serialize_js_safe_u64",
52        deserialize_with = "crate::wire_integer::deserialize_js_safe_u64"
53    )]
54    #[cfg_attr(
55        feature = "schema",
56        schemars(range(min = 0_u64, max = 9_007_199_254_740_991_u64))
57    )]
58    pub captured_at_ms: u64,
59    pub call_id: Uuid,
60    #[cfg_attr(feature = "schema", schemars(length(min = 1, max = 128)))]
61    pub name: String,
62    pub arguments: ManualActionArguments,
63}
64
65#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
66#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
67#[serde(tag = "kind", rename_all = "camelCase", deny_unknown_fields)]
68pub enum ManualActionArguments {
69    /// Arguments safe to persist in the recording document.
70    Captured { value: Value },
71    /// Complete arguments supplied by the replay host at execution time.
72    Protected {
73        #[serde(rename = "secretRef")]
74        #[cfg_attr(feature = "schema", schemars(length(min = 1, max = 128)))]
75        secret_ref: String,
76    },
77}
78
79#[cfg(test)]
80mod tests {
81    use serde_json::json;
82    use uuid::Uuid;
83
84    use super::{
85        MANUAL_RECORDING_VERSION, ManualActionArguments, ManualActionStep, ManualRecording,
86    };
87    use crate::{DeviceId, EventSequence};
88
89    #[test]
90    fn protected_recording_contains_only_an_opaque_reference() {
91        let recording = ManualRecording {
92            format_version: MANUAL_RECORDING_VERSION,
93            recording_id: Uuid::nil(),
94            source_device_id: DeviceId::new("web-1"),
95            action_space_sha256: "a".repeat(64),
96            started_at_ms: 100,
97            ended_at_ms: 101,
98            steps: vec![ManualActionStep {
99                sequence: EventSequence::FIRST,
100                captured_at_ms: 101,
101                call_id: Uuid::nil(),
102                name: "fillSecret".to_owned(),
103                arguments: ManualActionArguments::Protected {
104                    secret_ref: "login-password".to_owned(),
105                },
106            }],
107        };
108        let value = serde_json::to_value(recording).expect("serialize recording");
109        assert_eq!(value["formatVersion"], MANUAL_RECORDING_VERSION);
110        assert_eq!(
111            value["steps"][0]["arguments"],
112            json!({ "kind": "protected", "secretRef": "login-password" })
113        );
114        assert!(!value.to_string().contains("password-value"));
115    }
116}