nextjlc/
validation.rs

1/* src/validation.rs */
2
3/* SPDX-License-Identifier: MIT */
4/*
5 * Author Canmi <t@canmi.icu>
6 */
7
8/// A struct to hold the successful result of a validation check.
9/// It contains the calculated number of copper layers and a list of non-critical warnings.
10#[derive(Debug, PartialEq, Eq)]
11pub struct ValidationReport {
12    pub layer_count: u32,
13    pub warnings: Vec<String>,
14}
15
16/// Defines the prefixes for files that are absolutely required for a valid Gerber set.
17const REQUIRED_PREFIXES: &[&str] = &[
18    "Gerber_BoardOutlineLayer",
19    "Gerber_TopLayer",
20    "Gerber_TopSolderMaskLayer",
21];
22
23/// Validates a list of standardized Gerber filenames against a set of manufacturing rules.
24/// This function is pure: it takes a list of names and returns a result without any I/O.
25///
26/// # Arguments
27///
28/// * `files` - A slice of strings, where each string is a filename that has already
29///   been renamed to the standard format (e.g., by the `rename` module).
30///
31/// # Returns
32///
33/// * `Ok(ValidationReport)` - If all critical rules pass. The report includes the
34///   detected copper layer count and a list of warnings for non-critical issues
35///   (e.g., missing silkscreen or paste layers).
36/// * `Err(Vec<String>)` - If any critical rules fail. The vector contains a list of
37///   all error messages detailing what is missing or incorrect.
38pub fn validate_gerber_files(files: &[String]) -> Result<ValidationReport, Vec<String>> {
39    let mut errors: Vec<String> = Vec::new();
40    let mut warnings: Vec<String> = Vec::new();
41
42    // --- 1. Check for the presence of absolutely required files ---
43    for &prefix in REQUIRED_PREFIXES {
44        if !files.iter().any(|f| f.starts_with(prefix)) {
45            errors.push(format!("Missing required file starting with: '{}'", prefix));
46        }
47    }
48
49    // --- 2. Gather facts about the file set for conditional checks ---
50    let has_top_copper = files.iter().any(|f| f.starts_with("Gerber_TopLayer"));
51    let has_bottom_copper = files.iter().any(|f| f.starts_with("Gerber_BottomLayer"));
52    let inner_layer_count = files
53        .iter()
54        .filter(|f| f.starts_with("Gerber_InnerLayer"))
55        .count() as u32;
56
57    // --- 3. Perform conditional checks based on layer presence ---
58
59    // If a top copper layer exists, its associated layers should also exist.
60    if has_top_copper {
61        if !files
62            .iter()
63            .any(|f| f.starts_with("Gerber_TopSolderMaskLayer"))
64        {
65            // This is a required file, but we check it here for a more descriptive error message.
66            // Using a separate check to avoid duplicate messages if it's already in REQUIRED_PREFIXES.
67            if !errors
68                .iter()
69                .any(|e| e.contains("Gerber_TopSolderMaskLayer"))
70            {
71                errors.push("Top copper layer is present, but the required 'Gerber_TopSolderMaskLayer' is missing.".to_string());
72            }
73        }
74        if !files
75            .iter()
76            .any(|f| f.starts_with("Gerber_TopSilkscreenLayer"))
77        {
78            warnings.push(
79                "Warning: 'Gerber_TopSilkscreenLayer' is missing for the top side.".to_string(),
80            );
81        }
82        if !files
83            .iter()
84            .any(|f| f.starts_with("Gerber_TopPasteMaskLayer"))
85        {
86            warnings.push("Warning: 'Gerber_TopPasteMaskLayer' is missing. This is usually needed for SMD components.".to_string());
87        }
88    }
89
90    // If a bottom copper layer exists, its associated layers should also exist.
91    if has_bottom_copper {
92        if !files
93            .iter()
94            .any(|f| f.starts_with("Gerber_BottomSolderMaskLayer"))
95        {
96            errors.push(
97                "Bottom copper layer is present, but 'Gerber_BottomSolderMaskLayer' is missing."
98                    .to_string(),
99            );
100        }
101        if !files
102            .iter()
103            .any(|f| f.starts_with("Gerber_BottomSilkscreenLayer"))
104        {
105            warnings.push(
106                "Warning: 'Gerber_BottomSilkscreenLayer' is missing for the bottom side."
107                    .to_string(),
108            );
109        }
110        if !files
111            .iter()
112            .any(|f| f.starts_with("Gerber_BottomPasteMaskLayer"))
113        {
114            warnings.push(
115                "Warning: 'Gerber_BottomPasteMaskLayer' is missing for the bottom side."
116                    .to_string(),
117            );
118        }
119    }
120
121    // A multilayer board (top + inner) must have a bottom layer.
122    if has_top_copper && inner_layer_count > 0 && !has_bottom_copper {
123        errors.push("Invalid layer stackup: A board with top and inner copper layers must also have a bottom copper layer.".to_string());
124    }
125
126    // --- 4. Calculate final layer count ---
127    let total_layer_count =
128        (has_top_copper as u32) + (has_bottom_copper as u32) + inner_layer_count;
129
130    // --- 5. Return the final result ---
131    if errors.is_empty() {
132        Ok(ValidationReport {
133            layer_count: total_layer_count,
134            warnings,
135        })
136    } else {
137        Err(errors)
138    }
139}