Skip to main content

derive_defs/
validation.rs

1//! Validation utilities for derive-defs configuration.
2//!
3//! This module provides validation functions to ensure that configuration
4//! values are valid and will produce correct generated code.
5
6use crate::{Error, Result};
7use std::path::Path;
8
9/// Check if a string is a valid Rust identifier.
10///
11/// A valid identifier:
12/// - Starts with a letter (a-z, A-Z) or underscore (_)
13/// - Contains only letters, digits (0-9), and underscores
14/// - Is not a Rust keyword
15#[must_use]
16pub fn is_valid_identifier(name: &str) -> bool {
17    if name.is_empty() {
18        return false;
19    }
20
21    let mut chars = name.chars();
22
23    // First character must be letter or underscore
24    match chars.next() {
25        Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
26        _ => return false,
27    }
28
29    // Rest must be alphanumeric or underscore
30    if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_') {
31        return false;
32    }
33
34    // Check for Rust keywords
35    !is_rust_keyword(name)
36}
37
38/// Check if a string is a Rust keyword.
39fn is_rust_keyword(name: &str) -> bool {
40    const KEYWORDS: &[&str] = &[
41        "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn",
42        "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref",
43        "return", "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe",
44        "use", "where", "while", "async", "await", "dyn", // Reserved keywords
45        "abstract", "become", "box", "do", "final", "macro", "override", "priv", "typeof",
46        "unsized", "virtual", "yield", "try",
47    ];
48
49    KEYWORDS.contains(&name)
50}
51
52/// Validate a trait name.
53///
54/// Returns an error if the trait name is not a valid Rust identifier.
55///
56/// # Errors
57///
58/// Returns `Error::Validation` if the trait name is invalid.
59pub fn validate_trait_name(name: &str) -> Result<()> {
60    if !is_valid_identifier(name) {
61        return Err(Error::Validation(format!(
62            "Invalid trait name: '{name}' is not a valid Rust identifier"
63        )));
64    }
65    Ok(())
66}
67
68/// Validate all trait names in a configuration.
69///
70/// Returns an error if any trait name is invalid.
71///
72/// # Errors
73///
74/// Returns `Error::Validation` if any trait name is invalid.
75pub fn validate_trait_names(traits: &[String]) -> Result<()> {
76    for name in traits {
77        validate_trait_name(name)?;
78    }
79    Ok(())
80}
81
82/// Detect the crate type from Cargo.toml.
83///
84/// This helps provide better error messages and guidance for users.
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub enum CrateType {
87    /// A binary crate (`[[bin]]` in Cargo.toml)
88    Binary,
89    /// A library crate (`[lib]` in Cargo.toml)
90    Library,
91    /// A proc-macro crate (`[lib] proc-macro = true` in Cargo.toml)
92    ProcMacro,
93    /// Unknown or workspace-only crate
94    Unknown,
95}
96
97/// Detect the crate type by reading the Cargo.toml file.
98///
99/// # Errors
100///
101/// Returns an error if the Cargo.toml cannot be read or parsed.
102pub fn detect_crate_type(manifest_dir: &Path) -> Result<CrateType> {
103    let cargo_toml = manifest_dir.join("Cargo.toml");
104
105    let content = std::fs::read_to_string(&cargo_toml).map_err(|e| {
106        Error::ConfigRead(std::io::Error::new(
107            std::io::ErrorKind::NotFound,
108            format!("Could not read {}: {e}", cargo_toml.display()),
109        ))
110    })?;
111
112    // Check for proc-macro
113    if content.contains("proc-macro = true") {
114        return Ok(CrateType::ProcMacro);
115    }
116
117    // Check for binary
118    if content.contains("[[bin]]") || content.contains("[bin]") {
119        return Ok(CrateType::Binary);
120    }
121
122    // Check for library
123    if content.contains("[lib]") {
124        return Ok(CrateType::Library);
125    }
126
127    Ok(CrateType::Unknown)
128}
129
130/// Check if any workspace member is a proc-macro crate.
131fn has_proc_macro_member(workspace_root: &Path, workspace_content: &str) -> bool {
132    // Parse workspace members from the Cargo.toml
133    let members = workspace_content
134        .lines()
135        .find(|line| line.trim().starts_with("members"))
136        .and_then(|line| line.split('=').nth(1))
137        .map_or_else(Vec::new, |members_str| {
138            // Parse the members array
139            let members_str = members_str.trim();
140            if members_str.starts_with('[') {
141                members_str
142                    .trim_start_matches('[')
143                    .trim_end_matches(']')
144                    .split(',')
145                    .map(|s| s.trim().trim_matches('"').trim_matches('\'').to_string())
146                    .collect::<Vec<_>>()
147            } else {
148                vec![]
149            }
150        });
151
152    // Check each member
153    for member in members {
154        let member_path = if member == "." {
155            workspace_root.to_path_buf()
156        } else {
157            workspace_root.join(&member)
158        };
159
160        let cargo_toml = member_path.join("Cargo.toml");
161        if let Ok(content) = std::fs::read_to_string(&cargo_toml)
162            && content.contains("proc-macro = true")
163        {
164            return true;
165        }
166    }
167
168    false
169}
170
171/// Check if the current crate type is compatible with proc-macro generation.
172///
173/// Binary crates cannot define and use proc-macros in the same crate.
174/// This function provides helpful guidance when this is detected.
175///
176/// # Errors
177///
178/// Returns `Error::Validation` with guidance if binary crate does not have
179/// a separate macros crate in the workspace.
180pub fn validate_crate_type_for_macros(manifest_dir: &Path) -> Result<()> {
181    let crate_type = detect_crate_type(manifest_dir)?;
182
183    match crate_type {
184        CrateType::Binary => {
185            // Binary crate - check if there's a separate macros crate in workspace
186            let workspace_root = manifest_dir.parent().unwrap_or(manifest_dir);
187            let workspace_cargo = workspace_root.join("Cargo.toml");
188
189            if workspace_cargo.exists()
190                && let Ok(workspace_content) = std::fs::read_to_string(&workspace_cargo)
191                && workspace_content.contains("[workspace]")
192                && has_proc_macro_member(workspace_root, &workspace_content)
193            {
194                // User has a separate proc-macro crate in workspace
195                return Ok(());
196            }
197
198            // Binary crate without separate macros crate - provide guidance
199            Err(Error::Validation(
200                "Binary crates cannot define proc-macros that they also use. \
201                Create a separate proc-macro crate in your workspace. \
202                \n\n\
203                Recommended structure:\n\
204                - Create a `macros/` package with `[lib] proc-macro = true`\n\
205                - Move derive_defs.toml and build.rs to the macros package\n\
206                - Add `my-app-macros = { path = \"../macros\" }` to your binary's dependencies\n\
207                \n\n\
208                See https://doc.rust-lang.org/stable/reference/procedural-macros.html for more information.".to_string()
209            ))
210        }
211        CrateType::Library | CrateType::ProcMacro | CrateType::Unknown => Ok(()),
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn test_valid_identifiers() {
221        assert!(is_valid_identifier("Debug"));
222        assert!(is_valid_identifier("Clone"));
223        assert!(is_valid_identifier("Serialize"));
224        assert!(is_valid_identifier("my_trait"));
225        assert!(is_valid_identifier("_private"));
226        assert!(is_valid_identifier("Trait123"));
227    }
228
229    #[test]
230    fn test_invalid_identifiers() {
231        assert!(!is_valid_identifier(""));
232        assert!(!is_valid_identifier("123Trait"));
233        assert!(!is_valid_identifier("my-trait"));
234        assert!(!is_valid_identifier("my::trait"));
235        assert!(!is_valid_identifier("my trait"));
236        assert!(!is_valid_identifier("trait")); // keyword
237        assert!(!is_valid_identifier("impl")); // keyword
238    }
239
240    #[test]
241    fn test_validate_trait_name_valid() {
242        assert!(validate_trait_name("Debug").is_ok());
243        assert!(validate_trait_name("Clone").is_ok());
244    }
245
246    #[test]
247    fn test_validate_trait_name_invalid() {
248        assert!(validate_trait_name("").is_err());
249        assert!(validate_trait_name("123Trait").is_err());
250        assert!(validate_trait_name("trait").is_err());
251    }
252
253    #[test]
254    fn test_validate_trait_names() {
255        let valid = vec!["Debug".to_string(), "Clone".to_string()];
256        assert!(validate_trait_names(&valid).is_ok());
257
258        let invalid = vec!["Debug".to_string(), "123Invalid".to_string()];
259        assert!(validate_trait_names(&invalid).is_err());
260    }
261
262    #[test]
263    fn test_detect_crate_type_proc_macro() {
264        let content = r#"
265[package]
266name = "test-macros"
267version = "0.1.0"
268
269[lib]
270proc-macro = true
271"#;
272
273        let temp_dir = tempfile::tempdir().unwrap();
274        let cargo_toml = temp_dir.path().join("Cargo.toml");
275        std::fs::write(&cargo_toml, content).unwrap();
276
277        let crate_type = detect_crate_type(temp_dir.path()).unwrap();
278        assert_eq!(crate_type, CrateType::ProcMacro);
279    }
280
281    #[test]
282    fn test_detect_crate_type_binary() {
283        let content = r#"
284[package]
285name = "test-bin"
286version = "0.1.0"
287
288[[bin]]
289name = "test"
290path = "src/main.rs"
291"#;
292
293        let temp_dir = tempfile::tempdir().unwrap();
294        let cargo_toml = temp_dir.path().join("Cargo.toml");
295        std::fs::write(&cargo_toml, content).unwrap();
296
297        let crate_type = detect_crate_type(temp_dir.path()).unwrap();
298        assert_eq!(crate_type, CrateType::Binary);
299    }
300
301    #[test]
302    fn test_detect_crate_type_library() {
303        let content = r#"
304[package]
305name = "test-lib"
306version = "0.1.0"
307
308[lib]
309path = "src/lib.rs"
310"#;
311
312        let temp_dir = tempfile::tempdir().unwrap();
313        let cargo_toml = temp_dir.path().join("Cargo.toml");
314        std::fs::write(&cargo_toml, content).unwrap();
315
316        let crate_type = detect_crate_type(temp_dir.path()).unwrap();
317        assert_eq!(crate_type, CrateType::Library);
318    }
319
320    #[test]
321    fn test_validate_binary_without_macros_crate() {
322        let content = r#"
323[package]
324name = "test-bin"
325version = "0.1.0"
326
327[[bin]]
328name = "test"
329"#;
330
331        let temp_dir = tempfile::tempdir().unwrap();
332        let cargo_toml = temp_dir.path().join("Cargo.toml");
333        std::fs::write(&cargo_toml, content).unwrap();
334
335        let result = validate_crate_type_for_macros(temp_dir.path());
336        assert!(result.is_err());
337        assert!(
338            result
339                .unwrap_err()
340                .to_string()
341                .contains("Binary crates cannot define")
342        );
343    }
344
345    #[test]
346    fn test_validate_binary_without_macros_crate_in_workspace() {
347        let workspace_content = r#"
348[workspace]
349members = ["."]
350"#;
351
352        let bin_content = r#"
353[package]
354name = "test-bin"
355version = "0.1.0"
356
357[[bin]]
358name = "test"
359"#;
360
361        let temp_dir = tempfile::tempdir().unwrap();
362        let workspace_toml = temp_dir.path().join("Cargo.toml");
363        std::fs::write(&workspace_toml, workspace_content).unwrap();
364        std::fs::write(temp_dir.path().join("Cargo.toml"), bin_content).unwrap();
365
366        let result = validate_crate_type_for_macros(temp_dir.path());
367        assert!(result.is_err());
368        assert!(
369            result
370                .unwrap_err()
371                .to_string()
372                .contains("Binary crates cannot define")
373        );
374    }
375
376    #[test]
377    fn test_validate_binary_with_macros_crate() {
378        // Create workspace with macros member
379        let workspace_content = r#"
380[workspace]
381members = ["macros", "app"]
382"#;
383
384        let macros_content = r#"
385[package]
386name = "test-macros"
387version = "0.1.0"
388
389[lib]
390proc-macro = true
391"#;
392
393        let bin_content = r#"
394[package]
395name = "test-bin"
396version = "0.1.0"
397
398[[bin]]
399name = "test"
400"#;
401
402        let temp_dir = tempfile::tempdir().unwrap();
403        let workspace_toml = temp_dir.path().join("Cargo.toml");
404        std::fs::write(&workspace_toml, workspace_content).unwrap();
405
406        // Create macros directory with proc-macro crate
407        let macros_dir = temp_dir.path().join("macros");
408        std::fs::create_dir(&macros_dir).unwrap();
409        std::fs::write(macros_dir.join("Cargo.toml"), macros_content).unwrap();
410
411        // Create app directory (simulating binary crate location)
412        let app_dir = temp_dir.path().join("app");
413        std::fs::create_dir(&app_dir).unwrap();
414        std::fs::write(app_dir.join("Cargo.toml"), bin_content).unwrap();
415
416        let result = validate_crate_type_for_macros(&app_dir);
417        // Should be ok because we have a macros member with proc-macro = true
418        assert!(result.is_ok());
419    }
420}