A human-friendly DSL for ATProto Lexicons
27
fork

Configure Feed

Select the types of activity you want to include in your feed.

Skip main promotion in .defs lexicons

authored by stavola.xyz and committed by

Tangled e6b00ed8 23681144

+93 -104
+93 -104
mlf-codegen/src/lib.rs
··· 114 114 115 115 pub fn generate_lexicon(namespace: &str, lexicon: &Lexicon, workspace: &Workspace) -> Value { 116 116 let usage_counts = analyze_type_usage(lexicon); 117 - 118 - // Extract the last segment of the namespace to determine main 119 - let namespace_parts: Vec<&str> = namespace.split('.').collect(); 120 - let expected_main_name = namespace_parts.last().copied().unwrap_or(""); 121 - let is_defs_namespace = expected_main_name == "defs"; 122 - 123 - // Count main-eligible items (records, queries, procedures, subscriptions, def types) without @main 124 - let main_eligible_items: Vec<&Item> = lexicon.items.iter() 125 - .filter(|item| { 126 - matches!(item, Item::Record(_) | Item::Query(_) | Item::Procedure(_) | Item::Subscription(_) | Item::DefType(_)) 127 - }) 128 - .collect(); 129 - 130 - let main_eligible_count = main_eligible_items.len(); 131 - 132 - // Check if any item has @main annotation 133 - let has_explicit_main = main_eligible_items.iter().any(|item| { 134 - match item { 135 - Item::Record(r) => has_main_annotation(&r.annotations), 136 - Item::Query(q) => has_main_annotation(&q.annotations), 137 - Item::Procedure(p) => has_main_annotation(&p.annotations), 138 - Item::Subscription(s) => has_main_annotation(&s.annotations), 139 - Item::DefType(d) => has_main_annotation(&d.annotations), 140 - _ => false, 141 - } 142 - }); 117 + let eligibility = MainEligibility::for_lexicon(namespace, lexicon); 143 118 144 119 let mut defs = Map::new(); 145 120 146 121 for item in &lexicon.items { 147 122 match item { 148 123 Item::Record(record) => { 149 - let record_json = generate_record_json(record, &usage_counts, workspace, namespace); 150 - 151 - // Check if this should be main 152 - let is_main = if has_explicit_main { 153 - // If @main is used explicitly, only that item is main 154 - has_main_annotation(&record.annotations) 155 - } else { 156 - // Otherwise use heuristics: single item or name matches namespace 157 - main_eligible_count == 1 || (!is_defs_namespace && record.name.name == expected_main_name) 158 - }; 159 - 160 - if is_main { 161 - defs.insert("main".to_string(), record_json); 162 - } else { 163 - defs.insert(record.name.name.clone(), record_json); 164 - } 124 + let value = generate_record_json(record, &usage_counts, workspace, namespace); 125 + insert_def(&mut defs, &record.name.name, eligibility.is_main(&record.name.name, &record.annotations), value); 165 126 } 166 127 Item::Query(query) => { 167 - let query_json = generate_query_json(query, &usage_counts, workspace, namespace); 168 - 169 - let is_main = if has_explicit_main { 170 - has_main_annotation(&query.annotations) 171 - } else { 172 - main_eligible_count == 1 || (!is_defs_namespace && query.name.name == expected_main_name) 173 - }; 174 - 175 - if is_main { 176 - defs.insert("main".to_string(), query_json); 177 - } else { 178 - defs.insert(query.name.name.clone(), query_json); 179 - } 128 + let value = generate_query_json(query, &usage_counts, workspace, namespace); 129 + insert_def(&mut defs, &query.name.name, eligibility.is_main(&query.name.name, &query.annotations), value); 180 130 } 181 131 Item::Procedure(procedure) => { 182 - let procedure_json = generate_procedure_json(procedure, &usage_counts, workspace, namespace); 183 - 184 - let is_main = if has_explicit_main { 185 - has_main_annotation(&procedure.annotations) 186 - } else { 187 - main_eligible_count == 1 || (!is_defs_namespace && procedure.name.name == expected_main_name) 188 - }; 189 - 190 - if is_main { 191 - defs.insert("main".to_string(), procedure_json); 192 - } else { 193 - defs.insert(procedure.name.name.clone(), procedure_json); 194 - } 132 + let value = generate_procedure_json(procedure, &usage_counts, workspace, namespace); 133 + insert_def(&mut defs, &procedure.name.name, eligibility.is_main(&procedure.name.name, &procedure.annotations), value); 195 134 } 196 135 Item::Subscription(subscription) => { 197 - let subscription_json = generate_subscription_json(subscription, &usage_counts, workspace, namespace); 198 - 199 - let is_main = if has_explicit_main { 200 - has_main_annotation(&subscription.annotations) 201 - } else { 202 - main_eligible_count == 1 || (!is_defs_namespace && subscription.name.name == expected_main_name) 203 - }; 204 - 205 - if is_main { 206 - defs.insert("main".to_string(), subscription_json); 207 - } else { 208 - defs.insert(subscription.name.name.clone(), subscription_json); 209 - } 136 + let value = generate_subscription_json(subscription, &usage_counts, workspace, namespace); 137 + insert_def(&mut defs, &subscription.name.name, eligibility.is_main(&subscription.name.name, &subscription.annotations), value); 210 138 } 211 139 Item::DefType(def_type) => { 212 - let def_type_json = generate_def_type_json(def_type, &usage_counts, workspace, namespace); 213 - 214 - // Check if this should be main 215 - let is_main = if has_explicit_main { 216 - has_main_annotation(&def_type.annotations) 217 - } else { 218 - main_eligible_count == 1 || (!is_defs_namespace && def_type.name.name == expected_main_name) 219 - }; 220 - 221 - if is_main { 222 - defs.insert("main".to_string(), def_type_json); 223 - } else { 224 - defs.insert(def_type.name.name.clone(), def_type_json); 225 - } 226 - } 227 - Item::InlineType(_) => { 228 - // Inline types are never added to defs - they expand at point of use 229 - // TODO: inline expansion will be handled by workspace/cross-file resolution 140 + let value = generate_def_type_json(def_type, &usage_counts, workspace, namespace); 141 + insert_def(&mut defs, &def_type.name.name, eligibility.is_main(&def_type.name.name, &def_type.annotations), value); 230 142 } 231 143 Item::Token(token) => { 232 - let token_json = json!({ 233 - "type": "token", 234 - "description": extract_docs(&token.docs) 235 - }); 236 - defs.insert(token.name.name.clone(), token_json); 144 + let mut token_obj = Map::new(); 145 + token_obj.insert("type".to_string(), json!("token")); 146 + insert_opt_str(&mut token_obj, "description", &extract_docs(&token.docs)); 147 + defs.insert(token.name.name.clone(), Value::Object(token_obj)); 237 148 } 149 + // Inline types never appear in `defs` — they expand at their point 150 + // of use. Other item kinds (use statements, namespace blocks) are 151 + // structural and not emitted into the lexicon output. 238 152 _ => {} 239 153 } 240 154 } ··· 245 159 root.insert("id".to_string(), json!(namespace)); 246 160 root.insert("defs".to_string(), json!(defs)); 247 161 Value::Object(root) 162 + } 163 + 164 + /// Decides whether a given def should be emitted under the key `main` or 165 + /// under its own name. The rules are lexicon-level — they depend on 166 + /// whether the namespace is a `.defs` container, whether any item carries 167 + /// an explicit `@main`, and how many main-eligible items exist — so we 168 + /// compute the context once and consult it per item. 169 + struct MainEligibility { 170 + /// True when at least one item has `@main`; in that case only 171 + /// annotated items are main, and all heuristics are skipped. 172 + has_explicit_main: bool, 173 + /// Total count of items eligible to be main. 174 + eligible_count: usize, 175 + /// The last segment of the namespace (e.g. `profile` for 176 + /// `app.bsky.actor.profile`). Used for the "name matches namespace" 177 + /// heuristic. 178 + expected_main_name: String, 179 + /// True when the namespace ends in `.defs`. By convention these 180 + /// lexicons are containers for named defs and never have a main 181 + /// entry; heuristic promotion is suppressed here to preserve that 182 + /// convention on roundtrip. 183 + is_defs_namespace: bool, 184 + } 185 + 186 + impl MainEligibility { 187 + fn for_lexicon(namespace: &str, lexicon: &Lexicon) -> Self { 188 + let expected_main_name = namespace.rsplit('.').next().unwrap_or("").to_string(); 189 + let is_defs_namespace = expected_main_name == "defs"; 190 + 191 + let headers: Vec<_> = lexicon.items.iter().filter_map(item_header).collect(); 192 + let eligible_count = headers.len(); 193 + let has_explicit_main = headers.iter().any(|(_, ann)| has_main_annotation(ann)); 194 + 195 + Self { 196 + has_explicit_main, 197 + eligible_count, 198 + expected_main_name, 199 + is_defs_namespace, 200 + } 201 + } 202 + 203 + fn is_main(&self, name: &str, annotations: &[Annotation]) -> bool { 204 + if self.has_explicit_main { 205 + return has_main_annotation(annotations); 206 + } 207 + // `.defs` lexicons are pure containers — never promote a def to 208 + // `main`, not even when it's the only one in the file. 209 + if self.is_defs_namespace { 210 + return false; 211 + } 212 + self.eligible_count == 1 || name == self.expected_main_name 213 + } 214 + } 215 + 216 + /// Unified accessor for the main-eligible item kinds. Returns the def's 217 + /// declared name and annotations — the only facts the main-promotion 218 + /// decision needs from any given item. Returns `None` for item kinds 219 + /// that never participate in `main` eligibility (e.g. inline types, 220 + /// tokens, use statements). 221 + fn item_header(item: &Item) -> Option<(&str, &[Annotation])> { 222 + match item { 223 + Item::Record(r) => Some((&r.name.name, &r.annotations)), 224 + Item::Query(q) => Some((&q.name.name, &q.annotations)), 225 + Item::Procedure(p) => Some((&p.name.name, &p.annotations)), 226 + Item::Subscription(s) => Some((&s.name.name, &s.annotations)), 227 + Item::DefType(d) => Some((&d.name.name, &d.annotations)), 228 + _ => None, 229 + } 230 + } 231 + 232 + /// Insert a def into the lexicon's `defs` map under the canonical key — 233 + /// `"main"` for the main def, otherwise the def's own name. 234 + fn insert_def(defs: &mut Map<String, Value>, name: &str, is_main: bool, value: Value) { 235 + let key = if is_main { "main".to_string() } else { name.to_string() }; 236 + defs.insert(key, value); 248 237 } 249 238 250 239 fn analyze_type_usage(lexicon: &Lexicon) -> HashMap<String, usize> {