1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4use crate::models::status::{SyncEventStatus, SyncEventType};
5
6#[derive(Debug, Serialize, Deserialize)]
7#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
8pub struct EnrollRequest {
9 pub client_id: String,
10 pub client_secret: String,
11 pub instance_id: String,
12}
13
14#[derive(Debug, Serialize, Deserialize)]
15#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
16pub struct EnrollResponse {
17 pub service_id: Uuid,
18 pub session_token: String,
19 #[serde(default)]
22 pub paused: bool,
23}
24
25#[derive(Debug, Serialize, Deserialize)]
26#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
27pub struct HeartbeatRequest {
28 pub service_id: Uuid,
29 pub status: String,
30 pub current_operation: Option<String>,
31}
32
33#[derive(Debug, Serialize, Deserialize)]
34#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
35pub struct HeartbeatResponse {
36 pub session_token: String,
37 pub pending_commands: Vec<PendingCommand>,
38 #[serde(default)]
40 pub paused: bool,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
45pub struct PendingCommand {
46 pub id: Uuid,
47 pub command: String,
48 #[cfg_attr(feature = "openapi", schema(value_type = Object))]
49 pub payload: Option<serde_json::Value>,
50}
51
52#[derive(Debug, Serialize, Deserialize)]
53#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
54pub struct CommandUpdateRequest {
55 pub status: String,
56 #[cfg_attr(feature = "openapi", schema(value_type = Object))]
57 pub result: Option<serde_json::Value>,
58}
59
60#[derive(Debug, Serialize)]
61pub struct SyncEventCreate {
62 pub service_id: Uuid,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub command_id: Option<Uuid>,
65 pub event_type: SyncEventType,
66 pub status: SyncEventStatus,
67}
68
69#[derive(Debug, Deserialize)]
70pub struct SyncEventRef {
71 pub id: Uuid,
72}
73
74#[derive(Debug, Default, Serialize)]
75pub struct SyncEventUpdate {
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub status: Option<SyncEventStatus>,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 pub readings_synced: Option<u64>,
80 #[serde(skip_serializing_if = "Option::is_none")]
83 pub readings_skipped: Option<u64>,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub status_events_synced: Option<u64>,
86 #[serde(skip_serializing_if = "Vec::is_empty")]
87 pub errors: Vec<String>,
88 #[serde(skip_serializing_if = "Vec::is_empty")]
89 pub log: Vec<String>,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub duration_ms: Option<u64>,
92}
93
94#[derive(Debug, Default, Serialize)]
97pub struct SyncResult {
98 pub readings_synced: u64,
99 #[serde(default)]
102 pub readings_skipped: u64,
103 #[serde(default)]
106 pub readings_held: u64,
107 pub status_events_synced: u64,
108 pub full_sync: bool,
109 pub duration_ms: u64,
110 #[serde(skip_serializing_if = "Vec::is_empty")]
111 pub errors: Vec<String>,
112 #[serde(skip_serializing_if = "Vec::is_empty")]
113 pub log: Vec<String>,
114}
115
116#[derive(Debug)]
117pub enum SyncTrigger {
118 Scheduled,
119 Command { id: Uuid, full: bool },
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn test_enroll_request_serialization() {
128 let req = EnrollRequest {
129 client_id: "svc_abc".to_string(),
130 client_secret: "secret123".to_string(),
131 instance_id: "service-01".to_string(),
132 };
133 let json = serde_json::to_value(&req).unwrap();
134 assert_eq!(json["client_id"], "svc_abc");
135 assert_eq!(json["instance_id"], "service-01");
136 }
137
138 #[test]
139 fn test_enroll_response_deserialization() {
140 let json = serde_json::json!({
141 "service_id": "550e8400-e29b-41d4-a716-446655440000",
142 "session_token": "tok-abc"
143 });
144 let resp: EnrollResponse = serde_json::from_value(json).unwrap();
145 assert_eq!(resp.session_token, "tok-abc");
146 }
147
148 #[test]
149 fn test_heartbeat_response_with_commands() {
150 let json = serde_json::json!({
151 "session_token": "new-tok",
152 "pending_commands": [
153 {
154 "id": "550e8400-e29b-41d4-a716-446655440000",
155 "command": "trigger_sync",
156 "payload": null
157 }
158 ]
159 });
160 let resp: HeartbeatResponse = serde_json::from_value(json).unwrap();
161 assert_eq!(resp.pending_commands.len(), 1);
162 assert_eq!(resp.pending_commands[0].command, "trigger_sync");
163 }
164
165 #[test]
166 fn test_sync_result_default() {
167 let r = SyncResult::default();
168 assert_eq!(r.readings_synced, 0);
169 assert!(!r.full_sync);
170 assert!(r.errors.is_empty());
171 }
172
173 #[test]
174 fn test_sync_result_serialization_skips_empty() {
175 let r = SyncResult {
176 readings_synced: 100,
177 ..Default::default()
178 };
179 let json = serde_json::to_value(&r).unwrap();
180 assert_eq!(json["readings_synced"], 100);
181 assert!(json.get("errors").is_none());
182 }
183
184 #[test]
185 fn test_sync_event_create_serialization() {
186 let ev = SyncEventCreate {
187 service_id: Uuid::nil(),
188 command_id: None,
189 event_type: SyncEventType::Scheduled,
190 status: SyncEventStatus::Running,
191 };
192 let json = serde_json::to_value(&ev).unwrap();
193 assert_eq!(json["event_type"], "scheduled");
194 assert_eq!(json["status"], "running");
195 assert!(json.get("command_id").is_none());
196 }
197
198 #[test]
199 fn test_sync_event_update_skips_empty() {
200 let upd = SyncEventUpdate {
201 status: Some(SyncEventStatus::Completed),
202 readings_synced: Some(5),
203 ..Default::default()
204 };
205 let json = serde_json::to_value(&upd).unwrap();
206 assert_eq!(json["status"], "completed");
207 assert_eq!(json["readings_synced"], 5);
208 assert!(json.get("errors").is_none());
209 assert!(json.get("duration_ms").is_none());
210 }
211}