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}