A better Rust ATProto crate
at main 1275 lines 48 kB view raw
1use crate::error::Result; 2use crate::lexicon::{ 3 LexArrayItem, LexInteger, LexObject, LexObjectProperty, LexRecord, LexString, 4}; 5use heck::ToSnakeCase; 6use jacquard_common::deps::smol_str::SmolStr; 7use proc_macro2::TokenStream; 8use quote::quote; 9use std::collections::BTreeMap; 10 11use super::CodeGenerator; 12use super::prettify::GeneratedCode; 13use super::utils::{known_value_to_variant_name, make_ident, value_to_variant_name}; 14 15/// Enum variant kind for IntoStatic generation 16#[derive(Debug, Clone)] 17#[allow(dead_code)] 18pub(super) enum EnumVariantKind { 19 Unit, 20 Tuple, 21 Struct(Vec<String>), 22} 23 24impl<'c> CodeGenerator<'c> { 25 /// Generate all nested type definitions (unions, objects) for an object's properties. 26 /// This consolidates the pattern of iterating properties to find unions and nested objects 27 /// that need their own type definitions. 28 /// 29 /// # Parameters 30 /// - `include_nested_objects`: If false, skips generating nested object types (used by XRPC) 31 pub(super) fn generate_nested_types( 32 &self, 33 nsid: &str, 34 parent_type_name: &str, 35 properties: &BTreeMap<SmolStr, LexObjectProperty<'static>>, 36 include_nested_objects: bool, 37 resolved: &super::prettify::ResolvedImports, 38 ) -> Result<Vec<GeneratedCode>> { 39 let mut nested = Vec::new(); 40 41 for (field_name, field_type) in properties { 42 match field_type { 43 LexObjectProperty::Union(union) => { 44 // Skip empty, single-variant unions unless they're self-referential. 45 if !union.refs.is_empty() 46 && (union.refs.len() > 1 47 || self.is_self_referential_union(nsid, parent_type_name, &union)) 48 { 49 let union_name = 50 self.generate_field_type_name(nsid, parent_type_name, field_name, ""); 51 let refs: Vec<_> = union.refs.iter().cloned().collect(); 52 nested.push(self.generate_union( 53 nsid, 54 &union_name, 55 &refs, 56 None, 57 union.closed, 58 resolved, 59 )?); 60 } 61 } 62 LexObjectProperty::Object(nested_obj) if include_nested_objects => { 63 let object_name = 64 self.generate_field_type_name(nsid, parent_type_name, field_name, ""); 65 nested.push(self.generate_object(nsid, &object_name, &nested_obj, resolved)?); 66 } 67 LexObjectProperty::Array(array) => { 68 if let LexArrayItem::Union(union) = &array.items { 69 // Skip single-variant array unions. 70 if union.refs.len() > 1 { 71 let union_name = self.generate_field_type_name( 72 nsid, 73 parent_type_name, 74 field_name, 75 "Item", 76 ); 77 let refs: Vec<_> = union.refs.iter().cloned().collect(); 78 nested.push(self.generate_union( 79 nsid, 80 &union_name, 81 &refs, 82 None, 83 union.closed, 84 resolved, 85 )?); 86 } 87 } 88 } 89 LexObjectProperty::String(s) if s.known_values.is_some() => { 90 let enum_name = 91 self.generate_field_type_name(nsid, parent_type_name, field_name, ""); 92 nested.push(self.generate_inline_known_values_enum(&enum_name, s, resolved)?); 93 } 94 _ => {} 95 } 96 } 97 98 Ok(nested) 99 } 100 101 pub(super) fn generate_record( 102 &self, 103 nsid: &str, 104 def_name: &str, 105 record: &LexRecord<'static>, 106 resolved: &super::prettify::ResolvedImports, 107 ) -> Result<GeneratedCode> { 108 match &record.record { 109 crate::lexicon::LexRecordRecord::Object(obj) => { 110 let type_name = self.def_to_type_name(nsid, def_name); 111 let ident = syn::Ident::new(&type_name, proc_macro2::Span::call_site()); 112 113 // Records always get a lifetime since they have the #[lexicon] attribute 114 // which adds extra_data: BTreeMap<..., Data<'a>> 115 // Skip custom builder for types that conflict with the macro's unqualified type references 116 let has_builder = 117 !super::builder_heuristics::conflicts_with_builder_macro(&type_name); 118 119 // Generate main struct fields. 120 let (fields, default_fns) = 121 self.generate_object_fields(nsid, &type_name, obj, has_builder, resolved)?; 122 let doc = self.generate_doc_comment(record.description.as_ref()); 123 let manual_default = self.generate_manual_default(&type_name, obj, resolved); 124 125 let derive_attr = resolved.derive_standard(); 126 let lexicon_attr = 127 resolved.attribute_tokens(&super::prettify::ExternalImport::LexiconAttr); 128 let struct_def = quote! { 129 #doc 130 #lexicon_attr 131 #derive_attr 132 #[serde(rename_all = "camelCase", rename = #nsid, tag = "$type")] 133 pub struct #ident<'a> { 134 #fields 135 } 136 }; 137 138 // Generate custom builder if needed 139 let builder = if has_builder { 140 let ctx = super::builder_gen::BuilderGenContext::from_object( 141 self, nsid, &type_name, obj, true, // records always have lifetime 142 resolved, 143 ); 144 ctx.generate() 145 } else { 146 quote! {} 147 }; 148 149 // Generate union types and nested object types for this record 150 let unions = 151 self.generate_nested_types(nsid, &type_name, &obj.properties, true, resolved)?; 152 153 // Generate typed GetRecordOutput wrapper 154 let output_type_name = format!("{}GetRecordOutput", type_name); 155 let output_type_ident = 156 syn::Ident::new(&output_type_name, proc_macro2::Span::call_site()); 157 158 let is_none_path = resolved.option_is_none_path(); 159 let cid_type = resolved.type_tokens(&super::prettify::CommonType::Cid); 160 let at_uri_type = resolved.type_tokens(&super::prettify::CommonType::AtUri); 161 let option_cid = resolved.option_type(cid_type); 162 let output_wrapper = quote! { 163 /// Typed wrapper for GetRecord response with this collection's record type. 164 #derive_attr 165 #[serde(rename_all = "camelCase")] 166 pub struct #output_type_ident<'a> { 167 #[serde(skip_serializing_if = #is_none_path)] 168 #[serde(borrow)] 169 pub cid: #option_cid, 170 #[serde(borrow)] 171 pub uri: #at_uri_type, 172 #[serde(borrow)] 173 pub value: #ident<'a>, 174 } 175 }; 176 177 // Generate marker struct for XrpcResp. 178 let record_marker_name = format!("{}Record", type_name); 179 let record_marker_ident = 180 syn::Ident::new(&record_marker_name, proc_macro2::Span::call_site()); 181 182 let ser_path = 183 resolved.external_type_tokens(&super::prettify::ExternalImport::Serialize); 184 let de_path = 185 resolved.external_type_tokens(&super::prettify::ExternalImport::Deserialize); 186 let xrpc_resp_path = 187 resolved.external_type_tokens(&super::prettify::ExternalImport::XrpcResp); 188 let record_error_type = resolved 189 .type_tokens_with_lifetime(&super::prettify::CommonType::RecordError, "de"); 190 let record_marker = quote! { 191 /// Marker type for deserializing records from this collection. 192 #[derive(Debug, #ser_path, #de_path)] 193 pub struct #record_marker_ident; 194 195 impl #xrpc_resp_path for #record_marker_ident { 196 const NSID: &'static str = #nsid; 197 const ENCODING: &'static str = "application/json"; 198 type Output<'de> = #output_type_ident<'de>; 199 type Err<'de> = #record_error_type; 200 } 201 202 203 }; 204 let from_impl = quote! { 205 impl From<#output_type_ident<'_>> for #ident<'_> { 206 fn from(output: #output_type_ident<'_>) -> Self { 207 use jacquard_common::IntoStatic; 208 output.value.into_static() 209 } 210 } 211 }; 212 213 // Generate Collection trait impl. 214 let collection_path = resolved.type_path(&super::prettify::CommonType::Collection); 215 let collection_impl = quote! { 216 impl #collection_path for #ident<'_> { 217 const NSID: &'static str = #nsid; 218 type Record = #record_marker_ident; 219 } 220 }; 221 222 // Generate collection impl for the marker struct to drive fetch_record(). 223 let collection_marker_impl = quote! { 224 impl #collection_path for #record_marker_ident { 225 const NSID: &'static str = #nsid; 226 type Record = #record_marker_ident; 227 } 228 }; 229 230 // Generate LexiconSchema impl with shared lexicon_doc function 231 let (shared_fn, schema_impl) = 232 self.generate_schema_impl_with_shared(&type_name, nsid, "main", true, resolved); 233 234 // Merge nested type buckets into parent buckets. 235 let mut nested_type_defs = TokenStream::new(); 236 let mut nested_internals = TokenStream::new(); 237 for nested in unions { 238 nested_type_defs.extend(nested.type_defs); 239 nested_internals.extend(nested.inherent_impls); 240 nested_internals.extend(nested.trait_impls); 241 nested_internals.extend(nested.internals); 242 } 243 244 // Categorize tokens into buckets. 245 let type_defs = quote! { 246 #struct_def 247 #nested_type_defs 248 #output_wrapper 249 }; 250 251 let cowstr_type = resolved.type_tokens(&super::prettify::CommonType::CowStr); 252 let at_uri_path = resolved.type_path(&super::prettify::CommonType::AtUri); 253 let record_uri_path = 254 resolved.external_type_tokens(&super::prettify::ExternalImport::RecordUri); 255 let uri_error_path = 256 resolved.external_type_tokens(&super::prettify::ExternalImport::UriError); 257 let inherent_impls = quote! { 258 impl<'a> #ident<'a> { 259 pub fn uri(uri: impl Into<#cowstr_type>) -> Result<#record_uri_path<'a, #record_marker_ident>, #uri_error_path> { 260 #record_uri_path::try_from_uri(#at_uri_path::new_cow(uri.into())?) 261 } 262 } 263 }; 264 265 let trait_impls = quote! { 266 #record_marker 267 #from_impl 268 #collection_impl 269 #collection_marker_impl 270 #schema_impl 271 }; 272 273 let internals = quote! { 274 #(#default_fns)* 275 #manual_default 276 #nested_internals 277 #builder 278 #shared_fn 279 }; 280 281 Ok(GeneratedCode { 282 type_defs, 283 inherent_impls, 284 trait_impls, 285 internals, 286 imports: Default::default(), 287 }) 288 } 289 } 290 } 291 292 /// Generate an object type 293 pub(super) fn generate_object( 294 &self, 295 nsid: &str, 296 def_name: &str, 297 obj: &LexObject<'static>, 298 resolved: &super::prettify::ResolvedImports, 299 ) -> Result<GeneratedCode> { 300 let type_name = self.def_to_type_name(nsid, def_name); 301 let ident = syn::Ident::new(&type_name, proc_macro2::Span::call_site()); 302 303 // Objects always get a lifetime since they have the #[lexicon] attribute 304 // which adds extra_data: BTreeMap<..., Data<'a>> 305 306 // Smart heuristics for builder generation: 307 // - 0 required fields: Default instead of builder 308 // - All required fields are bare strings: Default instead of builder 309 // - 1+ required fields (not all strings): custom builder (but not if name conflicts) 310 let decision = super::builder_heuristics::should_generate_builder(&type_name, obj); 311 let has_builder = decision.has_builder; 312 313 let (fields, default_fns) = 314 self.generate_object_fields(nsid, &type_name, obj, has_builder, resolved)?; 315 let doc = self.generate_doc_comment(obj.description.as_ref()); 316 317 // Determine Default strategy: 318 // 1. Manual impl if schema defaults cover all required fields. 319 // 2. derive(Default) if heuristic says so (0 required, or all-string required). 320 // 3. No Default otherwise. 321 let manual_default = self.generate_manual_default(&type_name, obj, resolved); 322 let use_derive_default = manual_default.is_none() && decision.has_default; 323 324 let lexicon_attr = resolved.attribute_tokens(&super::prettify::ExternalImport::LexiconAttr); 325 let derive_attr = if use_derive_default { 326 resolved.derive_standard_with(quote! { Default }) 327 } else { 328 resolved.derive_standard() 329 }; 330 let struct_def = quote! { 331 #doc 332 #lexicon_attr 333 #derive_attr 334 #[serde(rename_all = "camelCase")] 335 pub struct #ident<'a> { 336 #fields 337 } 338 }; 339 340 // Generate custom builder if needed 341 let builder = if has_builder { 342 let ctx = super::builder_gen::BuilderGenContext::from_object( 343 self, nsid, &type_name, obj, true, // objects always have lifetime 344 resolved, 345 ); 346 ctx.generate() 347 } else { 348 quote! {} 349 }; 350 351 // Generate union types and nested object types for this object. 352 let nested_items = 353 self.generate_nested_types(nsid, &type_name, &obj.properties, true, resolved)?; 354 355 // Merge nested type buckets into parent buckets. 356 let mut nested_type_defs = TokenStream::new(); 357 let mut nested_internals = TokenStream::new(); 358 for nested in nested_items { 359 nested_type_defs.extend(nested.type_defs); 360 nested_internals.extend(nested.inherent_impls); 361 nested_internals.extend(nested.trait_impls); 362 nested_internals.extend(nested.internals); 363 } 364 365 // Generate LexiconSchema impl with shared lexicon_doc function. 366 let (shared_fn, schema_impl) = 367 self.generate_schema_impl_with_shared(&type_name, nsid, def_name, true, resolved); 368 369 // Categorize tokens into buckets. 370 let type_defs = quote! { 371 #struct_def 372 #nested_type_defs 373 }; 374 375 let trait_impls = quote! { 376 #schema_impl 377 }; 378 379 let internals = quote! { 380 #(#default_fns)* 381 #manual_default 382 #nested_internals 383 #builder 384 #shared_fn 385 }; 386 387 Ok(GeneratedCode { 388 type_defs, 389 inherent_impls: TokenStream::new(), 390 trait_impls, 391 internals, 392 imports: Default::default(), 393 }) 394 } 395 396 /// Generate fields for an object. 397 /// Returns (field tokens, companion default functions). 398 pub(super) fn generate_object_fields( 399 &self, 400 nsid: &str, 401 parent_type_name: &str, 402 obj: &LexObject<'static>, 403 _is_builder: bool, 404 resolved: &super::prettify::ResolvedImports, 405 ) -> Result<(TokenStream, Vec<TokenStream>)> { 406 let required = obj.required.as_ref().map(|r| r.as_slice()).unwrap_or(&[]); 407 let nullable = obj.nullable.as_ref().map(|n| n.as_slice()).unwrap_or(&[]); 408 409 let mut fields = Vec::new(); 410 let mut default_fns = Vec::new(); 411 for (field_name, field_type) in &obj.properties { 412 let is_required = required.contains(field_name); 413 let is_nullable = nullable.contains(field_name); 414 let (field_tokens, default_fn) = self.generate_field( 415 nsid, 416 parent_type_name, 417 field_name, 418 field_type, 419 is_required, 420 is_nullable, 421 resolved, 422 )?; 423 fields.push(field_tokens); 424 if let Some(f) = default_fn { 425 default_fns.push(f); 426 } 427 } 428 429 Ok((quote! { #(#fields)* }, default_fns)) 430 } 431 432 /// Generate a single field. 433 /// Returns (field tokens, optional companion default function). 434 pub(super) fn generate_field( 435 &self, 436 nsid: &str, 437 parent_type_name: &str, 438 field_name: &str, 439 field_type: &LexObjectProperty<'static>, 440 is_required: bool, 441 is_nullable: bool, 442 resolved: &super::prettify::ResolvedImports, 443 ) -> Result<(TokenStream, Option<TokenStream>)> { 444 if field_name.is_empty() { 445 eprintln!( 446 "Warning: Empty field name in lexicon '{}' type '{}', using 'unknown' as fallback", 447 nsid, parent_type_name 448 ); 449 } 450 let field_ident = make_ident(&field_name.to_snake_case()); 451 452 let rust_type = 453 self.property_to_rust_type(nsid, parent_type_name, field_name, field_type, resolved)?; 454 let needs_lifetime = self.property_needs_lifetime(field_type); 455 456 let is_optional = !is_required || is_nullable; 457 let rust_type = if !is_optional { 458 rust_type 459 } else { 460 resolved.option_type(rust_type) 461 }; 462 463 // Extract description from field type. 464 let description = match field_type { 465 LexObjectProperty::Ref(r) => r.description.as_ref(), 466 LexObjectProperty::Union(u) => u.description.as_ref(), 467 LexObjectProperty::Bytes(b) => b.description.as_ref(), 468 LexObjectProperty::CidLink(c) => c.description.as_ref(), 469 LexObjectProperty::Array(a) => a.description.as_ref(), 470 LexObjectProperty::Blob(b) => b.description.as_ref(), 471 LexObjectProperty::Object(o) => o.description.as_ref(), 472 LexObjectProperty::Boolean(b) => b.description.as_ref(), 473 LexObjectProperty::Integer(i) => i.description.as_ref(), 474 LexObjectProperty::String(s) => s.description.as_ref(), 475 LexObjectProperty::Unknown(u) => u.description.as_ref(), 476 }; 477 478 // Extract schema default and generate companion function + serde attr. 479 let (default_doc, serde_default_attr, default_fn) = self.extract_field_default( 480 parent_type_name, 481 field_name, 482 field_type, 483 is_optional, 484 resolved, 485 ); 486 487 // Combine description with default doc suffix. 488 let combined_desc = match (description, &default_doc) { 489 (Some(desc), Some(def_doc)) => Some(format!("{} {}", desc.as_ref(), def_doc)), 490 (Some(desc), None) => Some(desc.as_ref().to_string()), 491 (None, Some(def_doc)) => Some(def_doc.clone()), 492 (None, None) => None, 493 }; 494 let doc = combined_desc 495 .as_ref() 496 .map(|d| { 497 let d = d.as_str(); 498 quote! { #[doc = #d] } 499 }) 500 .unwrap_or_default(); 501 502 let mut attrs = Vec::new(); 503 504 if is_optional { 505 let is_none_path = resolved.option_is_none_path(); 506 attrs.push(quote! { #[serde(skip_serializing_if = #is_none_path)] }); 507 } 508 509 if let Some(serde_attr) = serde_default_attr { 510 attrs.push(serde_attr); 511 } 512 513 // Add serde(borrow) to all fields with lifetimes. 514 if needs_lifetime { 515 attrs.push(quote! { #[serde(borrow)] }); 516 } 517 518 if matches!(field_type, LexObjectProperty::Bytes(_)) { 519 if !is_optional { 520 attrs.push(quote! { #[serde(with = "jacquard_common::serde_bytes_helper")] }); 521 } else { 522 attrs.push( 523 quote! {#[serde(default, with = "jacquard_common::opt_serde_bytes_helper")] }, 524 ); 525 } 526 } 527 528 Ok(( 529 quote! { 530 #doc 531 #(#attrs)* 532 pub #field_ident: #rust_type, 533 }, 534 default_fn, 535 )) 536 } 537 538 /// Extract schema default value from a field type and generate the companion 539 /// default function and serde attribute. 540 /// 541 /// Returns (doc_suffix, serde_attr, companion_fn). 542 fn extract_field_default( 543 &self, 544 parent_type_name: &str, 545 field_name: &str, 546 field_type: &LexObjectProperty<'static>, 547 is_optional: bool, 548 resolved: &super::prettify::ResolvedImports, 549 ) -> (Option<String>, Option<TokenStream>, Option<TokenStream>) { 550 let fn_name = format!( 551 "_default_{}_{}", 552 parent_type_name.to_snake_case(), 553 field_name.to_snake_case() 554 ); 555 let fn_ident = syn::Ident::new(&fn_name, proc_macro2::Span::call_site()); 556 let serde_attr = quote! { #[serde(default = #fn_name)] }; 557 558 match field_type { 559 LexObjectProperty::Boolean(b) if b.default.is_some() => { 560 let v = b.default.unwrap(); 561 let doc = format!(" Defaults to `{}`.", v); 562 if is_optional { 563 let opt_bool = resolved.option_type(quote! { bool }); 564 ( 565 Some(doc), 566 Some(serde_attr), 567 Some(quote! { 568 fn #fn_ident() -> #opt_bool { Some(#v) } 569 }), 570 ) 571 } else { 572 ( 573 Some(doc), 574 Some(serde_attr), 575 Some(quote! { 576 fn #fn_ident() -> bool { #v } 577 }), 578 ) 579 } 580 } 581 LexObjectProperty::Integer(i) if i.default.is_some() => { 582 let v = i.default.unwrap(); 583 let doc = format!(" Defaults to `{}`.", v); 584 if is_optional { 585 let opt_i64 = resolved.option_type(quote! { i64 }); 586 ( 587 Some(doc), 588 Some(serde_attr), 589 Some(quote! { 590 fn #fn_ident() -> #opt_i64 { Some(#v) } 591 }), 592 ) 593 } else { 594 ( 595 Some(doc), 596 Some(serde_attr), 597 Some(quote! { 598 fn #fn_ident() -> i64 { #v } 599 }), 600 ) 601 } 602 } 603 LexObjectProperty::String(s) if s.default.is_some() && s.known_values.is_none() => { 604 let v = s.default.as_ref().unwrap().as_ref(); 605 let doc = format!(" Defaults to `\"{}\"`.", v); 606 let cowstr_path = resolved.type_path(&super::prettify::CommonType::CowStr); 607 if is_optional { 608 let opt_cowstr = resolved.option_type(quote! { #cowstr_path<'static> }); 609 ( 610 Some(doc), 611 Some(serde_attr), 612 Some(quote! { 613 fn #fn_ident() -> #opt_cowstr { 614 Some(#cowstr_path::from(#v)) 615 } 616 }), 617 ) 618 } else { 619 ( 620 Some(doc), 621 Some(serde_attr), 622 Some(quote! { 623 fn #fn_ident() -> #cowstr_path<'static> { 624 #cowstr_path::from(#v) 625 } 626 }), 627 ) 628 } 629 } 630 _ => (None, None, None), 631 } 632 } 633 634 /// Generate a manual `impl Default` for a struct when all required fields have 635 /// schema defaults. Optional fields default to `None` or `Some(schema_default)`. 636 pub(super) fn generate_manual_default( 637 &self, 638 type_name: &str, 639 obj: &LexObject<'static>, 640 resolved: &super::prettify::ResolvedImports, 641 ) -> Option<TokenStream> { 642 if !super::builder_heuristics::eligible_for_schema_default(obj) { 643 return None; 644 } 645 646 // Check if any field actually has a schema default. If none do, 647 // the existing derive(Default) is sufficient. 648 let any_schema_default = obj 649 .properties 650 .values() 651 .any(|p| super::builder_heuristics::has_schema_default(p)); 652 if !any_schema_default { 653 return None; 654 } 655 656 let ident = syn::Ident::new(type_name, proc_macro2::Span::call_site()); 657 let required = obj.required.as_ref().map(|r| r.as_slice()).unwrap_or(&[]); 658 let nullable = obj.nullable.as_ref().map(|n| n.as_slice()).unwrap_or(&[]); 659 660 let field_defaults: Vec<_> = obj 661 .properties 662 .iter() 663 .map(|(field_name, field_type)| { 664 let field_ident = make_ident(&field_name.to_snake_case()); 665 let is_required = required.contains(field_name); 666 let is_nullable = nullable.contains(field_name); 667 let is_optional = !is_required || is_nullable; 668 669 let value = self.schema_default_value(field_type, is_optional, resolved); 670 quote! { #field_ident: #value } 671 }) 672 .collect(); 673 674 Some(quote! { 675 impl Default for #ident<'_> { 676 fn default() -> Self { 677 Self { 678 #(#field_defaults,)* 679 extra_data: Default::default(), 680 } 681 } 682 } 683 }) 684 } 685 686 /// Generate the default value expression for a field. 687 fn schema_default_value( 688 &self, 689 field_type: &LexObjectProperty<'static>, 690 is_optional: bool, 691 resolved: &super::prettify::ResolvedImports, 692 ) -> TokenStream { 693 let inner = match field_type { 694 LexObjectProperty::Boolean(b) if b.default.is_some() => { 695 let v = b.default.unwrap(); 696 Some(quote! { #v }) 697 } 698 LexObjectProperty::Integer(i) if i.default.is_some() => { 699 let v = i.default.unwrap(); 700 Some(quote! { #v }) 701 } 702 LexObjectProperty::String(s) if s.default.is_some() && s.known_values.is_none() => { 703 let v = s.default.as_ref().unwrap().as_ref(); 704 let cowstr_path = resolved.type_path(&super::prettify::CommonType::CowStr); 705 Some(quote! { #cowstr_path::from(#v) }) 706 } 707 _ => None, 708 }; 709 710 match (inner, is_optional) { 711 (Some(val), true) => quote! { Some(#val) }, 712 (Some(val), false) => val, 713 (None, true) => quote! { None }, 714 (None, false) => quote! { Default::default() }, 715 } 716 } 717 718 /// Generate a union enum for refs 719 pub fn generate_union( 720 &self, 721 current_nsid: &str, 722 union_name: &str, 723 refs: &[jacquard_common::CowStr<'static>], 724 description: Option<&str>, 725 closed: Option<bool>, 726 resolved: &super::prettify::ResolvedImports, 727 ) -> Result<GeneratedCode> { 728 let enum_ident = syn::Ident::new(union_name, proc_macro2::Span::call_site()); 729 730 // Build variants using the union_codegen module 731 let ctx = super::union_codegen::UnionGenContext { 732 corpus: self.corpus, 733 namespace_deps: &self.namespace_deps, 734 current_nsid, 735 }; 736 737 let union_variants = 738 ctx.build_union_variants(refs, |ref_str| self.ref_to_rust_type(ref_str, resolved))?; 739 let variants = super::union_codegen::generate_variant_tokens(&union_variants); 740 741 let doc = description 742 .map(|d| quote! { #[doc = #d] }) 743 .unwrap_or_else(|| quote! {}); 744 745 // Only add open_union if not closed. 746 let is_open = closed != Some(true); 747 let derive_attr = resolved.derive_standard(); 748 749 let enum_def = if is_open { 750 let open_union_attr = 751 resolved.attribute_tokens(&super::prettify::ExternalImport::OpenUnion); 752 quote! { 753 #doc 754 #open_union_attr 755 #derive_attr 756 #[serde(tag = "$type", bound(deserialize = "'de: 'a"))] 757 pub enum #enum_ident<'a> { 758 #(#variants,)* 759 } 760 } 761 } else { 762 quote! { 763 #doc 764 #derive_attr 765 #[serde(tag = "$type")] 766 #[serde(bound(deserialize = "'de: 'a"))] 767 pub enum #enum_ident<'a> { 768 #(#variants,)* 769 } 770 } 771 }; 772 773 Ok(GeneratedCode::type_only(enum_def)) 774 } 775 776 /// Generate enum for string with known values. 777 pub(super) fn generate_known_values_enum( 778 &self, 779 nsid: &str, 780 def_name: &str, 781 string: &LexString<'static>, 782 resolved: &super::prettify::ResolvedImports, 783 ) -> Result<GeneratedCode> { 784 let type_name = self.def_to_type_name(nsid, def_name); 785 let ident = syn::Ident::new(&type_name, proc_macro2::Span::call_site()); 786 787 let known_values = string.known_values.as_ref().unwrap(); 788 let mut variants = Vec::new(); 789 let mut from_str_arms = Vec::new(); 790 let mut as_str_arms = Vec::new(); 791 792 let mut known_variant_names = std::collections::HashSet::new(); 793 for value in known_values { 794 // Convert value to valid Rust identifier 795 let value_str = value.as_ref(); 796 let variant_name = value_to_variant_name(value_str); 797 known_variant_names.insert(variant_name.clone()); 798 let variant_ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site()); 799 800 variants.push(quote! { 801 #variant_ident 802 }); 803 804 from_str_arms.push(quote! { 805 #value_str => Self::#variant_ident 806 }); 807 808 as_str_arms.push(quote! { 809 Self::#variant_ident => #value_str 810 }); 811 } 812 813 // Choose catch-all name, falling back if "Other" collides with a known value variant. 814 let catchall_name = if known_variant_names.contains("Other") { 815 "UnknownValue" 816 } else { 817 "Other" 818 }; 819 let catchall_ident = syn::Ident::new(catchall_name, proc_macro2::Span::call_site()); 820 821 let doc = self.generate_doc_comment(string.description.as_ref()); 822 823 // Generate IntoStatic impl 824 let variant_info: Vec<(String, EnumVariantKind)> = known_values 825 .iter() 826 .map(|value| { 827 let variant_name = value_to_variant_name(value.as_ref()); 828 (variant_name, EnumVariantKind::Unit) 829 }) 830 .chain(std::iter::once(( 831 catchall_name.to_string(), 832 EnumVariantKind::Tuple, 833 ))) 834 .collect(); 835 let into_static_impl = 836 self.generate_into_static_for_enum(&type_name, &variant_info, true, false); 837 838 let cowstr_type = resolved.type_tokens(&super::prettify::CommonType::CowStr); 839 let cowstr_path = resolved.type_path(&super::prettify::CommonType::CowStr); 840 let enum_def = quote! { 841 #doc 842 #[derive(Debug, Clone, PartialEq, Eq, Hash)] 843 pub enum #ident<'a> { 844 #(#variants,)* 845 #catchall_ident(#cowstr_type), 846 } 847 848 impl<'a> #ident<'a> { 849 pub fn as_str(&self) -> &str { 850 match self { 851 #(#as_str_arms,)* 852 Self::#catchall_ident(s) => s.as_ref(), 853 } 854 } 855 } 856 857 impl<'a> From<&'a str> for #ident<'a> { 858 fn from(s: &'a str) -> Self { 859 match s { 860 #(#from_str_arms,)* 861 _ => Self::#catchall_ident(#cowstr_path::from(s)), 862 } 863 } 864 } 865 866 impl<'a> From<String> for #ident<'a> { 867 fn from(s: String) -> Self { 868 match s.as_str() { 869 #(#from_str_arms,)* 870 _ => Self::#catchall_ident(#cowstr_path::from(s)), 871 } 872 } 873 } 874 875 impl<'a> AsRef<str> for #ident<'a> { 876 fn as_ref(&self) -> &str { 877 self.as_str() 878 } 879 } 880 881 impl<'a> core::fmt::Display for #ident<'a> { 882 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 883 write!(f, "{}", self.as_str()) 884 } 885 } 886 887 impl<'a> serde::Serialize for #ident<'a> { 888 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 889 where 890 S: serde::Serializer, 891 { 892 serializer.serialize_str(self.as_str()) 893 } 894 } 895 896 impl<'de, 'a> serde::Deserialize<'de> for #ident<'a> 897 where 898 'de: 'a, 899 { 900 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 901 where 902 D: serde::Deserializer<'de>, 903 { 904 let s = <&'de str>::deserialize(deserializer)?; 905 Ok(Self::from(s)) 906 } 907 } 908 909 #into_static_impl 910 }; 911 912 Ok(GeneratedCode::type_only(enum_def)) 913 } 914 915 /// Generate enum for inline string property with known values. 916 /// Unlike `generate_known_values_enum`, this takes the type name directly 917 /// and uses fragment extraction for NSID#fragment values. 918 pub(super) fn generate_inline_known_values_enum( 919 &self, 920 type_name: &str, 921 string: &LexString<'static>, 922 resolved: &super::prettify::ResolvedImports, 923 ) -> Result<GeneratedCode> { 924 let ident = syn::Ident::new(type_name, proc_macro2::Span::call_site()); 925 926 let known_values = string.known_values.as_ref().unwrap(); 927 let mut variants = Vec::new(); 928 let mut from_str_arms = Vec::new(); 929 let mut as_str_arms = Vec::new(); 930 let mut known_variant_names = std::collections::HashSet::new(); 931 932 for value in known_values { 933 let value_str = value.as_ref(); 934 // Use known_value_to_variant_name to extract fragment from NSID#fragment 935 let variant_name = known_value_to_variant_name(value_str); 936 known_variant_names.insert(variant_name.clone()); 937 let variant_ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site()); 938 939 variants.push(quote! { 940 #variant_ident 941 }); 942 943 from_str_arms.push(quote! { 944 #value_str => Self::#variant_ident 945 }); 946 947 as_str_arms.push(quote! { 948 Self::#variant_ident => #value_str 949 }); 950 } 951 952 // Choose catch-all name, falling back if "Other" collides with a known value variant. 953 let catchall_name = if known_variant_names.contains("Other") { 954 "UnknownValue" 955 } else { 956 "Other" 957 }; 958 let catchall_ident = syn::Ident::new(catchall_name, proc_macro2::Span::call_site()); 959 960 let doc = self.generate_doc_comment(string.description.as_ref()); 961 962 // Generate IntoStatic impl 963 let variant_info: Vec<(String, EnumVariantKind)> = known_values 964 .iter() 965 .map(|value| { 966 let variant_name = known_value_to_variant_name(value.as_ref()); 967 (variant_name, EnumVariantKind::Unit) 968 }) 969 .chain(std::iter::once(( 970 catchall_name.to_string(), 971 EnumVariantKind::Tuple, 972 ))) 973 .collect(); 974 let into_static_impl = 975 self.generate_into_static_for_enum(type_name, &variant_info, true, false); 976 977 let cowstr_type = resolved.type_tokens(&super::prettify::CommonType::CowStr); 978 let cowstr_path = resolved.type_path(&super::prettify::CommonType::CowStr); 979 let enum_def = quote! { 980 #doc 981 #[derive(Debug, Clone, PartialEq, Eq, Hash)] 982 pub enum #ident<'a> { 983 #(#variants,)* 984 #catchall_ident(#cowstr_type), 985 } 986 987 impl<'a> #ident<'a> { 988 pub fn as_str(&self) -> &str { 989 match self { 990 #(#as_str_arms,)* 991 Self::#catchall_ident(s) => s.as_ref(), 992 } 993 } 994 } 995 996 impl<'a> From<&'a str> for #ident<'a> { 997 fn from(s: &'a str) -> Self { 998 match s { 999 #(#from_str_arms,)* 1000 _ => Self::#catchall_ident(#cowstr_path::from(s)), 1001 } 1002 } 1003 } 1004 1005 impl<'a> From<String> for #ident<'a> { 1006 fn from(s: String) -> Self { 1007 match s.as_str() { 1008 #(#from_str_arms,)* 1009 _ => Self::#catchall_ident(#cowstr_path::from(s)), 1010 } 1011 } 1012 } 1013 1014 impl<'a> core::fmt::Display for #ident<'a> { 1015 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 1016 write!(f, "{}", self.as_str()) 1017 } 1018 } 1019 1020 impl<'a> AsRef<str> for #ident<'a> { 1021 fn as_ref(&self) -> &str { 1022 self.as_str() 1023 } 1024 } 1025 1026 impl<'a> serde::Serialize for #ident<'a> { 1027 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 1028 where 1029 S: serde::Serializer, 1030 { 1031 serializer.serialize_str(self.as_str()) 1032 } 1033 } 1034 1035 impl<'de, 'a> serde::Deserialize<'de> for #ident<'a> 1036 where 1037 'de: 'a, 1038 { 1039 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 1040 where 1041 D: serde::Deserializer<'de>, 1042 { 1043 let s = <&'de str>::deserialize(deserializer)?; 1044 Ok(Self::from(s)) 1045 } 1046 } 1047 1048 impl<'a> Default for #ident<'a> { 1049 fn default() -> Self { 1050 Self::#catchall_ident(Default::default()) 1051 } 1052 } 1053 1054 #into_static_impl 1055 }; 1056 1057 Ok(GeneratedCode::type_only(enum_def)) 1058 } 1059 1060 /// Generate enum for integer with enum values 1061 pub(super) fn generate_integer_enum( 1062 &self, 1063 nsid: &str, 1064 def_name: &str, 1065 integer: &LexInteger<'static>, 1066 ) -> Result<GeneratedCode> { 1067 let type_name = self.def_to_type_name(nsid, def_name); 1068 let ident = syn::Ident::new(&type_name, proc_macro2::Span::call_site()); 1069 1070 let enum_values = integer.r#enum.as_ref().unwrap(); 1071 let mut variants = Vec::new(); 1072 let mut from_i64_arms = Vec::new(); 1073 let mut to_i64_arms = Vec::new(); 1074 1075 for value in enum_values { 1076 let variant_name = format!("Value{}", value.abs()); 1077 let variant_ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site()); 1078 1079 variants.push(quote! { 1080 #[serde(rename = #value)] 1081 #variant_ident 1082 }); 1083 1084 from_i64_arms.push(quote! { 1085 #value => Self::#variant_ident 1086 }); 1087 1088 to_i64_arms.push(quote! { 1089 Self::#variant_ident => #value 1090 }); 1091 } 1092 1093 let doc = self.generate_doc_comment(integer.description.as_ref()); 1094 1095 let enum_def = quote! { 1096 #doc 1097 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 1098 pub enum #ident { 1099 #(#variants,)* 1100 #[serde(untagged)] 1101 Other(i64), 1102 } 1103 1104 impl #ident { 1105 pub fn as_i64(&self) -> i64 { 1106 match self { 1107 #(#to_i64_arms,)* 1108 Self::Other(n) => *n, 1109 } 1110 } 1111 } 1112 1113 impl From<i64> for #ident { 1114 fn from(n: i64) -> Self { 1115 match n { 1116 #(#from_i64_arms,)* 1117 _ => Self::Other(n), 1118 } 1119 } 1120 } 1121 1122 impl serde::Serialize for #ident { 1123 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 1124 where 1125 S: serde::Serializer, 1126 { 1127 serializer.serialize_i64(self.as_i64()) 1128 } 1129 } 1130 1131 impl<'de> serde::Deserialize<'de> for #ident { 1132 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 1133 where 1134 D: serde::Deserializer<'de>, 1135 { 1136 let n = i64::deserialize(deserializer)?; 1137 Ok(Self::from(n)) 1138 } 1139 } 1140 }; 1141 1142 Ok(GeneratedCode::type_only(enum_def)) 1143 } 1144 1145 /// Generate IntoStatic impl for a struct 1146 #[allow(dead_code)] 1147 pub(super) fn generate_into_static_for_struct( 1148 &self, 1149 type_name: &str, 1150 field_names: &[&str], 1151 has_lifetime: bool, 1152 has_extra_data: bool, 1153 ) -> TokenStream { 1154 let ident = syn::Ident::new(type_name, proc_macro2::Span::call_site()); 1155 1156 let field_idents: Vec<_> = field_names 1157 .iter() 1158 .map(|name| make_ident(&name.to_snake_case())) 1159 .collect(); 1160 1161 if has_lifetime { 1162 let field_conversions: Vec<_> = field_idents 1163 .iter() 1164 .map(|field| quote! { #field: self.#field.into_static() }) 1165 .collect(); 1166 1167 let extra_data_conversion = if has_extra_data { 1168 quote! { extra_data: self.extra_data.into_static(), } 1169 } else { 1170 quote! {} 1171 }; 1172 1173 quote! { 1174 impl jacquard_common::IntoStatic for #ident<'_> { 1175 type Output = #ident<'static>; 1176 1177 fn into_static(self) -> Self::Output { 1178 #ident { 1179 #(#field_conversions,)* 1180 #extra_data_conversion 1181 } 1182 } 1183 } 1184 } 1185 } else { 1186 quote! { 1187 impl jacquard_common::IntoStatic for #ident { 1188 type Output = #ident; 1189 1190 fn into_static(self) -> Self::Output { 1191 self 1192 } 1193 } 1194 } 1195 } 1196 } 1197 1198 /// Generate IntoStatic impl for an enum 1199 pub(super) fn generate_into_static_for_enum( 1200 &self, 1201 type_name: &str, 1202 variant_info: &[(String, EnumVariantKind)], 1203 has_lifetime: bool, 1204 is_open: bool, 1205 ) -> TokenStream { 1206 let ident = syn::Ident::new(type_name, proc_macro2::Span::call_site()); 1207 1208 if has_lifetime { 1209 let variant_conversions: Vec<_> = variant_info 1210 .iter() 1211 .map(|(variant_name, kind)| { 1212 let variant_ident = syn::Ident::new(variant_name, proc_macro2::Span::call_site()); 1213 match kind { 1214 EnumVariantKind::Unit => { 1215 quote! { 1216 #ident::#variant_ident => #ident::#variant_ident 1217 } 1218 } 1219 EnumVariantKind::Tuple => { 1220 quote! { 1221 #ident::#variant_ident(v) => #ident::#variant_ident(v.into_static()) 1222 } 1223 } 1224 EnumVariantKind::Struct(fields) => { 1225 let field_idents: Vec<_> = fields 1226 .iter() 1227 .map(|f| make_ident(&f.to_snake_case())) 1228 .collect(); 1229 let field_conversions: Vec<_> = field_idents 1230 .iter() 1231 .map(|f| quote! { #f: #f.into_static() }) 1232 .collect(); 1233 quote! { 1234 #ident::#variant_ident { #(#field_idents,)* } => #ident::#variant_ident { 1235 #(#field_conversions,)* 1236 } 1237 } 1238 } 1239 } 1240 }) 1241 .collect(); 1242 1243 let unknown_conversion = if is_open { 1244 quote! { 1245 #ident::Unknown(v) => #ident::Unknown(v.into_static()), 1246 } 1247 } else { 1248 quote! {} 1249 }; 1250 1251 quote! { 1252 impl jacquard_common::IntoStatic for #ident<'_> { 1253 type Output = #ident<'static>; 1254 1255 fn into_static(self) -> Self::Output { 1256 match self { 1257 #(#variant_conversions,)* 1258 #unknown_conversion 1259 } 1260 } 1261 } 1262 } 1263 } else { 1264 quote! { 1265 impl jacquard_common::IntoStatic for #ident { 1266 type Output = #ident; 1267 1268 fn into_static(self) -> Self::Output { 1269 self 1270 } 1271 } 1272 } 1273 } 1274 } 1275}