A better Rust ATProto crate
at main 170 lines 6.5 kB view raw
1use super::nsid_utils::NsidPath; 2use super::utils::{namespace_prefix, sanitize_name, sanitize_name_cow}; 3use super::CodeGenerator; 4use heck::{ToPascalCase, ToSnakeCase}; 5 6impl<'c> CodeGenerator<'c> { 7 /// Check if a single-variant union is self-referential 8 pub(super) fn is_self_referential_union( 9 &self, 10 nsid: &str, 11 parent_type_name: &str, 12 union: &crate::lexicon::LexRefUnion, 13 ) -> bool { 14 if union.refs.len() != 1 { 15 return false; 16 } 17 18 let ref_str = if union.refs[0].starts_with('#') { 19 format!("{}{}", nsid, union.refs[0]) 20 } else { 21 union.refs[0].to_string() 22 }; 23 24 let (ref_nsid, ref_def) = if let Some((nsid_part, fragment)) = ref_str.split_once('#') { 25 (nsid_part, fragment) 26 } else { 27 (ref_str.as_str(), "main") 28 }; 29 30 let ref_type_name = self.def_to_type_name(ref_nsid, ref_def); 31 ref_type_name == parent_type_name 32 } 33 34 /// Helper to generate field-based type name with collision detection 35 pub(super) fn generate_field_type_name( 36 &self, 37 nsid: &str, 38 parent_type_name: &str, 39 field_name: &str, 40 suffix: &str, // "" for union/object, "Item" for array unions 41 ) -> String { 42 let base_name = format!("{}{}{}", parent_type_name, field_name.to_pascal_case(), suffix); 43 44 // Check for collisions with lexicon defs 45 if let Some(doc) = self.corpus.get(nsid) { 46 let def_names: std::collections::HashSet<String> = doc 47 .defs 48 .keys() 49 .map(|name| self.def_to_type_name(nsid, name.as_ref())) 50 .collect(); 51 52 if def_names.contains(&base_name) { 53 // Use "Union" suffix for union types, "Record" for objects 54 let disambiguator = if suffix.is_empty() && !parent_type_name.is_empty() { 55 "Union" 56 } else { 57 "Record" 58 }; 59 return format!("{}{}{}{}", parent_type_name, disambiguator, field_name.to_pascal_case(), suffix); 60 } 61 } 62 63 base_name 64 } 65 66 /// Convert lexicon def name to base Rust type name (without prelude collision handling) 67 fn def_to_base_type_name(&self, nsid: &str, def_name: &str) -> String { 68 if def_name == "main" { 69 // Use last segment of NSID 70 let nsid_path = NsidPath::parse(nsid); 71 let base_name = nsid_path.last_segment().to_pascal_case(); 72 73 // Check if any other def would collide with this name 74 if let Some(doc) = self.corpus.get(nsid) { 75 let has_collision = doc.defs.keys().any(|other_def| { 76 let other_def_str: &str = other_def.as_ref(); 77 other_def_str != "main" && other_def_str.to_pascal_case() == base_name 78 }); 79 80 if has_collision { 81 return format!("{}Record", base_name); 82 } 83 } 84 85 base_name 86 } else { 87 def_name.to_pascal_case() 88 } 89 } 90 91 /// Apply prelude collision fix if needed 92 fn apply_prelude_collision_fix(&self, nsid: &str, def_name: &str, base_name: String) -> String { 93 // Prelude types that would shadow if used as type names 94 const PRELUDE_TYPES: &[&str] = &[ 95 "Option", "Result", "String", "Vec", "Box", 96 "Some", "None", "Ok", "Err", 97 ]; 98 99 if !PRELUDE_TYPES.contains(&base_name.as_str()) { 100 return base_name; 101 } 102 103 // Add contextual prefix to avoid collision 104 if def_name == "main" { 105 // Use second-to-last NSID segment for main defs 106 let nsid_path = NsidPath::parse(nsid); 107 let parts = nsid_path.segments(); 108 if parts.len() >= 2 { 109 format!("{}{}", parts[parts.len() - 2].to_pascal_case(), base_name) 110 } else { 111 format!("Lex{}", base_name) // fallback 112 } 113 } else { 114 // Use main def's type name as prefix for nested defs 115 let main_base = self.def_to_base_type_name(nsid, "main"); 116 format!("{}{}", main_base, base_name) 117 } 118 } 119 120 /// Convert lexicon def name to Rust type name 121 pub(super) fn def_to_type_name(&self, nsid: &str, def_name: &str) -> String { 122 let base_name = self.def_to_base_type_name(nsid, def_name); 123 self.apply_prelude_collision_fix(nsid, def_name, base_name) 124 } 125 126 /// Convert NSID to file path relative to output directory 127 /// 128 /// - `app.bsky.feed.post` → `app_bsky/feed/post.rs` 129 /// - `com.atproto.label.defs` → `com_atproto/label.rs` (defs go in parent) 130 pub(super) fn nsid_to_file_path(&self, nsid: &str) -> std::path::PathBuf { 131 let nsid_path = NsidPath::parse(nsid); 132 let parts = nsid_path.segments(); 133 134 if parts.len() < 2 { 135 // Shouldn't happen with valid NSIDs, but handle gracefully 136 return format!("{}.rs", sanitize_name(parts[0])).into(); 137 } 138 139 let last = nsid_path.last_segment(); 140 141 if nsid_path.is_defs() && parts.len() >= 3 { 142 // defs go in parent module: com.atproto.label.defs → com_atproto/label.rs 143 let first_two = namespace_prefix(parts[0], parts[1]); 144 if parts.len() == 3 { 145 // com.atproto.defs → com_atproto.rs 146 format!("{}.rs", first_two).into() 147 } else { 148 // com.atproto.label.defs → com_atproto/label.rs 149 let middle: Vec<&str> = parts[2..parts.len() - 1].iter().copied().collect(); 150 let mut path = std::path::PathBuf::from(first_two); 151 for segment in &middle[..middle.len() - 1] { 152 path.push(sanitize_name_cow(segment).as_ref()); 153 } 154 path.push(format!("{}.rs", sanitize_name_cow(middle.last().unwrap()))); 155 path 156 } 157 } else { 158 // Regular path: app.bsky.feed.post → app_bsky/feed/post.rs 159 let first_two = namespace_prefix(parts[0], parts[1]); 160 let mut path = std::path::PathBuf::from(first_two); 161 162 for segment in &parts[2..parts.len() - 1] { 163 path.push(sanitize_name_cow(segment).as_ref()); 164 } 165 166 path.push(format!("{}.rs", sanitize_name_cow(&last.to_snake_case()))); 167 path 168 } 169 } 170}