peat_schema/validation/
model.rs1use super::{ValidationError, ValidationResult};
6use crate::model::v1::{
7 DeploymentPolicy, DeploymentPriority, DeploymentState, ModelDeployment, ModelDeploymentStatus,
8 ModelType,
9};
10
11pub fn validate_model_deployment(deployment: &ModelDeployment) -> ValidationResult<()> {
27 if deployment.deployment_id.is_empty() {
29 return Err(ValidationError::MissingField("deployment_id".to_string()));
30 }
31
32 if deployment.model_id.is_empty() {
33 return Err(ValidationError::MissingField("model_id".to_string()));
34 }
35
36 if deployment.model_version.is_empty() {
37 return Err(ValidationError::MissingField("model_version".to_string()));
38 }
39
40 if deployment.model_type == ModelType::Unspecified as i32 {
42 return Err(ValidationError::InvalidValue(
43 "model_type must be specified".to_string(),
44 ));
45 }
46
47 if deployment.model_url.is_empty() {
49 return Err(ValidationError::MissingField("model_url".to_string()));
50 }
51
52 if !deployment.model_url.contains("://") {
54 return Err(ValidationError::InvalidValue(
55 "model_url must be a valid URL with scheme".to_string(),
56 ));
57 }
58
59 if deployment.checksum_sha256.is_empty() {
61 return Err(ValidationError::MissingField("checksum_sha256".to_string()));
62 }
63
64 if deployment.checksum_sha256.len() != 64 {
65 return Err(ValidationError::InvalidValue(format!(
66 "checksum_sha256 must be 64 hex characters, got {}",
67 deployment.checksum_sha256.len()
68 )));
69 }
70
71 if !deployment
73 .checksum_sha256
74 .chars()
75 .all(|c| c.is_ascii_hexdigit())
76 {
77 return Err(ValidationError::InvalidValue(
78 "checksum_sha256 must contain only hex characters".to_string(),
79 ));
80 }
81
82 if deployment.file_size_bytes == 0 {
84 return Err(ValidationError::InvalidValue(
85 "file_size_bytes must be non-zero".to_string(),
86 ));
87 }
88
89 if deployment.target_platforms.is_empty() {
91 return Err(ValidationError::MissingField(
92 "target_platforms (at least one required)".to_string(),
93 ));
94 }
95
96 if deployment.deployment_policy == DeploymentPolicy::Unspecified as i32 {
98 return Err(ValidationError::InvalidValue(
99 "deployment_policy must be specified".to_string(),
100 ));
101 }
102
103 if deployment.priority == DeploymentPriority::Unspecified as i32 {
105 return Err(ValidationError::InvalidValue(
106 "priority must be specified".to_string(),
107 ));
108 }
109
110 if deployment.deployed_at.is_none() {
112 return Err(ValidationError::MissingField("deployed_at".to_string()));
113 }
114
115 if deployment.deployed_by.is_empty() {
117 return Err(ValidationError::MissingField("deployed_by".to_string()));
118 }
119
120 Ok(())
121}
122
123pub fn validate_model_deployment_status(status: &ModelDeploymentStatus) -> ValidationResult<()> {
134 if status.deployment_id.is_empty() {
136 return Err(ValidationError::MissingField("deployment_id".to_string()));
137 }
138
139 if status.platform_id.is_empty() {
140 return Err(ValidationError::MissingField("platform_id".to_string()));
141 }
142
143 if status.state == DeploymentState::Unspecified as i32 {
145 return Err(ValidationError::InvalidValue(
146 "state must be specified".to_string(),
147 ));
148 }
149
150 if status.progress_percent > 100 {
152 return Err(ValidationError::InvalidValue(format!(
153 "progress_percent {} must be between 0 and 100",
154 status.progress_percent
155 )));
156 }
157
158 if status.updated_at.is_none() {
160 return Err(ValidationError::MissingField("updated_at".to_string()));
161 }
162
163 if status.state == DeploymentState::Failed as i32 && status.error_message.is_empty() {
165 return Err(ValidationError::MissingField(
166 "error_message (required when state is FAILED)".to_string(),
167 ));
168 }
169
170 if (status.state == DeploymentState::Complete as i32
172 || status.state == DeploymentState::Verifying as i32)
173 && status.downloaded_hash.is_empty()
174 {
175 return Err(ValidationError::MissingField(
176 "downloaded_hash (required for COMPLETE or VERIFYING state)".to_string(),
177 ));
178 }
179
180 if !status.downloaded_hash.is_empty() {
182 if status.downloaded_hash.len() != 64 {
183 return Err(ValidationError::InvalidValue(format!(
184 "downloaded_hash must be 64 hex characters, got {}",
185 status.downloaded_hash.len()
186 )));
187 }
188
189 if !status
190 .downloaded_hash
191 .chars()
192 .all(|c| c.is_ascii_hexdigit())
193 {
194 return Err(ValidationError::InvalidValue(
195 "downloaded_hash must contain only hex characters".to_string(),
196 ));
197 }
198 }
199
200 Ok(())
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206 use crate::common::v1::Timestamp;
207
208 fn valid_model_deployment() -> ModelDeployment {
209 ModelDeployment {
210 deployment_id: "deploy-2025-001".to_string(),
211 model_id: "yolov8-poi-v2.1".to_string(),
212 model_version: "2.1.0".to_string(),
213 model_type: ModelType::Detector as i32,
214 model_url: "https://models.example.com/yolov8-poi-v2.1.onnx".to_string(),
215 checksum_sha256: "a".repeat(64), file_size_bytes: 45_000_000,
217 target_platforms: vec!["Alpha-3".to_string(), "Bravo-1".to_string()],
218 deployment_policy: DeploymentPolicy::Rolling as i32,
219 priority: DeploymentPriority::Normal as i32,
220 deployed_at: Some(Timestamp {
221 seconds: 1702000000,
222 nanos: 0,
223 }),
224 deployed_by: "MLOps-Pipeline".to_string(),
225 rollback_model_id: String::new(),
226 metadata: None,
227 }
228 }
229
230 fn valid_deployment_status() -> ModelDeploymentStatus {
231 ModelDeploymentStatus {
232 deployment_id: "deploy-2025-001".to_string(),
233 platform_id: "Alpha-3".to_string(),
234 state: DeploymentState::Downloading as i32,
235 progress_percent: 45,
236 error_message: String::new(),
237 updated_at: Some(Timestamp {
238 seconds: 1702000100,
239 nanos: 0,
240 }),
241 downloaded_hash: String::new(),
242 previous_version: "2.0.0".to_string(),
243 }
244 }
245
246 #[test]
247 fn test_valid_model_deployment() {
248 let deployment = valid_model_deployment();
249 assert!(validate_model_deployment(&deployment).is_ok());
250 }
251
252 #[test]
253 fn test_missing_deployment_id() {
254 let mut deployment = valid_model_deployment();
255 deployment.deployment_id = String::new();
256 let err = validate_model_deployment(&deployment).unwrap_err();
257 assert!(matches!(err, ValidationError::MissingField(f) if f == "deployment_id"));
258 }
259
260 #[test]
261 fn test_missing_model_id() {
262 let mut deployment = valid_model_deployment();
263 deployment.model_id = String::new();
264 let err = validate_model_deployment(&deployment).unwrap_err();
265 assert!(matches!(err, ValidationError::MissingField(f) if f == "model_id"));
266 }
267
268 #[test]
269 fn test_unspecified_model_type() {
270 let mut deployment = valid_model_deployment();
271 deployment.model_type = ModelType::Unspecified as i32;
272 let err = validate_model_deployment(&deployment).unwrap_err();
273 assert!(matches!(err, ValidationError::InvalidValue(_)));
274 }
275
276 #[test]
277 fn test_invalid_model_url() {
278 let mut deployment = valid_model_deployment();
279 deployment.model_url = "not-a-valid-url".to_string();
280 let err = validate_model_deployment(&deployment).unwrap_err();
281 assert!(matches!(err, ValidationError::InvalidValue(_)));
282 }
283
284 #[test]
285 fn test_invalid_checksum_length() {
286 let mut deployment = valid_model_deployment();
287 deployment.checksum_sha256 = "abc123".to_string(); let err = validate_model_deployment(&deployment).unwrap_err();
289 assert!(matches!(err, ValidationError::InvalidValue(_)));
290 }
291
292 #[test]
293 fn test_invalid_checksum_chars() {
294 let mut deployment = valid_model_deployment();
295 deployment.checksum_sha256 = "g".repeat(64); let err = validate_model_deployment(&deployment).unwrap_err();
297 assert!(matches!(err, ValidationError::InvalidValue(_)));
298 }
299
300 #[test]
301 fn test_zero_file_size() {
302 let mut deployment = valid_model_deployment();
303 deployment.file_size_bytes = 0;
304 let err = validate_model_deployment(&deployment).unwrap_err();
305 assert!(matches!(err, ValidationError::InvalidValue(_)));
306 }
307
308 #[test]
309 fn test_empty_target_platforms() {
310 let mut deployment = valid_model_deployment();
311 deployment.target_platforms = vec![];
312 let err = validate_model_deployment(&deployment).unwrap_err();
313 assert!(matches!(err, ValidationError::MissingField(_)));
314 }
315
316 #[test]
317 fn test_unspecified_deployment_policy() {
318 let mut deployment = valid_model_deployment();
319 deployment.deployment_policy = DeploymentPolicy::Unspecified as i32;
320 let err = validate_model_deployment(&deployment).unwrap_err();
321 assert!(matches!(err, ValidationError::InvalidValue(_)));
322 }
323
324 #[test]
325 fn test_missing_deployed_at() {
326 let mut deployment = valid_model_deployment();
327 deployment.deployed_at = None;
328 let err = validate_model_deployment(&deployment).unwrap_err();
329 assert!(matches!(err, ValidationError::MissingField(f) if f == "deployed_at"));
330 }
331
332 #[test]
333 fn test_valid_deployment_status() {
334 let status = valid_deployment_status();
335 assert!(validate_model_deployment_status(&status).is_ok());
336 }
337
338 #[test]
339 fn test_status_missing_deployment_id() {
340 let mut status = valid_deployment_status();
341 status.deployment_id = String::new();
342 let err = validate_model_deployment_status(&status).unwrap_err();
343 assert!(matches!(err, ValidationError::MissingField(f) if f == "deployment_id"));
344 }
345
346 #[test]
347 fn test_missing_platform_id() {
348 let mut status = valid_deployment_status();
349 status.platform_id = String::new();
350 let err = validate_model_deployment_status(&status).unwrap_err();
351 assert!(matches!(err, ValidationError::MissingField(f) if f == "platform_id"));
352 }
353
354 #[test]
355 fn test_unspecified_state() {
356 let mut status = valid_deployment_status();
357 status.state = DeploymentState::Unspecified as i32;
358 let err = validate_model_deployment_status(&status).unwrap_err();
359 assert!(matches!(err, ValidationError::InvalidValue(_)));
360 }
361
362 #[test]
363 fn test_invalid_progress_percent() {
364 let mut status = valid_deployment_status();
365 status.progress_percent = 150; let err = validate_model_deployment_status(&status).unwrap_err();
367 assert!(matches!(err, ValidationError::InvalidValue(_)));
368 }
369
370 #[test]
371 fn test_missing_updated_at() {
372 let mut status = valid_deployment_status();
373 status.updated_at = None;
374 let err = validate_model_deployment_status(&status).unwrap_err();
375 assert!(matches!(err, ValidationError::MissingField(f) if f == "updated_at"));
376 }
377
378 #[test]
379 fn test_failed_state_requires_error_message() {
380 let mut status = valid_deployment_status();
381 status.state = DeploymentState::Failed as i32;
382 status.error_message = String::new();
383 let err = validate_model_deployment_status(&status).unwrap_err();
384 assert!(matches!(err, ValidationError::MissingField(_)));
385 }
386
387 #[test]
388 fn test_complete_state_requires_hash() {
389 let mut status = valid_deployment_status();
390 status.state = DeploymentState::Complete as i32;
391 status.downloaded_hash = String::new();
392 let err = validate_model_deployment_status(&status).unwrap_err();
393 assert!(matches!(err, ValidationError::MissingField(_)));
394 }
395
396 #[test]
397 fn test_valid_complete_status() {
398 let mut status = valid_deployment_status();
399 status.state = DeploymentState::Complete as i32;
400 status.downloaded_hash = "a".repeat(64);
401 status.progress_percent = 100;
402 assert!(validate_model_deployment_status(&status).is_ok());
403 }
404
405 #[test]
406 fn test_invalid_downloaded_hash_length() {
407 let mut status = valid_deployment_status();
408 status.state = DeploymentState::Complete as i32;
409 status.downloaded_hash = "abc123".to_string(); let err = validate_model_deployment_status(&status).unwrap_err();
411 assert!(matches!(err, ValidationError::InvalidValue(_)));
412 }
413}