A better Rust ATProto crate
1use heck::ToPascalCase; 2use jacquard_common::CowStr; 3use proc_macro2::TokenStream; 4use quote::quote; 5 6/// Convert a value string to a valid Rust variant name 7pub(super) fn value_to_variant_name(value: &str) -> String { 8 // Remove leading special chars and convert to pascal case 9 let clean = value.trim_start_matches(|c: char| !c.is_alphanumeric()); 10 let variant = clean.replace('-', "_").to_pascal_case(); 11 12 // Prefix with underscore if starts with digit 13 if variant.chars().next().map_or(false, |c| c.is_ascii_digit()) { 14 format!("_{}", variant) 15 } else if variant.is_empty() { 16 "Unknown".to_string() 17 } else { 18 variant 19 } 20} 21 22/// Sanitize a string to be safe for identifiers and filenames 23pub(super) fn sanitize_name(s: &str) -> String { 24 if s.is_empty() { 25 return "unknown".to_string(); 26 } 27 28 // Replace invalid characters with underscores 29 let mut sanitized: String = s 30 .chars() 31 .map(|c| { 32 if c.is_alphanumeric() || c == '_' { 33 c 34 } else { 35 '_' 36 } 37 }) 38 .collect(); 39 40 // Ensure it doesn't start with a digit 41 if sanitized 42 .chars() 43 .next() 44 .map_or(false, |c| c.is_ascii_digit()) 45 { 46 sanitized = format!("_{}", sanitized); 47 } 48 49 sanitized 50} 51 52/// Create an identifier, using raw identifier if necessary for keywords 53pub(super) fn make_ident(s: &str) -> syn::Ident { 54 if s.is_empty() { 55 eprintln!("Warning: Empty identifier encountered, using 'unknown' as fallback"); 56 return syn::Ident::new("unknown", proc_macro2::Span::call_site()); 57 } 58 59 let sanitized = sanitize_name(s); 60 61 // Try to parse as ident, fall back to raw ident if needed 62 syn::parse_str::<syn::Ident>(&sanitized).unwrap_or_else(|_| { 63 // only print if the sanitization actually changed the name 64 // for types where the name is a keyword, will prepend 'r#' 65 if s != sanitized { 66 eprintln!( 67 "Warning: Invalid identifier '{}' sanitized to '{}'", 68 s, sanitized 69 ); 70 syn::Ident::new(&sanitized, proc_macro2::Span::call_site()) 71 } else { 72 syn::Ident::new_raw(&sanitized, proc_macro2::Span::call_site()) 73 } 74 }) 75} 76 77/// Generate doc comment from optional description 78pub(super) fn generate_doc_comment(desc: Option<&CowStr>) -> TokenStream { 79 if let Some(description) = desc { 80 let desc_str = format!(" {description}"); 81 quote! { 82 #[doc = #desc_str] 83 } 84 } else { 85 quote! {} 86 } 87}