Skip to main content

core_api/node/contracts/
camera.rs

1use serde::{Deserialize, Serialize};
2
3pub const VERSION: &str = crate::node::settings::VERSION;
4pub const NODE_TYPE: &str = "camera";
5pub const APP_TARGET_PREFIX: &str = "/app/plugin/camera/";
6pub const RUNTIME_TARGET_PREFIX: &str = "/camera/app/";
7pub const DEVICE_LIST: &str = "device/list";
8pub const MANAGEMENT_SNAPSHOT: &str = "management/snapshot";
9pub const DEVICE_REMOVE: &str = "device/remove";
10pub const WATCH_STATUS: &str = "watch/status";
11pub const WATCH_SET: &str = "watch/set";
12pub const APP_DEVICE_LIST_TARGET: &str = "/app/plugin/camera/device/list";
13pub const APP_MANAGEMENT_SNAPSHOT_TARGET: &str = "/app/plugin/camera/management/snapshot";
14pub const APP_DEVICE_REMOVE_TARGET: &str = "/app/plugin/camera/device/remove";
15pub const APP_WATCH_STATUS_TARGET: &str = "/app/plugin/camera/watch/status";
16pub const APP_WATCH_SET_TARGET: &str = "/app/plugin/camera/watch/set";
17pub const RUNTIME_DEVICE_LIST_TARGET: &str = "/camera/app/device/list";
18pub const RUNTIME_MANAGEMENT_SNAPSHOT_TARGET: &str = "/camera/app/management/snapshot";
19pub const RUNTIME_DEVICE_REMOVE_TARGET: &str = "/camera/app/device/remove";
20pub const RUNTIME_WATCH_STATUS_TARGET: &str = "/camera/app/watch/status";
21pub const RUNTIME_WATCH_SET_TARGET: &str = "/camera/app/watch/set";
22pub const DEVICE_ADD_FLOW: &str = "device.add";
23pub const SETTINGS_STATUS: &str = crate::node::settings::STATUS;
24pub const SETTINGS_UPDATE: &str = crate::node::settings::UPDATE;
25pub const SETTINGS_REVERT: &str = crate::node::settings::REVERT;
26pub const SETTINGS_RETRY: &str = crate::node::settings::RETRY;
27pub const RECOGNITION_SECTION: &str = "recognition";
28
29pub type SettingsStatusRequest = crate::node::settings::StatusRequest;
30pub type RecognitionUpdateRequest = crate::node::settings::UpdateRequest<RecognitionSettings>;
31
32#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
33#[serde(rename_all = "camelCase", deny_unknown_fields)]
34pub struct EmptyRequest {}
35
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
37#[serde(rename_all = "camelCase", deny_unknown_fields)]
38pub struct DeviceIdRequest {
39    pub device_id: String,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
43#[serde(rename_all = "camelCase", deny_unknown_fields)]
44pub struct WatchStatusRequest {
45    pub device_id: String,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
49#[serde(rename_all = "camelCase", deny_unknown_fields)]
50pub struct WatchSetRequest {
51    pub device_id: String,
52    pub enabled: bool,
53}
54
55#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
56#[serde(rename_all = "snake_case")]
57pub enum CameraDeviceHealth {
58    Applying,
59    Ready,
60    Degraded,
61    Error,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
65#[serde(rename_all = "camelCase", deny_unknown_fields)]
66pub struct ProviderCatalogItem {
67    pub id: String,
68    pub display_name: String,
69    pub description: String,
70    pub supports_multiple_cameras: bool,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
74#[serde(rename_all = "camelCase", deny_unknown_fields)]
75pub struct CameraDevice {
76    pub id: String,
77    pub name: String,
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub manufacturer_name: Option<String>,
80    pub capabilities: Vec<String>,
81    pub health: CameraDeviceHealth,
82    #[serde(default, skip_serializing_if = "Option::is_none")]
83    pub last_error: Option<String>,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
87#[serde(rename_all = "camelCase", deny_unknown_fields)]
88pub struct OnlineStateObservation {
89    pub id: String,
90    pub state: String,
91    #[serde(default, skip_serializing_if = "Option::is_none")]
92    pub observed_at: Option<i64>,
93    #[serde(default, skip_serializing_if = "Option::is_none")]
94    pub source: Option<String>,
95    #[serde(default, skip_serializing_if = "Option::is_none")]
96    pub reason: Option<String>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
100#[serde(rename_all = "camelCase", deny_unknown_fields)]
101pub struct WatchControlResponse {
102    pub ok: bool,
103    pub device_id: String,
104    pub enabled: bool,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
108#[serde(rename_all = "camelCase", deny_unknown_fields)]
109pub struct ManagementSnapshot {
110    pub providers: Vec<ProviderCatalogItem>,
111    pub devices: Vec<CameraDevice>,
112    pub online_states: Vec<OnlineStateObservation>,
113    pub watch_states: Vec<WatchControlResponse>,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
117#[serde(rename_all = "camelCase", deny_unknown_fields)]
118pub struct DeviceListResponse {
119    pub devices: Vec<CameraDevice>,
120    pub online_states: Vec<OnlineStateObservation>,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
124#[serde(rename_all = "camelCase", deny_unknown_fields)]
125pub struct DeviceMutationResponse {
126    pub device_ids: Vec<String>,
127    pub changed: bool,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase", deny_unknown_fields)]
132pub struct RecognitionSettings {
133    pub enabled: bool,
134}
135
136pub type RecognitionCommandRequest = crate::node::settings::SectionCommandRequest;
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn manifest_and_rust_camera_contract_agree() {
144        let manifest: serde_json::Value =
145            serde_json::from_str(include_str!("../../../manifest/core.v1.json")).unwrap();
146        let camera = &manifest["protocols"]["cameraPlugin"];
147        assert_eq!(camera["version"], VERSION);
148        assert_eq!(camera["nodeType"], NODE_TYPE);
149        assert_eq!(camera["appTargetPrefix"], APP_TARGET_PREFIX);
150        assert_eq!(camera["runtimeTargetPrefix"], RUNTIME_TARGET_PREFIX);
151        assert_eq!(camera["routes"]["deviceList"], DEVICE_LIST);
152        assert_eq!(camera["routes"]["managementSnapshot"], MANAGEMENT_SNAPSHOT);
153        assert_eq!(camera["routes"]["deviceRemove"], DEVICE_REMOVE);
154        assert_eq!(camera["routes"]["watchStatus"], WATCH_STATUS);
155        assert_eq!(camera["routes"]["watchSet"], WATCH_SET);
156        assert_eq!(camera["flows"]["deviceAdd"], DEVICE_ADD_FLOW);
157        assert_eq!(
158            APP_DEVICE_LIST_TARGET,
159            format!("{APP_TARGET_PREFIX}{DEVICE_LIST}")
160        );
161        assert_eq!(
162            APP_MANAGEMENT_SNAPSHOT_TARGET,
163            format!("{APP_TARGET_PREFIX}{MANAGEMENT_SNAPSHOT}")
164        );
165        assert_eq!(
166            APP_DEVICE_REMOVE_TARGET,
167            format!("{APP_TARGET_PREFIX}{DEVICE_REMOVE}")
168        );
169        assert_eq!(
170            APP_WATCH_STATUS_TARGET,
171            format!("{APP_TARGET_PREFIX}{WATCH_STATUS}")
172        );
173        assert_eq!(
174            APP_WATCH_SET_TARGET,
175            format!("{APP_TARGET_PREFIX}{WATCH_SET}")
176        );
177        assert_eq!(
178            RUNTIME_DEVICE_LIST_TARGET,
179            format!("{RUNTIME_TARGET_PREFIX}{DEVICE_LIST}")
180        );
181        assert_eq!(
182            RUNTIME_MANAGEMENT_SNAPSHOT_TARGET,
183            format!("{RUNTIME_TARGET_PREFIX}{MANAGEMENT_SNAPSHOT}")
184        );
185        assert_eq!(
186            RUNTIME_DEVICE_REMOVE_TARGET,
187            format!("{RUNTIME_TARGET_PREFIX}{DEVICE_REMOVE}")
188        );
189        assert_eq!(
190            RUNTIME_WATCH_STATUS_TARGET,
191            format!("{RUNTIME_TARGET_PREFIX}{WATCH_STATUS}")
192        );
193        assert_eq!(
194            RUNTIME_WATCH_SET_TARGET,
195            format!("{RUNTIME_TARGET_PREFIX}{WATCH_SET}")
196        );
197    }
198
199    #[test]
200    fn management_requests_are_strict() {
201        let invalid = serde_json::json!({"deviceId": "camera-1", "enabled": true});
202        assert!(serde_json::from_value::<WatchStatusRequest>(invalid).is_err());
203        let request: WatchSetRequest = serde_json::from_value(serde_json::json!({
204            "deviceId": "camera-1",
205            "enabled": true
206        }))
207        .unwrap();
208        assert_eq!(request.device_id, "camera-1");
209        assert!(request.enabled);
210    }
211
212    #[test]
213    fn management_conformance_corpus_matches_schema() {
214        let schema: serde_json::Value = serde_json::from_str(include_str!(
215            "../../../schema/camera-plugin-management.v1.schema.json"
216        ))
217        .unwrap();
218        let corpus: serde_json::Value = serde_json::from_str(include_str!(
219            "../../../fixtures/camera-plugin-management.conformance.json"
220        ))
221        .unwrap();
222        let validator = jsonschema::validator_for(&schema).unwrap();
223        for example in corpus["valid"].as_array().unwrap() {
224            assert!(
225                validator.is_valid(&example["value"]),
226                "valid example was rejected: {}",
227                example["name"]
228            );
229        }
230        for example in corpus["invalid"].as_array().unwrap() {
231            assert!(
232                !validator.is_valid(&example["value"]),
233                "invalid example was accepted: {}",
234                example["name"]
235            );
236        }
237    }
238}