derive_defs/
validation.rs1use crate::{Error, Result};
7use std::path::Path;
8
9#[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 match chars.next() {
25 Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
26 _ => return false,
27 }
28
29 if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_') {
31 return false;
32 }
33
34 !is_rust_keyword(name)
36}
37
38fn 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", "abstract", "become", "box", "do", "final", "macro", "override", "priv", "typeof",
46 "unsized", "virtual", "yield", "try",
47 ];
48
49 KEYWORDS.contains(&name)
50}
51
52pub 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
68pub fn validate_trait_names(traits: &[String]) -> Result<()> {
76 for name in traits {
77 validate_trait_name(name)?;
78 }
79 Ok(())
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
86pub enum CrateType {
87 Binary,
89 Library,
91 ProcMacro,
93 Unknown,
95}
96
97pub 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 if content.contains("proc-macro = true") {
114 return Ok(CrateType::ProcMacro);
115 }
116
117 if content.contains("[[bin]]") || content.contains("[bin]") {
119 return Ok(CrateType::Binary);
120 }
121
122 if content.contains("[lib]") {
124 return Ok(CrateType::Library);
125 }
126
127 Ok(CrateType::Unknown)
128}
129
130fn has_proc_macro_member(workspace_root: &Path, workspace_content: &str) -> bool {
132 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 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 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
171pub 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 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 return Ok(());
196 }
197
198 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")); assert!(!is_valid_identifier("impl")); }
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 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 let macros_dir = temp_dir.path().join("macros");
408 std::fs::create_dir(¯os_dir).unwrap();
409 std::fs::write(macros_dir.join("Cargo.toml"), macros_content).unwrap();
410
411 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 assert!(result.is_ok());
419 }
420}