A human-friendly DSL for ATProto Lexicons
27
fork

Configure Feed

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

Rewrite #main refs to match renamed main def

authored by stavola.xyz and committed by

Tangled fd3c06a0 0630254c

+93 -25
+61 -25
mlf-cli/src/generate/mlf.rs
··· 244 244 } 245 245 })?; 246 246 247 - // Create a context to pass the current namespace to type generation 248 247 let ctx = ConversionContext { 249 248 current_namespace: nsid.to_string(), 249 + local_main_name: last_segment.to_string(), 250 250 warnings: RefCell::new(Vec::new()), 251 251 }; 252 252 ··· 260 260 261 261 match def_type { 262 262 "record" => { 263 - let mlf = generate_record(name, def, last_segment, &ctx)?; 263 + let mlf = generate_record(name, def, &ctx)?; 264 264 output.push_str(&mlf); 265 265 output.push('\n'); 266 266 } 267 267 "query" => { 268 - let mlf = generate_query(name, def, last_segment, &ctx)?; 268 + let mlf = generate_query(name, def, &ctx)?; 269 269 output.push_str(&mlf); 270 270 output.push('\n'); 271 271 } 272 272 "procedure" => { 273 - let mlf = generate_procedure(name, def, last_segment, &ctx)?; 273 + let mlf = generate_procedure(name, def, &ctx)?; 274 274 output.push_str(&mlf); 275 275 output.push('\n'); 276 276 } 277 277 "subscription" => { 278 - let mlf = generate_subscription(name, def, last_segment, &ctx)?; 278 + let mlf = generate_subscription(name, def, &ctx)?; 279 279 output.push_str(&mlf); 280 280 output.push('\n'); 281 281 } ··· 286 286 } 287 287 _ => { 288 288 // All other types (object, string, array, union, etc.) are treated as def type 289 - let mlf = generate_def_type(name, def, last_segment, &ctx)?; 289 + let mlf = generate_def_type(name, def, &ctx)?; 290 290 output.push_str(&mlf); 291 291 output.push('\n'); 292 292 } ··· 301 301 302 302 struct ConversionContext { 303 303 current_namespace: String, 304 + /// MLF-side name of this lexicon's main def. 305 + /// 306 + /// When a def is named `main` in the source JSON we rename it in 307 + /// MLF to the namespace's last segment (so `app.bsky.feed.post`'s 308 + /// `defs.main` becomes `def type post` / `record post`). That rename 309 + /// exploits MLF's implicit-main convention — a def whose name 310 + /// matches the namespace's last segment is treated as `main` 311 + /// automatically — and makes the MLF read more naturally. 312 + /// 313 + /// But sibling defs often reference the main via a local `#main` 314 + /// ref; if we naively emit those as `main` in MLF they'd point at a 315 + /// no-longer-existent def. Storing the rename here lets every 316 + /// ref-emission path rewrite `main` → `local_main_name` 317 + /// consistently, keeping the MLF internally resolvable regardless 318 + /// of what the source JSON called the main def. 319 + local_main_name: String, 304 320 /// Non-fatal issues accumulated during conversion. Callers receive 305 321 /// these via [`MlfGenerateOutput`] and decide what to do with them 306 322 /// (print to stderr, collect for a summary, suppress). ··· 332 348 } 333 349 } 334 350 335 - fn generate_record(name: &str, def: &Value, last_segment: &str, ctx: &ConversionContext) -> Result<String, MlfGenerateError> { 351 + fn generate_record(name: &str, def: &Value, ctx: &ConversionContext) -> Result<String, MlfGenerateError> { 336 352 let mut output = String::new(); 337 353 338 354 // Add doc comment if present ··· 351 367 352 368 // Use last segment of NSID for "main" definitions 353 369 let record_name = if name == "main" { 354 - escape_name(last_segment) 370 + escape_name(&ctx.local_main_name) 355 371 } else { 356 372 escape_name(name) 357 373 }; ··· 399 415 Ok(output) 400 416 } 401 417 402 - fn generate_query(name: &str, def: &Value, last_segment: &str, ctx: &ConversionContext) -> Result<String, MlfGenerateError> { 418 + fn generate_query(name: &str, def: &Value, ctx: &ConversionContext) -> Result<String, MlfGenerateError> { 403 419 let mut output = String::new(); 404 420 405 421 // Add doc comment ··· 417 433 } 418 434 419 435 let query_name = if name == "main" { 420 - escape_name(last_segment) 436 + escape_name(&ctx.local_main_name) 421 437 } else { 422 438 escape_name(name) 423 439 }; ··· 493 509 Ok(output) 494 510 } 495 511 496 - fn generate_procedure(name: &str, def: &Value, last_segment: &str, ctx: &ConversionContext) -> Result<String, MlfGenerateError> { 512 + fn generate_procedure(name: &str, def: &Value, ctx: &ConversionContext) -> Result<String, MlfGenerateError> { 497 513 let mut output = String::new(); 498 514 499 515 // Add doc comment ··· 511 527 } 512 528 513 529 let procedure_name = if name == "main" { 514 - escape_name(last_segment) 530 + escape_name(&ctx.local_main_name) 515 531 } else { 516 532 escape_name(name) 517 533 }; ··· 592 608 Ok(output) 593 609 } 594 610 595 - fn generate_subscription(name: &str, def: &Value, last_segment: &str, ctx: &ConversionContext) -> Result<String, MlfGenerateError> { 611 + fn generate_subscription(name: &str, def: &Value, ctx: &ConversionContext) -> Result<String, MlfGenerateError> { 596 612 let mut output = String::new(); 597 613 598 614 // Add doc comment ··· 610 626 } 611 627 612 628 let subscription_name = if name == "main" { 613 - escape_name(last_segment) 629 + escape_name(&ctx.local_main_name) 614 630 } else { 615 631 escape_name(name) 616 632 }; ··· 681 697 Ok(output) 682 698 } 683 699 684 - fn generate_def_type(name: &str, def: &Value, last_segment: &str, ctx: &ConversionContext) -> Result<String, MlfGenerateError> { 700 + fn generate_def_type(name: &str, def: &Value, ctx: &ConversionContext) -> Result<String, MlfGenerateError> { 685 701 let mut output = String::new(); 686 702 687 703 // Add doc comment if present ··· 701 717 // Use last segment of NSID for "main" definitions 702 718 // Keywords are now allowed by the parser, so just escape with backticks 703 719 let def_name = if name == "main" { 704 - escape_name(last_segment) 720 + escape_name(&ctx.local_main_name) 705 721 } else { 706 722 escape_name(name) 707 723 }; ··· 1098 1114 Ok(resolve_ref_string(ref_str, ctx)) 1099 1115 } 1100 1116 1101 - /// Resolve a Lexicon ref string (e.g. `#defName` or `namespace#defName`) 1102 - /// into the MLF type name to emit in source. 1117 + /// Resolve a Lexicon ref string (e.g. `#defName`, `namespace#defName`, 1118 + /// or a bare NSID) into the MLF type name to emit in source. 1119 + /// 1120 + /// Local refs to this lexicon's `main` def get rewritten to the 1121 + /// renamed def name held in [`ConversionContext::local_main_name`] — 1122 + /// see that field's docs for the full rationale. 1103 1123 fn resolve_ref_string(ref_str: &str, ctx: &ConversionContext) -> String { 1104 1124 if let Some(stripped) = ref_str.strip_prefix('#') { 1105 - stripped.to_string() 1106 - } else if let Some((namespace, def_name)) = ref_str.split_once('#') { 1125 + return rewrite_local_main(stripped, ctx); 1126 + } 1127 + if let Some((namespace, def_name)) = ref_str.split_once('#') { 1107 1128 if namespace == ctx.current_namespace { 1108 - def_name.to_string() 1109 - } else { 1110 - format!("{}.{}", namespace, def_name) 1129 + return rewrite_local_main(def_name, ctx); 1111 1130 } 1131 + return format!("{}.{}", namespace, def_name); 1132 + } 1133 + // Bare NSID — implicit main of the named lexicon. If it names the 1134 + // current lexicon (rare but possible), rewrite to the local main 1135 + // name; otherwise leave as-is. 1136 + if ref_str == ctx.current_namespace { 1137 + return ctx.local_main_name.clone(); 1138 + } 1139 + ref_str.to_string() 1140 + } 1141 + 1142 + /// Rewrite a bare def name to account for the lexicon's main-def 1143 + /// rename. A def name of `"main"` in a lexicon whose main has been 1144 + /// renamed to its namespace's last segment becomes that segment 1145 + /// instead; any other name passes through unchanged. 1146 + fn rewrite_local_main(def_name: &str, ctx: &ConversionContext) -> String { 1147 + if def_name == "main" { 1148 + ctx.local_main_name.clone() 1112 1149 } else { 1113 - // Malformed ref — pass through unchanged rather than inventing a namespace. 1114 - ref_str.to_string() 1150 + def_name.to_string() 1115 1151 } 1116 1152 }
+9
tests/lexicon_to_mlf/main_renamed_local_ref/expected.mlf
··· 1 + @main 2 + def type thing = { 3 + value!: string, 4 + }; 5 + 6 + def type container = { 7 + items!: thing[], 8 + }; 9 +
+23
tests/lexicon_to_mlf/main_renamed_local_ref/input.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.example.thing", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "required": ["value"], 8 + "properties": { 9 + "value": {"type": "string"} 10 + } 11 + }, 12 + "container": { 13 + "type": "object", 14 + "required": ["items"], 15 + "properties": { 16 + "items": { 17 + "type": "array", 18 + "items": {"type": "ref", "ref": "#main"} 19 + } 20 + } 21 + } 22 + } 23 + }