A human-friendly DSL for ATProto Lexicons
27
fork

Configure Feed

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

Emit union refs as nsids

authored by stavola.xyz and committed by

Tangled 0fbec7bc 1ad33d36

+66 -74
+66 -74
mlf-codegen/src/lib.rs
··· 412 412 } 413 413 } 414 414 415 + /// Resolve a typed reference path to the ATProto NSID string it names 416 + /// (e.g. `#foo`, `app.bsky.actor.defs#profileViewBasic`, or a bare 417 + /// `com.atproto.repo.strongRef` for implicit-main references). Used by 418 + /// both direct `ref` types and union members — any place that needs to 419 + /// emit the string form of a reference. 420 + /// 421 + /// When workspace resolution fails (unresolved import, malformed path, 422 + /// etc.) we fall back to a best-effort rendering rather than erroring: 423 + /// the semantic check still happens in the resolver, so emitting a 424 + /// recognisable-looking ref keeps the JSON self-describing for tooling. 425 + fn resolve_ref_nsid(path: &Path, workspace: &Workspace, current_namespace: &str) -> String { 426 + if let Some(full_namespace) = workspace.resolve_reference_namespace(path, current_namespace) { 427 + let last_segment = path.segments.last().unwrap().name.as_str(); 428 + if full_namespace == current_namespace { 429 + // Sibling def in the current lexicon. 430 + return format!("#{}", last_segment); 431 + } 432 + return format!("{}#{}", full_namespace, last_segment); 433 + } 434 + 435 + if path.segments.len() == 1 { 436 + let name = &path.segments[0].name; 437 + // Single-segment path that didn't resolve via the workspace — 438 + // maybe an imported symbol whose original path we can look up. 439 + let imports = workspace.get_imports(current_namespace); 440 + if let Some((_, original_path)) = imports.iter().find(|(local, _)| local == name) { 441 + if original_path.len() > 1 { 442 + let namespace = original_path[..original_path.len() - 1].join("."); 443 + let type_name = original_path.last().unwrap(); 444 + return format!("{}#{}", namespace, type_name); 445 + } 446 + } 447 + // Not imported either — assume it names a sibling def. 448 + return format!("#{}", name); 449 + } 450 + 451 + // Multi-segment unresolved path: trust the author's NSID as written. 452 + let namespace = path.segments[..path.segments.len() - 1] 453 + .iter() 454 + .map(|s| s.name.as_str()) 455 + .collect::<Vec<_>>() 456 + .join("."); 457 + let def_name = &path.segments.last().unwrap().name; 458 + format!("{}#{}", namespace, def_name) 459 + } 460 + 415 461 fn generate_record_json(record: &Record, usage_counts: &HashMap<String, usize>, workspace: &Workspace, current_namespace: &str) -> Value { 416 462 let mut required = Vec::new(); 417 463 let mut properties = Map::new(); ··· 609 655 match ty { 610 656 Type::Primitive { kind, .. } => generate_primitive_json(*kind), 611 657 Type::Reference { path, .. } => { 612 - // Try to resolve this reference in the workspace 613 - if let Some(resolved_ty) = workspace.resolve_type_reference(path) { 614 - // Check if this is an inline type by looking in the workspace 615 - if workspace.is_inline_type(path) { 616 - // Inline type: expand it recursively 658 + // Inline types expand in place; other references resolve to an NSID. 659 + if workspace.is_inline_type(path) { 660 + if let Some(resolved_ty) = workspace.resolve_type_reference(path) { 617 661 return generate_type_json(&resolved_ty, usage_counts, workspace, current_namespace); 618 662 } 619 663 } 620 - 621 - // Not an inline type (or couldn't resolve) - generate a ref 622 - // First, try to get the fully resolved namespace for this type 623 - if let Some(full_namespace) = workspace.resolve_reference_namespace(path, current_namespace) { 624 - // We have the full namespace where this type is defined 625 - if full_namespace == current_namespace { 626 - // It's in the current namespace - use local reference 627 - let type_name = path.segments.last().unwrap().name.as_str(); 628 - json!({ 629 - "type": "ref", 630 - "ref": format!("#{}", type_name) 631 - }) 632 - } else { 633 - // It's in a different namespace - use full reference 634 - let type_name = path.segments.last().unwrap().name.as_str(); 635 - json!({ 636 - "type": "ref", 637 - "ref": format!("{}#{}", full_namespace, type_name) 638 - }) 639 - } 640 - } else if path.segments.len() == 1 { 641 - // Couldn't resolve namespace - check if it's an imported type 642 - let name = &path.segments[0].name; 643 - let imports = workspace.get_imports(current_namespace); 644 - 645 - // Look for this name in imports 646 - if let Some((_local_name, original_path)) = imports.iter().find(|(local, _)| local == name) { 647 - // Build the full namespace#type reference from the import path 648 - // original_path is like ["com", "atproto", "label", "defs", "label"] 649 - // We want "com.atproto.label.defs#label" 650 - if original_path.len() > 1 { 651 - let namespace = original_path[..original_path.len() - 1].join("."); 652 - let type_name = original_path.last().unwrap(); 653 - json!({ 654 - "type": "ref", 655 - "ref": format!("{}#{}", namespace, type_name) 656 - }) 657 - } else { 658 - // Fallback: single-segment import (shouldn't happen but handle it) 659 - json!({ 660 - "type": "ref", 661 - "ref": format!("#{}", name) 662 - }) 663 - } 664 - } else { 665 - // Not an import - assume local reference 666 - json!({ 667 - "type": "ref", 668 - "ref": format!("#{}", name) 669 - }) 670 - } 671 - } else { 672 - // Multi-segment path ref - use as-is 673 - let namespace = path.segments[..path.segments.len()-1] 674 - .iter() 675 - .map(|s| s.name.as_str()) 676 - .collect::<Vec<_>>() 677 - .join("."); 678 - let def_name = &path.segments.last().unwrap().name; 679 - 680 - json!({ 681 - "type": "ref", 682 - "ref": format!("{}#{}", namespace, def_name) 683 - }) 684 - } 664 + let nsid = resolve_ref_nsid(path, workspace, current_namespace); 665 + json!({ "type": "ref", "ref": nsid }) 685 666 } 686 667 Type::Array { inner, .. } => { 687 668 json!({ ··· 690 671 }) 691 672 } 692 673 Type::Union { types, closed, .. } => { 674 + // ATProto unions carry an array of NSID *strings*. For ref 675 + // members we resolve the NSID directly; for non-ref members 676 + // (e.g. `null` produced by MLF's nullable-via-union pattern) 677 + // we fall through to the typed form. That fallback is 678 + // non-spec — ATProto unions strictly contain refs — but 679 + // preserves the member's semantics until a proper nullable 680 + // lowering is added upstream. 693 681 let refs: Vec<Value> = types 694 682 .iter() 695 - .map(|t| generate_type_json(t, usage_counts, workspace, current_namespace)) 683 + .map(|t| match t { 684 + Type::Reference { path, .. } => { 685 + Value::String(resolve_ref_nsid(path, workspace, current_namespace)) 686 + } 687 + _ => generate_type_json(t, usage_counts, workspace, current_namespace), 688 + }) 696 689 .collect(); 697 690 let mut union_obj = Map::new(); 698 691 union_obj.insert("type".to_string(), json!("union")); 699 - union_obj.insert("refs".to_string(), json!(refs)); 700 - // Only emit "closed" field if true (closed unions) 701 - // Open unions omit the field (defaults to false per ATProto spec) 692 + union_obj.insert("refs".to_string(), Value::Array(refs)); 693 + // Only emit "closed" when true; open unions omit it per spec. 702 694 if *closed { 703 695 union_obj.insert("closed".to_string(), json!(true)); 704 696 }