1use super::{ValidationError, ValidationResult};
6use crate::product::v1::{
7 AlertProduct, AlertSeverity, AlertType, ChatProduct, ClassificationProduct, DetectionProduct,
8 EmbeddingProduct, ImageFormat, ImageProduct, Product, ProductType, SegmentationProduct,
9 SummaryProduct, SummaryType, TranscriptionProduct,
10};
11
12pub fn validate_product(product: &Product) -> ValidationResult<()> {
22 if product.product_id.is_empty() {
24 return Err(ValidationError::MissingField("product_id".to_string()));
25 }
26
27 if product.product_type == ProductType::Unspecified as i32 {
29 return Err(ValidationError::InvalidValue(
30 "product_type must be specified".to_string(),
31 ));
32 }
33
34 if product.source_platform.is_empty() {
35 return Err(ValidationError::MissingField("source_platform".to_string()));
36 }
37
38 if product.timestamp.is_none() {
40 return Err(ValidationError::MissingField("timestamp".to_string()));
41 }
42
43 if product.confidence < 0.0 || product.confidence > 1.0 {
45 return Err(ValidationError::InvalidConfidence(product.confidence));
46 }
47
48 if let Some(ref source) = product.model_source {
50 if source.model_id.is_empty() {
51 return Err(ValidationError::MissingField(
52 "model_source.model_id".to_string(),
53 ));
54 }
55 }
56
57 use crate::product::v1::product::Content;
59 match &product.content {
60 Some(Content::Image(img)) => validate_image_product(img)?,
61 Some(Content::Classification(cls)) => validate_classification_product(cls)?,
62 Some(Content::Detection(det)) => validate_detection_product(det)?,
63 Some(Content::Summary(sum)) => validate_summary_product(sum)?,
64 Some(Content::Chat(chat)) => validate_chat_product(chat)?,
65 Some(Content::Alert(alert)) => validate_alert_product(alert)?,
66 Some(Content::Embedding(emb)) => validate_embedding_product(emb)?,
67 Some(Content::Segmentation(seg)) => validate_segmentation_product(seg)?,
68 Some(Content::Transcription(trans)) => validate_transcription_product(trans)?,
69 None => {
70 return Err(ValidationError::MissingField("content".to_string()));
71 }
72 }
73
74 Ok(())
75}
76
77pub fn validate_image_product(image: &ImageProduct) -> ValidationResult<()> {
79 if image.format == ImageFormat::Unspecified as i32 {
81 return Err(ValidationError::InvalidValue(
82 "image format must be specified".to_string(),
83 ));
84 }
85
86 if image.width == 0 {
88 return Err(ValidationError::InvalidValue(
89 "image width must be positive".to_string(),
90 ));
91 }
92
93 if image.height == 0 {
94 return Err(ValidationError::InvalidValue(
95 "image height must be positive".to_string(),
96 ));
97 }
98
99 use crate::product::v1::image_product::ImageData;
101 match &image.image_data {
102 Some(ImageData::Data(bytes)) => {
103 if bytes.is_empty() {
104 return Err(ValidationError::InvalidValue(
105 "image data must not be empty".to_string(),
106 ));
107 }
108 }
109 Some(ImageData::DataBase64(b64)) => {
110 if b64.is_empty() {
111 return Err(ValidationError::InvalidValue(
112 "image data_base64 must not be empty".to_string(),
113 ));
114 }
115 }
116 Some(ImageData::Url(url)) => {
117 if url.is_empty() {
118 return Err(ValidationError::InvalidValue(
119 "image url must not be empty".to_string(),
120 ));
121 }
122 if !url.contains("://") {
123 return Err(ValidationError::InvalidValue(
124 "image url must be a valid URL with scheme".to_string(),
125 ));
126 }
127 }
128 Some(ImageData::BlobHash(hash)) => {
129 if hash.is_empty() {
130 return Err(ValidationError::InvalidValue(
131 "image blob_hash must not be empty".to_string(),
132 ));
133 }
134 }
135 None => {
136 return Err(ValidationError::MissingField("image_data".to_string()));
137 }
138 }
139
140 Ok(())
141}
142
143pub fn validate_classification_product(cls: &ClassificationProduct) -> ValidationResult<()> {
145 if cls.label.is_empty() {
146 return Err(ValidationError::MissingField("label".to_string()));
147 }
148
149 if cls.confidence < 0.0 || cls.confidence > 1.0 {
150 return Err(ValidationError::InvalidConfidence(cls.confidence));
151 }
152
153 for score in &cls.top_k {
155 if score.score < 0.0 || score.score > 1.0 {
156 return Err(ValidationError::InvalidConfidence(score.score));
157 }
158 }
159
160 Ok(())
161}
162
163pub fn validate_detection_product(det: &DetectionProduct) -> ValidationResult<()> {
165 if det.label.is_empty() {
166 return Err(ValidationError::MissingField("label".to_string()));
167 }
168
169 if det.confidence < 0.0 || det.confidence > 1.0 {
170 return Err(ValidationError::InvalidConfidence(det.confidence));
171 }
172
173 if det.bbox.len() != 4 {
175 return Err(ValidationError::InvalidValue(format!(
176 "bbox must have 4 elements, got {}",
177 det.bbox.len()
178 )));
179 }
180
181 if det.frame_size.len() != 2 {
183 return Err(ValidationError::InvalidValue(format!(
184 "frame_size must have 2 elements, got {}",
185 det.frame_size.len()
186 )));
187 }
188
189 Ok(())
190}
191
192pub fn validate_summary_product(summary: &SummaryProduct) -> ValidationResult<()> {
194 if summary.text.is_empty() {
195 return Err(ValidationError::MissingField("text".to_string()));
196 }
197
198 if summary.summary_type == SummaryType::Unspecified as i32 {
200 return Err(ValidationError::InvalidValue(
201 "summary_type must be specified".to_string(),
202 ));
203 }
204
205 Ok(())
206}
207
208pub fn validate_chat_product(chat: &ChatProduct) -> ValidationResult<()> {
210 if chat.response.is_empty() {
211 return Err(ValidationError::MissingField("response".to_string()));
212 }
213
214 if chat.model_name.is_empty() {
215 return Err(ValidationError::MissingField("model_name".to_string()));
216 }
217
218 if chat.temperature < 0.0 {
220 return Err(ValidationError::InvalidValue(
221 "temperature must be non-negative".to_string(),
222 ));
223 }
224
225 if chat.top_p < 0.0 || chat.top_p > 1.0 {
227 return Err(ValidationError::InvalidValue(format!(
228 "top_p {} must be between 0.0 and 1.0",
229 chat.top_p
230 )));
231 }
232
233 Ok(())
234}
235
236pub fn validate_alert_product(alert: &AlertProduct) -> ValidationResult<()> {
238 if alert.alert_type == AlertType::Unspecified as i32 {
240 return Err(ValidationError::InvalidValue(
241 "alert_type must be specified".to_string(),
242 ));
243 }
244
245 if alert.severity == AlertSeverity::Unspecified as i32 {
247 return Err(ValidationError::InvalidValue(
248 "severity must be specified".to_string(),
249 ));
250 }
251
252 if alert.message.is_empty() {
253 return Err(ValidationError::MissingField("message".to_string()));
254 }
255
256 Ok(())
257}
258
259pub fn validate_embedding_product(emb: &EmbeddingProduct) -> ValidationResult<()> {
261 if emb.vector.is_empty() {
262 return Err(ValidationError::MissingField("vector".to_string()));
263 }
264
265 if emb.dimensions == 0 {
266 return Err(ValidationError::InvalidValue(
267 "dimensions must be positive".to_string(),
268 ));
269 }
270
271 if emb.vector.len() != emb.dimensions as usize {
273 return Err(ValidationError::ConstraintViolation(format!(
274 "vector length {} does not match dimensions {}",
275 emb.vector.len(),
276 emb.dimensions
277 )));
278 }
279
280 if emb.embedding_model.is_empty() {
281 return Err(ValidationError::MissingField("embedding_model".to_string()));
282 }
283
284 Ok(())
285}
286
287pub fn validate_segmentation_product(seg: &SegmentationProduct) -> ValidationResult<()> {
289 if seg.mask_data.is_empty() {
290 return Err(ValidationError::MissingField("mask_data".to_string()));
291 }
292
293 if seg.width == 0 {
294 return Err(ValidationError::InvalidValue(
295 "width must be positive".to_string(),
296 ));
297 }
298
299 if seg.height == 0 {
300 return Err(ValidationError::InvalidValue(
301 "height must be positive".to_string(),
302 ));
303 }
304
305 Ok(())
306}
307
308pub fn validate_transcription_product(trans: &TranscriptionProduct) -> ValidationResult<()> {
310 if trans.text.is_empty() {
311 return Err(ValidationError::MissingField("text".to_string()));
312 }
313
314 if trans.language.is_empty() {
315 return Err(ValidationError::MissingField("language".to_string()));
316 }
317
318 if trans.confidence < 0.0 || trans.confidence > 1.0 {
319 return Err(ValidationError::InvalidConfidence(trans.confidence));
320 }
321
322 if trans.duration_seconds < 0.0 {
323 return Err(ValidationError::InvalidValue(
324 "duration_seconds must be non-negative".to_string(),
325 ));
326 }
327
328 Ok(())
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334 use crate::common::v1::Timestamp;
335 use crate::product::v1::product::Content;
336
337 fn valid_detection_product() -> Product {
338 Product {
339 product_id: "det-001".to_string(),
340 product_type: ProductType::Detection as i32,
341 source_platform: "Alpha-3".to_string(),
342 timestamp: Some(Timestamp {
343 seconds: 1702000000,
344 nanos: 0,
345 }),
346 confidence: 0.92,
347 model_source: None,
348 track_id: String::new(),
349 position: None,
350 content: Some(Content::Detection(DetectionProduct {
351 label: "person".to_string(),
352 confidence: 0.92,
353 bbox: vec![100, 200, 50, 100],
354 frame_size: vec![1920, 1080],
355 frame_number: 0,
356 detection_index: 0,
357 })),
358 attributes_json: String::new(),
359 }
360 }
361
362 #[test]
363 fn test_valid_detection_product() {
364 let product = valid_detection_product();
365 assert!(validate_product(&product).is_ok());
366 }
367
368 #[test]
369 fn test_missing_product_id() {
370 let mut product = valid_detection_product();
371 product.product_id = String::new();
372 let err = validate_product(&product).unwrap_err();
373 assert!(matches!(err, ValidationError::MissingField(f) if f == "product_id"));
374 }
375
376 #[test]
377 fn test_unspecified_product_type() {
378 let mut product = valid_detection_product();
379 product.product_type = ProductType::Unspecified as i32;
380 let err = validate_product(&product).unwrap_err();
381 assert!(matches!(err, ValidationError::InvalidValue(_)));
382 }
383
384 #[test]
385 fn test_missing_source_platform() {
386 let mut product = valid_detection_product();
387 product.source_platform = String::new();
388 let err = validate_product(&product).unwrap_err();
389 assert!(matches!(err, ValidationError::MissingField(f) if f == "source_platform"));
390 }
391
392 #[test]
393 fn test_invalid_confidence() {
394 let mut product = valid_detection_product();
395 product.confidence = 1.5;
396 let err = validate_product(&product).unwrap_err();
397 assert!(matches!(err, ValidationError::InvalidConfidence(_)));
398 }
399
400 #[test]
401 fn test_missing_content() {
402 let mut product = valid_detection_product();
403 product.content = None;
404 let err = validate_product(&product).unwrap_err();
405 assert!(matches!(err, ValidationError::MissingField(f) if f == "content"));
406 }
407
408 #[test]
409 fn test_invalid_bbox_length() {
410 let mut product = valid_detection_product();
411 product.content = Some(Content::Detection(DetectionProduct {
412 label: "person".to_string(),
413 confidence: 0.92,
414 bbox: vec![100, 200], frame_size: vec![1920, 1080],
416 frame_number: 0,
417 detection_index: 0,
418 }));
419 let err = validate_product(&product).unwrap_err();
420 assert!(matches!(err, ValidationError::InvalidValue(_)));
421 }
422
423 #[test]
424 fn test_valid_classification_product() {
425 let product = Product {
426 product_id: "cls-001".to_string(),
427 product_type: ProductType::Classification as i32,
428 source_platform: "Alpha-3".to_string(),
429 timestamp: Some(Timestamp {
430 seconds: 1702000000,
431 nanos: 0,
432 }),
433 confidence: 0.95,
434 model_source: None,
435 track_id: String::new(),
436 position: None,
437 content: Some(Content::Classification(ClassificationProduct {
438 label: "vehicle".to_string(),
439 confidence: 0.95,
440 top_k: vec![],
441 taxonomy: "coco".to_string(),
442 })),
443 attributes_json: String::new(),
444 };
445 assert!(validate_product(&product).is_ok());
446 }
447
448 #[test]
449 fn test_valid_embedding_product() {
450 let product = Product {
451 product_id: "emb-001".to_string(),
452 product_type: ProductType::Embedding as i32,
453 source_platform: "Alpha-3".to_string(),
454 timestamp: Some(Timestamp {
455 seconds: 1702000000,
456 nanos: 0,
457 }),
458 confidence: 1.0,
459 model_source: None,
460 track_id: String::new(),
461 position: None,
462 content: Some(Content::Embedding(EmbeddingProduct {
463 vector: vec![0.1, 0.2, 0.3, 0.4],
464 dimensions: 4,
465 embedding_model: "test-model".to_string(),
466 source_hash: String::new(),
467 normalized: false,
468 })),
469 attributes_json: String::new(),
470 };
471 assert!(validate_product(&product).is_ok());
472 }
473
474 #[test]
475 fn test_embedding_dimension_mismatch() {
476 let product = Product {
477 product_id: "emb-001".to_string(),
478 product_type: ProductType::Embedding as i32,
479 source_platform: "Alpha-3".to_string(),
480 timestamp: Some(Timestamp {
481 seconds: 1702000000,
482 nanos: 0,
483 }),
484 confidence: 1.0,
485 model_source: None,
486 track_id: String::new(),
487 position: None,
488 content: Some(Content::Embedding(EmbeddingProduct {
489 vector: vec![0.1, 0.2, 0.3, 0.4],
490 dimensions: 8, embedding_model: "test-model".to_string(),
492 source_hash: String::new(),
493 normalized: false,
494 })),
495 attributes_json: String::new(),
496 };
497 let err = validate_product(&product).unwrap_err();
498 assert!(matches!(err, ValidationError::ConstraintViolation(_)));
499 }
500}