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}