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