1use crate::gldf::GldfProduct;
7use std::collections::{HashMap, HashSet};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ValidationLevel {
12 Error,
14 Warning,
16 Info,
18}
19
20#[derive(Debug, Clone)]
22pub struct ValidationError {
23 pub path: String,
25 pub level: ValidationLevel,
27 pub message: String,
29 pub code: &'static str,
31}
32
33impl ValidationError {
34 pub fn error(path: impl Into<String>, code: &'static str, message: impl Into<String>) -> Self {
36 Self {
37 path: path.into(),
38 level: ValidationLevel::Error,
39 message: message.into(),
40 code,
41 }
42 }
43
44 pub fn warning(
46 path: impl Into<String>,
47 code: &'static str,
48 message: impl Into<String>,
49 ) -> Self {
50 Self {
51 path: path.into(),
52 level: ValidationLevel::Warning,
53 message: message.into(),
54 code,
55 }
56 }
57
58 pub fn info(path: impl Into<String>, code: &'static str, message: impl Into<String>) -> Self {
60 Self {
61 path: path.into(),
62 level: ValidationLevel::Info,
63 message: message.into(),
64 code,
65 }
66 }
67}
68
69#[derive(Debug, Clone, Default)]
71pub struct ValidationResult {
72 pub errors: Vec<ValidationError>,
74}
75
76impl ValidationResult {
77 pub fn new() -> Self {
79 Self { errors: Vec::new() }
80 }
81
82 pub fn add(&mut self, error: ValidationError) {
84 self.errors.push(error);
85 }
86
87 pub fn has_errors(&self) -> bool {
89 self.errors
90 .iter()
91 .any(|e| e.level == ValidationLevel::Error)
92 }
93
94 pub fn has_warnings(&self) -> bool {
96 self.errors
97 .iter()
98 .any(|e| e.level == ValidationLevel::Warning)
99 }
100
101 pub fn is_valid(&self) -> bool {
103 self.errors.is_empty()
104 }
105
106 pub fn errors_only(&self) -> Vec<&ValidationError> {
108 self.errors
109 .iter()
110 .filter(|e| e.level == ValidationLevel::Error)
111 .collect()
112 }
113
114 pub fn warnings_only(&self) -> Vec<&ValidationError> {
116 self.errors
117 .iter()
118 .filter(|e| e.level == ValidationLevel::Warning)
119 .collect()
120 }
121
122 pub fn count_by_level(&self) -> (usize, usize, usize) {
124 let errors = self
125 .errors
126 .iter()
127 .filter(|e| e.level == ValidationLevel::Error)
128 .count();
129 let warnings = self
130 .errors
131 .iter()
132 .filter(|e| e.level == ValidationLevel::Warning)
133 .count();
134 let info = self
135 .errors
136 .iter()
137 .filter(|e| e.level == ValidationLevel::Info)
138 .count();
139 (errors, warnings, info)
140 }
141}
142
143pub fn validate_gldf(
152 product: &GldfProduct,
153 embedded_files: &HashMap<String, Vec<u8>>,
154) -> ValidationResult {
155 let mut result = ValidationResult::new();
156
157 let file_ids: HashSet<&str> = product
159 .general_definitions
160 .files
161 .file
162 .iter()
163 .map(|f| f.id.as_str())
164 .collect();
165
166 validate_header(product, &mut result);
168
169 validate_files(product, embedded_files, &mut result);
171
172 validate_photometries(product, &file_ids, &mut result);
174
175 validate_geometries(product, &file_ids, &mut result);
177
178 validate_light_sources(product, &mut result);
180
181 validate_emitters(product, &mut result);
183
184 validate_variants(product, &mut result);
186
187 validate_id_uniqueness(product, &mut result);
189
190 result
191}
192
193fn validate_header(product: &GldfProduct, result: &mut ValidationResult) {
194 let header = &product.header;
195
196 if header.author.is_empty() {
198 result.add(ValidationError::error(
199 "header.author",
200 "HEADER_001",
201 "Author is required",
202 ));
203 }
204
205 if header.manufacturer.is_empty() {
207 result.add(ValidationError::error(
208 "header.manufacturer",
209 "HEADER_002",
210 "Manufacturer is required",
211 ));
212 }
213
214 let version = &header.format_version;
216 if version.major < 1 {
217 result.add(ValidationError::warning(
218 "header.formatVersion",
219 "HEADER_003",
220 "Format version major should be at least 1",
221 ));
222 }
223
224 if header.creation_time_code.is_empty() {
226 result.add(ValidationError::info(
227 "header.creationTimeCode",
228 "HEADER_004",
229 "Consider adding a creation time code",
230 ));
231 }
232}
233
234fn validate_files(
235 product: &GldfProduct,
236 embedded_files: &HashMap<String, Vec<u8>>,
237 result: &mut ValidationResult,
238) {
239 for (i, file) in product.general_definitions.files.file.iter().enumerate() {
240 let path = format!("generalDefinitions.files[{}]", i);
241
242 if file.id.is_empty() {
244 result.add(ValidationError::error(
245 format!("{}.id", path),
246 "FILE_001",
247 "File ID is required",
248 ));
249 }
250
251 if file.content_type.is_empty() {
253 result.add(ValidationError::error(
254 format!("{}.contentType", path),
255 "FILE_002",
256 "Content type is required",
257 ));
258 }
259
260 if file.file_name.is_empty() {
262 result.add(ValidationError::error(
263 format!("{}.fileName", path),
264 "FILE_003",
265 "File name is required",
266 ));
267 }
268
269 if file.type_attr != "url" && !file.id.is_empty() && !embedded_files.contains_key(&file.id)
271 {
272 result.add(ValidationError::warning(
273 path.to_string(),
274 "FILE_004",
275 format!(
276 "Embedded file '{}' not found for file definition '{}'",
277 file.file_name, file.id
278 ),
279 ));
280 }
281
282 let valid_content_types = [
284 "ldc/eulumdat",
285 "ldc/ies",
286 "geo/l3d",
287 "geo/m3d",
288 "geo/r3d",
289 "image/png",
290 "image/jpg",
291 "image/jpeg",
292 "image/svg",
293 "document/pdf",
294 "spectrum/txt",
295 "sensor/sens-ldt",
296 "symbol/dxf",
297 "symbol/svg",
298 "other",
299 ];
300
301 if !file.content_type.is_empty()
302 && !valid_content_types.iter().any(|ct| {
303 file.content_type
304 .starts_with(ct.split('/').next().unwrap_or(""))
305 })
306 {
307 result.add(ValidationError::warning(
308 format!("{}.contentType", path),
309 "FILE_005",
310 format!("Unusual content type: '{}'", file.content_type),
311 ));
312 }
313 }
314}
315
316fn validate_photometries(
317 product: &GldfProduct,
318 file_ids: &HashSet<&str>,
319 result: &mut ValidationResult,
320) {
321 if let Some(ref photometries) = product.general_definitions.photometries {
322 for (i, photometry) in photometries.photometry.iter().enumerate() {
323 let path = format!("generalDefinitions.photometries[{}]", i);
324
325 if photometry.id.is_empty() {
327 result.add(ValidationError::error(
328 format!("{}.id", path),
329 "PHOT_001",
330 "Photometry ID is required",
331 ));
332 }
333
334 if let Some(ref file_ref) = photometry.photometry_file_reference {
336 if !file_ids.contains(file_ref.file_id.as_str()) {
337 result.add(ValidationError::error(
338 format!("{}.photometryFileReference.fileId", path),
339 "PHOT_002",
340 format!(
341 "Referenced file '{}' not found in file definitions",
342 file_ref.file_id
343 ),
344 ));
345 }
346 }
347 }
348 }
349}
350
351fn validate_geometries(
352 product: &GldfProduct,
353 file_ids: &HashSet<&str>,
354 result: &mut ValidationResult,
355) {
356 if let Some(ref geometries) = product.general_definitions.geometries {
357 for (i, geom) in geometries.simple_geometry.iter().enumerate() {
359 let path = format!("generalDefinitions.geometries.simpleGeometry[{}]", i);
360
361 if geom.id.is_empty() {
362 result.add(ValidationError::error(
363 format!("{}.id", path),
364 "GEOM_001",
365 "Geometry ID is required",
366 ));
367 }
368 }
369
370 for (i, geom) in geometries.model_geometry.iter().enumerate() {
372 let path = format!("generalDefinitions.geometries.modelGeometry[{}]", i);
373
374 if geom.id.is_empty() {
375 result.add(ValidationError::error(
376 format!("{}.id", path),
377 "GEOM_002",
378 "Model geometry ID is required",
379 ));
380 }
381
382 for (j, file_ref) in geom.geometry_file_reference.iter().enumerate() {
384 if !file_ids.contains(file_ref.file_id.as_str()) {
385 result.add(ValidationError::error(
386 format!("{}.geometryFileReference[{}].fileId", path, j),
387 "GEOM_003",
388 format!("Referenced geometry file '{}' not found", file_ref.file_id),
389 ));
390 }
391 }
392 }
393 }
394}
395
396fn validate_light_sources(product: &GldfProduct, result: &mut ValidationResult) {
397 if let Some(ref light_sources) = product.general_definitions.light_sources {
398 for (i, source) in light_sources.fixed_light_source.iter().enumerate() {
400 let path = format!("generalDefinitions.lightSources.fixedLightSource[{}]", i);
401
402 if source.id.is_empty() {
403 result.add(ValidationError::error(
404 format!("{}.id", path),
405 "LS_001",
406 "Fixed light source ID is required",
407 ));
408 }
409 }
410
411 for (i, source) in light_sources.changeable_light_source.iter().enumerate() {
413 let path = format!(
414 "generalDefinitions.lightSources.changeableLightSource[{}]",
415 i
416 );
417
418 if source.id.is_empty() {
419 result.add(ValidationError::error(
420 format!("{}.id", path),
421 "LS_002",
422 "Changeable light source ID is required",
423 ));
424 }
425 }
426 }
427}
428
429fn validate_emitters(product: &GldfProduct, result: &mut ValidationResult) {
430 if let Some(ref emitters) = product.general_definitions.emitters {
431 for (i, emitter) in emitters.emitter.iter().enumerate() {
432 let path = format!("generalDefinitions.emitters[{}]", i);
433
434 if emitter.id.is_empty() {
435 result.add(ValidationError::error(
436 format!("{}.id", path),
437 "EMIT_001",
438 "Emitter ID is required",
439 ));
440 }
441 }
442 }
443}
444
445fn validate_variants(product: &GldfProduct, result: &mut ValidationResult) {
446 if let Some(ref variants) = product.product_definitions.variants {
447 if variants.variant.is_empty() {
448 result.add(ValidationError::warning(
449 "productDefinitions.variants",
450 "VAR_001",
451 "No variants defined - consider adding at least one variant",
452 ));
453 }
454
455 for (i, variant) in variants.variant.iter().enumerate() {
456 let path = format!("productDefinitions.variants[{}]", i);
457
458 if variant.id.is_empty() {
459 result.add(ValidationError::error(
460 format!("{}.id", path),
461 "VAR_002",
462 "Variant ID is required",
463 ));
464 }
465 }
466 } else {
467 result.add(ValidationError::warning(
468 "productDefinitions.variants",
469 "VAR_003",
470 "No variants section - consider adding variants",
471 ));
472 }
473}
474
475fn validate_id_uniqueness(product: &GldfProduct, result: &mut ValidationResult) {
476 let mut file_ids = HashSet::new();
478 for file in &product.general_definitions.files.file {
479 if !file.id.is_empty() && !file_ids.insert(&file.id) {
480 result.add(ValidationError::error(
481 format!("generalDefinitions.files.{}", file.id),
482 "UNIQUE_001",
483 format!("Duplicate file ID: '{}'", file.id),
484 ));
485 }
486 }
487
488 if let Some(ref variants) = product.product_definitions.variants {
490 let mut variant_ids = HashSet::new();
491 for variant in &variants.variant {
492 if !variant.id.is_empty() && !variant_ids.insert(&variant.id) {
493 result.add(ValidationError::error(
494 format!("productDefinitions.variants.{}", variant.id),
495 "UNIQUE_002",
496 format!("Duplicate variant ID: '{}'", variant.id),
497 ));
498 }
499 }
500 }
501
502 if let Some(ref photometries) = product.general_definitions.photometries {
504 let mut phot_ids = HashSet::new();
505 for photometry in &photometries.photometry {
506 if !photometry.id.is_empty() && !phot_ids.insert(&photometry.id) {
507 result.add(ValidationError::error(
508 format!("generalDefinitions.photometries.{}", photometry.id),
509 "UNIQUE_003",
510 format!("Duplicate photometry ID: '{}'", photometry.id),
511 ));
512 }
513 }
514 }
515
516 if let Some(ref emitters) = product.general_definitions.emitters {
518 let mut emitter_ids = HashSet::new();
519 for emitter in &emitters.emitter {
520 if !emitter.id.is_empty() && !emitter_ids.insert(&emitter.id) {
521 result.add(ValidationError::error(
522 format!("generalDefinitions.emitters.{}", emitter.id),
523 "UNIQUE_004",
524 format!("Duplicate emitter ID: '{}'", emitter.id),
525 ));
526 }
527 }
528 }
529}
530
531impl GldfProduct {
532 pub fn validate(&self, embedded_files: &HashMap<String, Vec<u8>>) -> ValidationResult {
540 validate_gldf(self, embedded_files)
541 }
542
543 pub fn validate_structure(&self) -> ValidationResult {
547 validate_gldf(self, &HashMap::new())
548 }
549}
550
551#[cfg(test)]
552mod tests {
553 use super::*;
554
555 #[test]
556 fn test_validation_result_creation() {
557 let mut result = ValidationResult::new();
558 assert!(result.is_valid());
559 assert!(!result.has_errors());
560
561 result.add(ValidationError::error("test", "TEST_001", "Test error"));
562 assert!(!result.is_valid());
563 assert!(result.has_errors());
564 }
565
566 #[test]
567 fn test_validation_levels() {
568 let mut result = ValidationResult::new();
569 result.add(ValidationError::error("a", "E001", "Error"));
570 result.add(ValidationError::warning("b", "W001", "Warning"));
571 result.add(ValidationError::info("c", "I001", "Info"));
572
573 let (errors, warnings, info) = result.count_by_level();
574 assert_eq!(errors, 1);
575 assert_eq!(warnings, 1);
576 assert_eq!(info, 1);
577 }
578
579 #[test]
580 fn test_default_product_validation() {
581 let product = GldfProduct::default();
582 let result = product.validate_structure();
583
584 assert!(result.has_errors());
586 }
587}