A better Rust ATProto crate
at main 1280 lines 47 kB view raw
1use crate::error::Result; 2use crate::lexicon::{ 3 LexXrpcBody, LexXrpcBodySchema, LexXrpcError, LexXrpcProcedure, LexXrpcQuery, 4 LexXrpcSubscription, LexXrpcSubscriptionMessageSchema, 5}; 6use heck::{ToPascalCase, ToSnakeCase}; 7use proc_macro2::TokenStream; 8use quote::quote; 9 10use super::CodeGenerator; 11use super::utils::make_ident; 12 13impl<'c> CodeGenerator<'c> { 14 /// Generate query type 15 pub(super) fn generate_query( 16 &self, 17 nsid: &str, 18 def_name: &str, 19 query: &LexXrpcQuery<'static>, 20 ) -> Result<TokenStream> { 21 let type_base = self.def_to_type_name(nsid, def_name); 22 let mut output = Vec::new(); 23 24 let params_has_lifetime = query 25 .parameters 26 .as_ref() 27 .map(|p| match p { 28 crate::lexicon::LexXrpcQueryParameter::Params(params) => { 29 self.params_need_lifetime(params) 30 } 31 }) 32 .unwrap_or(false); 33 let has_params = query.parameters.is_some(); 34 let has_output = query.output.is_some(); 35 let has_errors = query.errors.is_some(); 36 37 if let Some(params) = &query.parameters { 38 let params_struct = self.generate_params_struct(&type_base, nsid, params)?; 39 output.push(params_struct); 40 } 41 42 if let Some(body) = &query.output { 43 let output_struct = self.generate_output_struct(nsid, &type_base, body)?; 44 output.push(output_struct); 45 } 46 47 if let Some(errors) = &query.errors { 48 let error_enum = self.generate_error_enum(&type_base, errors)?; 49 output.push(error_enum); 50 } 51 52 // Generate XrpcRequest impl 53 let output_encoding = query 54 .output 55 .as_ref() 56 .map(|o| o.encoding.as_ref()) 57 .unwrap_or("application/json"); 58 let output_has_schema = query 59 .output 60 .as_ref() 61 .map(|o| o.schema.is_some()) 62 .unwrap_or(false); 63 64 let xrpc_impl = self.generate_xrpc_request_impl( 65 nsid, 66 &type_base, 67 quote! { jacquard_common::xrpc::XrpcMethod::Query }, 68 output_encoding, 69 has_params, 70 params_has_lifetime, 71 has_output, 72 output_has_schema, 73 has_errors, 74 false, // queries never have binary inputs 75 )?; 76 output.push(xrpc_impl); 77 78 Ok(quote! { 79 #(#output)* 80 }) 81 } 82 83 /// Generate procedure type 84 pub(super) fn generate_procedure( 85 &self, 86 nsid: &str, 87 def_name: &str, 88 proc: &LexXrpcProcedure<'static>, 89 ) -> Result<TokenStream> { 90 let type_base = self.def_to_type_name(nsid, def_name); 91 let mut output = Vec::new(); 92 93 // Check if input is a binary body (no schema) 94 let is_binary_input = proc 95 .input 96 .as_ref() 97 .map(|i| i.schema.is_none()) 98 .unwrap_or(false); 99 100 // Input bodies with schemas have lifetimes (they get #[lexicon] attribute) 101 // Binary inputs don't have lifetimes 102 let params_has_lifetime = proc.input.is_some() && !is_binary_input; 103 let has_input = proc.input.is_some(); 104 let has_output = proc.output.is_some(); 105 let has_errors = proc.errors.is_some(); 106 107 if let Some(params) = &proc.parameters { 108 let params_struct = self.generate_params_struct_proc(&type_base, nsid, params)?; 109 output.push(params_struct); 110 } 111 112 if let Some(body) = &proc.input { 113 let input_struct = self.generate_input_struct(nsid, &type_base, body)?; 114 output.push(input_struct); 115 } 116 117 if let Some(body) = &proc.output { 118 let output_struct = self.generate_output_struct(nsid, &type_base, body)?; 119 output.push(output_struct); 120 } 121 122 if let Some(errors) = &proc.errors { 123 let error_enum = self.generate_error_enum(&type_base, errors)?; 124 output.push(error_enum); 125 } 126 127 // Generate XrpcRequest impl 128 let input_encoding = proc 129 .input 130 .as_ref() 131 .map(|i| i.encoding.as_ref()) 132 .unwrap_or("application/json"); 133 let output_encoding = proc 134 .output 135 .as_ref() 136 .map(|o| o.encoding.as_ref()) 137 .unwrap_or("application/json"); 138 let output_has_schema = proc 139 .output 140 .as_ref() 141 .map(|o| o.schema.is_some()) 142 .unwrap_or(false); 143 let xrpc_impl = self.generate_xrpc_request_impl( 144 nsid, 145 &type_base, 146 quote! { jacquard_common::xrpc::XrpcMethod::Procedure(#input_encoding) }, 147 output_encoding, 148 has_input, 149 params_has_lifetime, 150 has_output, 151 output_has_schema, 152 has_errors, 153 is_binary_input, 154 )?; 155 output.push(xrpc_impl); 156 157 Ok(quote! { 158 #(#output)* 159 }) 160 } 161 162 pub(super) fn generate_subscription( 163 &self, 164 nsid: &str, 165 def_name: &str, 166 sub: &LexXrpcSubscription<'static>, 167 ) -> Result<TokenStream> { 168 let type_base = self.def_to_type_name(nsid, def_name); 169 let mut output = Vec::new(); 170 171 if let Some(params) = &sub.parameters { 172 // Extract LexXrpcParameters from the enum 173 match params { 174 crate::lexicon::LexXrpcSubscriptionParameter::Params(params_inner) => { 175 let params_struct = 176 self.generate_params_struct_inner(&type_base, nsid, params_inner)?; 177 output.push(params_struct); 178 } 179 } 180 } 181 182 if let Some(message) = &sub.message { 183 if let Some(schema) = &message.schema { 184 let message_type = self.generate_subscription_message(nsid, &type_base, schema)?; 185 output.push(message_type); 186 } 187 } 188 189 if let Some(errors) = &sub.errors { 190 let error_enum = self.generate_error_enum(&type_base, errors)?; 191 output.push(error_enum); 192 } 193 194 // Generate XrpcSubscription trait impl 195 let params_has_lifetime = sub 196 .parameters 197 .as_ref() 198 .map(|p| match p { 199 crate::lexicon::LexXrpcSubscriptionParameter::Params(params) => { 200 self.params_need_lifetime(params) 201 } 202 }) 203 .unwrap_or(false); 204 205 let has_params = sub.parameters.is_some(); 206 let has_message = sub.message.is_some(); 207 let has_errors = sub.errors.is_some(); 208 209 let subscription_impl = self.generate_xrpc_subscription_impl( 210 nsid, 211 &type_base, 212 has_params, 213 params_has_lifetime, 214 has_message, 215 has_errors, 216 )?; 217 output.push(subscription_impl); 218 219 Ok(quote! { 220 #(#output)* 221 }) 222 } 223 224 pub(super) fn generate_subscription_message( 225 &self, 226 nsid: &str, 227 type_base: &str, 228 schema: &LexXrpcSubscriptionMessageSchema<'static>, 229 ) -> Result<TokenStream> { 230 use crate::lexicon::LexXrpcSubscriptionMessageSchema; 231 232 match schema { 233 LexXrpcSubscriptionMessageSchema::Union(union) => { 234 // Generate a union enum for the message 235 let enum_name = format!("{}Message", type_base); 236 let enum_ident = syn::Ident::new(&enum_name, proc_macro2::Span::call_site()); 237 238 // Build variants using the union_codegen module (simple mode, no collision detection) 239 let ctx = super::union_codegen::UnionGenContext { 240 corpus: self.corpus, 241 namespace_deps: &self.namespace_deps, 242 current_nsid: nsid, 243 }; 244 245 let union_variants = ctx.build_simple_union_variants(&union.refs, |ref_str| { 246 self.ref_to_rust_type(ref_str) 247 })?; 248 let variants = super::union_codegen::generate_variant_tokens(&union_variants); 249 250 // Generate decode arms for framed decoding 251 let decode_arms: Vec<_> = union_variants 252 .iter() 253 .map(|variant| { 254 let ref_str_literal = &variant.ref_str; 255 let variant_ident = 256 syn::Ident::new(&variant.variant_name, proc_macro2::Span::call_site()); 257 quote! { 258 #ref_str_literal => { 259 let variant = serde_ipld_dagcbor::from_slice(body)?; 260 Ok(Self::#variant_ident(Box::new(variant))) 261 } 262 } 263 }) 264 .collect(); 265 266 let doc = self.generate_doc_comment(union.description.as_ref()); 267 268 // Generate decode_framed method for DAG-CBOR subscriptions 269 let decode_framed_impl = quote! { 270 impl<'a> #enum_ident<'a> { 271 /// Decode a framed DAG-CBOR message (header + body). 272 pub fn decode_framed<'de: 'a>(bytes: &'de [u8]) -> Result<#enum_ident<'a>, jacquard_common::error::DecodeError> { 273 let (header, body) = jacquard_common::xrpc::subscription::parse_event_header(bytes)?; 274 match header.t.as_str() { 275 #(#decode_arms)* 276 unknown => Err(jacquard_common::error::DecodeError::UnknownEventType( 277 unknown.into() 278 )), 279 } 280 } 281 } 282 }; 283 284 Ok(quote! { 285 #doc 286 #[jacquard_derive::open_union] 287 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic)] 288 #[serde(tag = "$type")] 289 #[serde(bound(deserialize = "'de: 'a"))] 290 pub enum #enum_ident<'a> { 291 #(#variants,)* 292 } 293 294 #decode_framed_impl 295 }) 296 } 297 LexXrpcSubscriptionMessageSchema::Object(obj) => { 298 // Generate a struct for the message 299 let struct_name = format!("{}Message", type_base); 300 let struct_ident = syn::Ident::new(&struct_name, proc_macro2::Span::call_site()); 301 302 let fields = self.generate_object_fields("", &struct_name, obj, false)?; 303 let doc = self.generate_doc_comment(obj.description.as_ref()); 304 305 // Subscription message structs always get a lifetime since they have the #[lexicon] attribute 306 // which adds extra_data: BTreeMap<..., Data<'a>> 307 let struct_def = quote! { 308 #doc 309 #[jacquard_derive::lexicon] 310 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic)] 311 #[serde(rename_all = "camelCase")] 312 pub struct #struct_ident<'a> { 313 #fields 314 } 315 }; 316 317 // Generate union types for this message 318 let unions = 319 self.generate_nested_types(nsid, &struct_name, &obj.properties, false)?; 320 321 Ok(quote! { 322 #struct_def 323 #(#unions)* 324 }) 325 } 326 LexXrpcSubscriptionMessageSchema::Ref(ref_type) => { 327 // Just a type alias to the referenced type 328 // Refs generally have lifetimes, so always add <'a> 329 let type_name = format!("{}Message", type_base); 330 let ident = syn::Ident::new(&type_name, proc_macro2::Span::call_site()); 331 let rust_type = self.ref_to_rust_type(&ref_type.r#ref)?; 332 let doc = self.generate_doc_comment(ref_type.description.as_ref()); 333 334 Ok(quote! { 335 #doc 336 pub type #ident<'a> = #rust_type; 337 }) 338 } 339 } 340 } 341 342 /// Generate params struct from XRPC query parameters 343 pub(super) fn generate_params_struct( 344 &self, 345 type_base: &str, 346 nsid: &str, 347 params: &crate::lexicon::LexXrpcQueryParameter<'static>, 348 ) -> Result<TokenStream> { 349 use crate::lexicon::LexXrpcQueryParameter; 350 match params { 351 LexXrpcQueryParameter::Params(p) => { 352 self.generate_params_struct_inner(type_base, nsid, p) 353 } 354 } 355 } 356 357 /// Generate params struct from XRPC procedure parameters (query string params) 358 pub(super) fn generate_params_struct_proc( 359 &self, 360 type_base: &str, 361 nsid: &str, 362 params: &crate::lexicon::LexXrpcProcedureParameter<'static>, 363 ) -> Result<TokenStream> { 364 use crate::lexicon::LexXrpcProcedureParameter; 365 match params { 366 // For procedures, query string params still get "Params" suffix since the main struct is the input 367 LexXrpcProcedureParameter::Params(p) => { 368 let struct_name = format!("{}Params", type_base); 369 let ident = syn::Ident::new(&struct_name, proc_macro2::Span::call_site()); 370 self.generate_params_struct_inner_with_name(&ident, nsid, p) 371 } 372 } 373 } 374 375 /// Generate params struct inner (shared implementation) 376 pub(super) fn generate_params_struct_inner( 377 &self, 378 type_base: &str, 379 nsid: &str, 380 p: &crate::lexicon::LexXrpcParameters<'static>, 381 ) -> Result<TokenStream> { 382 let ident = syn::Ident::new(type_base, proc_macro2::Span::call_site()); 383 self.generate_params_struct_inner_with_name(&ident, nsid, p) 384 } 385 386 /// Generate params struct with custom name 387 pub(super) fn generate_params_struct_inner_with_name( 388 &self, 389 ident: &syn::Ident, 390 nsid: &str, 391 p: &crate::lexicon::LexXrpcParameters<'static>, 392 ) -> Result<TokenStream> { 393 let required = p.required.as_ref().map(|r| r.as_slice()).unwrap_or(&[]); 394 let mut fields = Vec::new(); 395 let mut default_fns = Vec::new(); 396 397 for (field_name, field_type) in &p.properties { 398 let is_required = required.contains(field_name); 399 let (field_tokens, default_fn) = 400 self.generate_param_field_with_default("", field_name, field_type, is_required)?; 401 fields.push(field_tokens); 402 if let Some(fn_def) = default_fn { 403 default_fns.push(fn_def); 404 } 405 } 406 407 let doc = self.generate_doc_comment(p.description.as_ref()); 408 let needs_lifetime = self.params_need_lifetime(p); 409 410 let derives = quote! { 411 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic)] 412 }; 413 414 let struct_body = if fields.is_empty() { 415 quote! { 416 pub struct #ident; 417 } 418 } else if needs_lifetime { 419 quote! { 420 pub struct #ident<'a> { 421 #(#fields)* 422 } 423 } 424 } else { 425 quote! { 426 pub struct #ident { 427 #(#fields)* 428 } 429 } 430 }; 431 432 let struct_def = if needs_lifetime { 433 quote! { 434 #doc 435 #derives 436 #[serde(rename_all = "camelCase")] 437 #struct_body 438 } 439 } else { 440 quote! { 441 #doc 442 #derives 443 #[serde(rename_all = "camelCase")] 444 #struct_body 445 } 446 }; 447 448 let type_name = ident.to_string(); 449 let ctx = super::builder_gen::BuilderGenContext::from_parameters( 450 self, 451 nsid, 452 &type_name, 453 p, 454 needs_lifetime, 455 ); 456 let builder = ctx.generate(); 457 458 Ok(quote! { 459 #(#default_fns)* 460 #struct_def 461 #builder 462 }) 463 } 464 465 /// Generate input struct from XRPC body 466 pub(super) fn generate_input_struct( 467 &self, 468 nsid: &str, 469 type_base: &str, 470 body: &LexXrpcBody<'static>, 471 ) -> Result<TokenStream> { 472 let ident = syn::Ident::new(type_base, proc_macro2::Span::call_site()); 473 474 // Check if this is a binary body (no schema, just raw bytes) 475 let is_binary_body = body.schema.is_none(); 476 477 // Determine if we should derive Default or generate custom builder 478 // Binary bodies skipped (single field), schema-based inputs use heuristics 479 let (has_default, has_builder) = if is_binary_body { 480 (false, false) // binary bodies don't get builder (single field) 481 } else if let Some(crate::lexicon::LexXrpcBodySchema::Object(obj)) = &body.schema { 482 let decision = super::builder_heuristics::should_generate_builder(type_base, obj); 483 (decision.has_default, decision.has_builder) 484 } else { 485 (false, false) 486 }; 487 488 let fields = if let Some(schema) = &body.schema { 489 self.generate_body_fields(nsid, type_base, schema, has_builder)? 490 } else { 491 // Binary body: just a bytes field 492 quote! { 493 pub body: bytes::Bytes, 494 } 495 }; 496 497 let doc = self.generate_doc_comment(body.description.as_ref()); 498 499 // Binary bodies don't need #[lexicon] attribute or lifetime 500 let struct_def = if is_binary_body { 501 quote! { 502 #doc 503 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic)] 504 #[serde(rename_all = "camelCase")] 505 pub struct #ident { 506 #fields 507 } 508 } 509 } else if has_default { 510 quote! { 511 #doc 512 #[jacquard_derive::lexicon] 513 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic, Default)] 514 #[serde(rename_all = "camelCase")] 515 pub struct #ident<'a> { 516 #fields 517 } 518 } 519 } else { 520 quote! { 521 #doc 522 #[jacquard_derive::lexicon] 523 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic)] 524 #[serde(rename_all = "camelCase")] 525 pub struct #ident<'a> { 526 #fields 527 } 528 } 529 }; 530 531 // Generate custom builder if needed (binary bodies skipped - single field) 532 let builder = if !is_binary_body && has_builder { 533 if let Some(crate::lexicon::LexXrpcBodySchema::Object(obj)) = &body.schema { 534 let ctx = super::builder_gen::BuilderGenContext::from_object( 535 self, nsid, type_base, obj, 536 true, // input structs always have lifetime (for #[lexicon]) 537 ); 538 ctx.generate() 539 } else { 540 quote! {} 541 } 542 } else { 543 quote! {} 544 }; 545 546 // Generate union types if schema is an Object 547 let unions = if let Some(crate::lexicon::LexXrpcBodySchema::Object(obj)) = &body.schema { 548 self.generate_nested_types(nsid, type_base, &obj.properties, false)? 549 } else { 550 Vec::new() 551 }; 552 553 Ok(quote! { 554 #struct_def 555 #builder 556 #(#unions)* 557 }) 558 } 559 560 /// Generate output struct from XRPC body 561 pub(super) fn generate_output_struct( 562 &self, 563 nsid: &str, 564 type_base: &str, 565 body: &LexXrpcBody<'static>, 566 ) -> Result<TokenStream> { 567 let struct_name = format!("{}Output", type_base); 568 let ident = syn::Ident::new(&struct_name, proc_macro2::Span::call_site()); 569 570 let fields = if let Some(schema) = &body.schema { 571 self.generate_body_fields(nsid, &struct_name, schema, false)? 572 } else { 573 quote! { 574 pub body: bytes::Bytes, 575 } 576 }; 577 578 let doc = self.generate_doc_comment(body.description.as_ref()); 579 580 // Determine if we should derive Default 581 // Check if schema is an Object and apply heuristics 582 let has_default = if let Some(crate::lexicon::LexXrpcBodySchema::Object(obj)) = &body.schema 583 { 584 super::builder_heuristics::should_generate_builder(&struct_name, obj).has_default 585 } else { 586 false 587 }; 588 589 // Output structs always get a lifetime since they have the #[lexicon] attribute 590 // which adds extra_data: BTreeMap<..., Data<'a>> 591 let struct_def = if has_default { 592 quote! { 593 #doc 594 #[jacquard_derive::lexicon] 595 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic, Default)] 596 #[serde(rename_all = "camelCase")] 597 pub struct #ident<'a> { 598 #fields 599 } 600 } 601 } else if body.schema.is_none() { 602 quote! { 603 #doc 604 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic)] 605 #[serde(rename_all = "camelCase")] 606 pub struct #ident { 607 #fields 608 } 609 } 610 } else { 611 quote! { 612 #doc 613 #[jacquard_derive::lexicon] 614 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic)] 615 #[serde(rename_all = "camelCase")] 616 pub struct #ident<'a> { 617 #fields 618 } 619 } 620 }; 621 622 // Generate union types if schema is an Object 623 let unions = if let Some(crate::lexicon::LexXrpcBodySchema::Object(obj)) = &body.schema { 624 self.generate_nested_types(nsid, &struct_name, &obj.properties, false)? 625 } else { 626 Vec::new() 627 }; 628 629 Ok(quote! { 630 #struct_def 631 #(#unions)* 632 }) 633 } 634 635 /// Generate fields from XRPC body schema 636 pub(super) fn generate_body_fields( 637 &self, 638 nsid: &str, 639 parent_type_name: &str, 640 schema: &LexXrpcBodySchema<'static>, 641 is_builder: bool, 642 ) -> Result<TokenStream> { 643 use crate::lexicon::LexXrpcBodySchema; 644 645 match schema { 646 LexXrpcBodySchema::Object(obj) => { 647 self.generate_object_fields(nsid, parent_type_name, obj, is_builder) 648 } 649 LexXrpcBodySchema::Ref(ref_type) => { 650 let rust_type = self.ref_to_rust_type(&ref_type.r#ref)?; 651 Ok(quote! { 652 #[serde(flatten)] 653 #[serde(borrow)] 654 pub value: #rust_type, 655 }) 656 } 657 LexXrpcBodySchema::Union(_union) => { 658 let rust_type = quote! { jacquard_common::types::value::Data<'a> }; 659 Ok(quote! { 660 #[serde(flatten)] 661 #[serde(borrow)] 662 pub value: #rust_type, 663 }) 664 } 665 } 666 } 667 668 /// Generate a field for XRPC parameters 669 pub(super) fn generate_param_field( 670 &self, 671 _nsid: &str, 672 field_name: &str, 673 field_type: &crate::lexicon::LexXrpcParametersProperty<'static>, 674 is_required: bool, 675 ) -> Result<TokenStream> { 676 use crate::lexicon::LexXrpcParametersProperty; 677 678 let field_ident = make_ident(&field_name.to_snake_case()); 679 680 let (rust_type, needs_lifetime) = match field_type { 681 LexXrpcParametersProperty::Boolean(_) => (quote! { bool }, false), 682 LexXrpcParametersProperty::Integer(_) => (quote! { i64 }, false), 683 LexXrpcParametersProperty::String(s) => { 684 (self.string_to_rust_type(s), self.string_needs_lifetime(s)) 685 } 686 LexXrpcParametersProperty::Unknown(_) => { 687 (quote! { jacquard_common::types::value::Data<'a> }, true) 688 } 689 LexXrpcParametersProperty::Array(arr) => { 690 let needs_lifetime = match &arr.items { 691 crate::lexicon::LexPrimitiveArrayItem::Boolean(_) 692 | crate::lexicon::LexPrimitiveArrayItem::Integer(_) => false, 693 crate::lexicon::LexPrimitiveArrayItem::String(s) => { 694 self.string_needs_lifetime(s) 695 } 696 crate::lexicon::LexPrimitiveArrayItem::Unknown(_) => true, 697 }; 698 let item_type = match &arr.items { 699 crate::lexicon::LexPrimitiveArrayItem::Boolean(_) => quote! { bool }, 700 crate::lexicon::LexPrimitiveArrayItem::Integer(_) => quote! { i64 }, 701 crate::lexicon::LexPrimitiveArrayItem::String(s) => self.string_to_rust_type(s), 702 crate::lexicon::LexPrimitiveArrayItem::Unknown(_) => { 703 quote! { jacquard_common::types::value::Data<'a> } 704 } 705 }; 706 (quote! { Vec<#item_type> }, needs_lifetime) 707 } 708 }; 709 710 let rust_type = if is_required { 711 rust_type 712 } else { 713 quote! { std::option::Option<#rust_type> } 714 }; 715 716 let mut attrs = Vec::new(); 717 718 if !is_required { 719 attrs.push(quote! { #[serde(skip_serializing_if = "std::option::Option::is_none")] }); 720 } 721 722 // Add serde(borrow) to all fields with lifetimes 723 if needs_lifetime { 724 attrs.push(quote! { #[serde(borrow)] }); 725 } 726 727 Ok(quote! { 728 #(#attrs)* 729 pub #field_ident: #rust_type, 730 }) 731 } 732 733 /// Generate param field with serde default if present 734 /// Returns (field_tokens, optional_default_function) 735 pub(super) fn generate_param_field_with_default( 736 &self, 737 nsid: &str, 738 field_name: &str, 739 field_type: &crate::lexicon::LexXrpcParametersProperty<'static>, 740 is_required: bool, 741 ) -> Result<(TokenStream, Option<TokenStream>)> { 742 use crate::lexicon::LexXrpcParametersProperty; 743 use heck::ToSnakeCase; 744 745 // Get base field 746 let base_field = self.generate_param_field(nsid, field_name, field_type, is_required)?; 747 748 // Generate default function and attribute for required fields with defaults 749 // For optional fields, just add doc comments 750 let (doc_comment, serde_attr, default_fn) = if is_required { 751 match field_type { 752 LexXrpcParametersProperty::Boolean(b) if b.default.is_some() => { 753 let v = b.default.unwrap(); 754 let fn_name = format!("_default_{}", field_name.to_snake_case()); 755 let fn_ident = syn::Ident::new(&fn_name, proc_macro2::Span::call_site()); 756 ( 757 Some(format!(" Defaults to `{}`", v)), 758 Some(quote! { #[serde(default = #fn_name)] }), 759 Some(quote! { 760 fn #fn_ident() -> bool { #v } 761 }), 762 ) 763 } 764 LexXrpcParametersProperty::Integer(i) if i.default.is_some() => { 765 let v = i.default.unwrap(); 766 let fn_name = format!("_default_{}", field_name.to_snake_case()); 767 let fn_ident = syn::Ident::new(&fn_name, proc_macro2::Span::call_site()); 768 ( 769 Some(format!(" Defaults to `{}`", v)), 770 Some(quote! { #[serde(default = #fn_name)] }), 771 Some(quote! { 772 fn #fn_ident() -> i64 { #v } 773 }), 774 ) 775 } 776 LexXrpcParametersProperty::String(s) if s.default.is_some() => { 777 let v = s.default.as_ref().unwrap().as_ref(); 778 let fn_name = format!("_default_{}", field_name.to_snake_case()); 779 let fn_ident = syn::Ident::new(&fn_name, proc_macro2::Span::call_site()); 780 ( 781 Some(format!(" Defaults to `\"{}\"`", v)), 782 Some(quote! { #[serde(default = #fn_name)] }), 783 Some(quote! { 784 fn #fn_ident() -> jacquard_common::CowStr<'static> { 785 jacquard_common::CowStr::from(#v) 786 } 787 }), 788 ) 789 } 790 _ => (None, None, None), 791 } 792 } else { 793 // Optional fields - just doc comments, no serde defaults 794 let doc = match field_type { 795 LexXrpcParametersProperty::Integer(i) => { 796 let mut parts = Vec::new(); 797 if let Some(def) = i.default { 798 parts.push(format!("default: {}", def)); 799 } 800 if let Some(min) = i.minimum { 801 parts.push(format!("min: {}", min)); 802 } 803 if let Some(max) = i.maximum { 804 parts.push(format!("max: {}", max)); 805 } 806 if !parts.is_empty() { 807 Some(format!("({})", parts.join(", "))) 808 } else { 809 None 810 } 811 } 812 LexXrpcParametersProperty::String(s) => { 813 let mut parts = Vec::new(); 814 if let Some(def) = s.default.as_ref() { 815 parts.push(format!("default: \"{}\"", def.as_ref())); 816 } 817 if let Some(min) = s.min_length { 818 parts.push(format!("min length: {}", min)); 819 } 820 if let Some(max) = s.max_length { 821 parts.push(format!("max length: {}", max)); 822 } 823 if !parts.is_empty() { 824 Some(format!("({})", parts.join(", "))) 825 } else { 826 None 827 } 828 } 829 LexXrpcParametersProperty::Boolean(b) => { 830 b.default.map(|v| format!(" (default: {})", v)) 831 } 832 _ => None, 833 }; 834 (doc, None, None) 835 }; 836 837 let doc = doc_comment.as_ref().map(|d| quote! { #[doc = #d] }); 838 let field_with_attrs = match (doc, serde_attr) { 839 (Some(doc), Some(attr)) => quote! { 840 #doc 841 #attr 842 #base_field 843 }, 844 (Some(doc), None) => quote! { 845 #doc 846 #base_field 847 }, 848 (None, Some(attr)) => quote! { 849 #attr 850 #base_field 851 }, 852 (None, None) => base_field, 853 }; 854 855 Ok((field_with_attrs, default_fn)) 856 } 857 858 /// Generate error enum from XRPC errors 859 pub(super) fn generate_error_enum( 860 &self, 861 type_base: &str, 862 errors: &[LexXrpcError<'static>], 863 ) -> Result<TokenStream> { 864 let enum_name = format!("{}Error", type_base); 865 let ident = syn::Ident::new(&enum_name, proc_macro2::Span::call_site()); 866 867 let mut variants = Vec::new(); 868 let mut display_arms = Vec::new(); 869 870 for error in errors { 871 let variant_name = error.name.to_pascal_case(); 872 let variant_ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site()); 873 874 let error_name = error.name.as_ref(); 875 let doc = self.generate_doc_comment(error.description.as_ref()); 876 877 variants.push(quote! { 878 #doc 879 #[serde(rename = #error_name)] 880 #variant_ident(std::option::Option<jacquard_common::CowStr<'a>>) 881 }); 882 883 display_arms.push(quote! { 884 Self::#variant_ident(msg) => { 885 write!(f, #error_name)?; 886 if let Some(msg) = msg { 887 write!(f, ": {}", msg)?; 888 } 889 Ok(()) 890 } 891 }); 892 } 893 894 // IntoStatic impl is generated by the derive macro now 895 896 Ok(quote! { 897 #[jacquard_derive::open_union] 898 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, thiserror::Error, miette::Diagnostic, jacquard_derive::IntoStatic)] 899 #[serde(tag = "error", content = "message")] 900 #[serde(bound(deserialize = "'de: 'a"))] 901 pub enum #ident<'a> { 902 #(#variants,)* 903 } 904 905 impl core::fmt::Display for #ident<'_> { 906 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 907 match self { 908 #(#display_arms)* 909 Self::Unknown(err) => write!(f, "Unknown error: {:?}", err), 910 } 911 } 912 } 913 }) 914 } 915 916 /// Generate XrpcRequest trait impl for a query or procedure 917 pub(super) fn generate_xrpc_request_impl( 918 &self, 919 nsid: &str, 920 type_base: &str, 921 method: TokenStream, 922 output_encoding: &str, 923 has_params: bool, 924 params_has_lifetime: bool, 925 has_output: bool, 926 output_has_schema: bool, 927 has_errors: bool, 928 is_binary_input: bool, 929 ) -> Result<TokenStream> { 930 let output_type = if has_output { 931 let output_ident = syn::Ident::new( 932 &format!("{}Output", type_base), 933 proc_macro2::Span::call_site(), 934 ); 935 // Only add lifetime if output has a schema (binary outputs without schema don't have lifetimes) 936 if output_has_schema { 937 quote! { 938 #output_ident<'de> 939 } 940 } else { 941 quote! { 942 #output_ident 943 } 944 } 945 } else { 946 quote! { () } 947 }; 948 949 let error_type = if has_errors { 950 let error_ident = syn::Ident::new( 951 &format!("{}Error", type_base), 952 proc_macro2::Span::call_site(), 953 ); 954 quote! { #error_ident<'de> } 955 } else { 956 quote! { jacquard_common::xrpc::GenericError<'de> } 957 }; 958 959 // Generate the response type that implements XrpcResp 960 let response_ident = syn::Ident::new( 961 &format!("{}Response", type_base), 962 proc_macro2::Span::call_site(), 963 ); 964 965 // Generate the endpoint type that implements XrpcEndpoint 966 let endpoint_ident = syn::Ident::new( 967 &format!("{}Request", type_base), 968 proc_macro2::Span::call_site(), 969 ); 970 971 let decode_output_method = if output_encoding == "application/json" { 972 quote! {} 973 } else { 974 let output_ident = syn::Ident::new( 975 &format!("{}Output", type_base), 976 proc_macro2::Span::call_site(), 977 ); 978 quote! { 979 980 fn decode_output<'de>(body: &'de [u8]) -> Result<Self::Output<'de>, jacquard_common::error::DecodeError> 981 where 982 Self::Output<'de>: serde::Deserialize<'de>, 983 { 984 Ok(#output_ident { 985 body: bytes::Bytes::copy_from_slice(body), 986 }) 987 } 988 } 989 }; 990 991 let encode_output_method = if output_encoding == "application/json" { 992 quote! {} 993 } else { 994 quote! { 995 fn encode_output(output: &Self::Output<'_>) -> Result<Vec<u8>, jacquard_common::xrpc::EncodeError> { 996 Ok(output.body.to_vec()) 997 } 998 } 999 }; 1000 1001 let response_type = quote! { 1002 #[doc = " Response type for "] 1003 #[doc = #nsid] 1004 pub struct #response_ident; 1005 1006 impl jacquard_common::xrpc::XrpcResp for #response_ident { 1007 const NSID: &'static str = #nsid; 1008 const ENCODING: &'static str = #output_encoding; 1009 type Output<'de> = #output_type; 1010 type Err<'de> = #error_type; 1011 1012 #encode_output_method 1013 #decode_output_method 1014 } 1015 }; 1016 1017 // Generate encode_body() method for binary inputs 1018 let encode_body_method = if is_binary_input { 1019 quote! { 1020 fn encode_body(&self) -> Result<Vec<u8>, jacquard_common::xrpc::EncodeError> { 1021 Ok(self.body.to_vec()) 1022 } 1023 } 1024 } else { 1025 quote! {} 1026 }; 1027 1028 // Generate decode_body() method for binary inputs 1029 let decode_body_method = if is_binary_input { 1030 quote! { 1031 fn decode_body<'de>( 1032 body: &'de [u8], 1033 ) -> Result<Box<Self>, jacquard_common::error::DecodeError> 1034 where 1035 Self: serde::Deserialize<'de>, 1036 { 1037 Ok(Box::new(Self { 1038 body: bytes::Bytes::copy_from_slice(body), 1039 })) 1040 } 1041 } 1042 } else { 1043 quote! {} 1044 }; 1045 1046 let endpoint_path = format!("/xrpc/{}", nsid); 1047 1048 if has_params { 1049 // Implement on the params/input struct itself 1050 let request_ident = syn::Ident::new(type_base, proc_macro2::Span::call_site()); 1051 1052 let (impl_generics, impl_target, endpoint_request_type) = if params_has_lifetime { 1053 ( 1054 quote! { <'a> }, 1055 quote! { #request_ident<'a> }, 1056 quote! { #request_ident<'de> }, 1057 ) 1058 } else { 1059 ( 1060 quote! {}, 1061 quote! { #request_ident }, 1062 quote! { #request_ident }, 1063 ) 1064 }; 1065 1066 Ok(quote! { 1067 #response_type 1068 1069 impl #impl_generics jacquard_common::xrpc::XrpcRequest for #impl_target { 1070 const NSID: &'static str = #nsid; 1071 const METHOD: jacquard_common::xrpc::XrpcMethod = #method; 1072 1073 type Response = #response_ident; 1074 1075 #encode_body_method 1076 #decode_body_method 1077 } 1078 1079 #[doc = " Endpoint type for "] 1080 #[doc = #nsid] 1081 pub struct #endpoint_ident; 1082 1083 impl jacquard_common::xrpc::XrpcEndpoint for #endpoint_ident { 1084 const PATH: &'static str = #endpoint_path; 1085 const METHOD: jacquard_common::xrpc::XrpcMethod = #method; 1086 1087 type Request<'de> = #endpoint_request_type; 1088 type Response = #response_ident; 1089 } 1090 }) 1091 } else { 1092 // No params - generate a marker struct 1093 let request_ident = syn::Ident::new(type_base, proc_macro2::Span::call_site()); 1094 1095 Ok(quote! { 1096 /// XRPC request marker type 1097 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, jacquard_derive::IntoStatic)] 1098 pub struct #request_ident; 1099 1100 #response_type 1101 1102 impl jacquard_common::xrpc::XrpcRequest for #request_ident { 1103 const NSID: &'static str = #nsid; 1104 const METHOD: jacquard_common::xrpc::XrpcMethod = #method; 1105 1106 type Response = #response_ident; 1107 } 1108 1109 #[doc = " Endpoint type for "] 1110 #[doc = #nsid] 1111 pub struct #endpoint_ident; 1112 1113 impl jacquard_common::xrpc::XrpcEndpoint for #endpoint_ident { 1114 const PATH: &'static str = #endpoint_path; 1115 const METHOD: jacquard_common::xrpc::XrpcMethod = #method; 1116 1117 type Request<'de> = #request_ident; 1118 type Response = #response_ident; 1119 } 1120 }) 1121 } 1122 } 1123 1124 /// Generate XrpcSubscription trait impl for a subscription endpoint 1125 pub(super) fn generate_xrpc_subscription_impl( 1126 &self, 1127 nsid: &str, 1128 type_base: &str, 1129 has_params: bool, 1130 params_has_lifetime: bool, 1131 has_message: bool, 1132 has_errors: bool, 1133 ) -> Result<TokenStream> { 1134 // Generate stream response marker struct 1135 let stream_ident = syn::Ident::new( 1136 &format!("{}Stream", type_base), 1137 proc_macro2::Span::call_site(), 1138 ); 1139 1140 let message_type = if has_message { 1141 let msg_ident = syn::Ident::new( 1142 &format!("{}Message", type_base), 1143 proc_macro2::Span::call_site(), 1144 ); 1145 quote! { #msg_ident<'de> } 1146 } else { 1147 quote! { () } 1148 }; 1149 1150 let error_type = if has_errors { 1151 let err_ident = syn::Ident::new( 1152 &format!("{}Error", type_base), 1153 proc_macro2::Span::call_site(), 1154 ); 1155 quote! { #err_ident<'de> } 1156 } else { 1157 quote! { jacquard_common::xrpc::GenericError<'de> } 1158 }; 1159 1160 // Determine encoding from nsid convention 1161 // ATProto subscriptions use DAG-CBOR, community ones might use JSON 1162 let is_dag_cbor = nsid.starts_with("com.atproto"); 1163 let encoding = if is_dag_cbor { 1164 quote! { jacquard_common::xrpc::MessageEncoding::DagCbor } 1165 } else { 1166 quote! { jacquard_common::xrpc::MessageEncoding::Json } 1167 }; 1168 1169 // Generate SubscriptionResp impl 1170 // For DAG-CBOR subscriptions, override decode_message to use framed decoding 1171 let decode_message_override = if is_dag_cbor && has_message { 1172 let msg_ident = syn::Ident::new( 1173 &format!("{}Message", type_base), 1174 proc_macro2::Span::call_site(), 1175 ); 1176 quote! { 1177 fn decode_message<'de>(bytes: &'de [u8]) -> Result<Self::Message<'de>, jacquard_common::error::DecodeError> { 1178 #msg_ident::decode_framed(bytes) 1179 } 1180 } 1181 } else { 1182 quote! {} 1183 }; 1184 1185 let stream_resp_impl = quote! { 1186 #[doc = "Stream response type for "] 1187 #[doc = #nsid] 1188 pub struct #stream_ident; 1189 1190 impl jacquard_common::xrpc::SubscriptionResp for #stream_ident { 1191 const NSID: &'static str = #nsid; 1192 const ENCODING: jacquard_common::xrpc::MessageEncoding = #encoding; 1193 1194 type Message<'de> = #message_type; 1195 type Error<'de> = #error_type; 1196 1197 #decode_message_override 1198 } 1199 }; 1200 1201 let params_ident = if has_params { 1202 syn::Ident::new(type_base, proc_macro2::Span::call_site()) 1203 } else { 1204 // Generate marker struct if no params 1205 let marker = syn::Ident::new(type_base, proc_macro2::Span::call_site()); 1206 let endpoint_ident = syn::Ident::new( 1207 &format!("{}Endpoint", type_base), 1208 proc_macro2::Span::call_site(), 1209 ); 1210 let endpoint_path = format!("/xrpc/{}", nsid); 1211 1212 return Ok(quote! { 1213 #stream_resp_impl 1214 1215 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)] 1216 pub struct #marker; 1217 1218 impl jacquard_common::xrpc::XrpcSubscription for #marker { 1219 const NSID: &'static str = #nsid; 1220 const ENCODING: jacquard_common::xrpc::MessageEncoding = #encoding; 1221 1222 type Stream = #stream_ident; 1223 } 1224 1225 pub struct #endpoint_ident; 1226 1227 impl jacquard_common::xrpc::SubscriptionEndpoint for #endpoint_ident { 1228 const PATH: &'static str = #endpoint_path; 1229 const ENCODING: jacquard_common::xrpc::MessageEncoding = #encoding; 1230 1231 type Params<'de> = #marker; 1232 type Stream = #stream_ident; 1233 } 1234 }); 1235 }; 1236 1237 let (impl_generics, impl_target, endpoint_params_type) = 1238 if has_params && params_has_lifetime { 1239 ( 1240 quote! { <'a> }, 1241 quote! { #params_ident<'a> }, 1242 quote! { #params_ident<'de> }, 1243 ) 1244 } else { 1245 ( 1246 quote! {}, 1247 quote! { #params_ident }, 1248 quote! { #params_ident }, 1249 ) 1250 }; 1251 1252 let endpoint_ident = syn::Ident::new( 1253 &format!("{}Endpoint", type_base), 1254 proc_macro2::Span::call_site(), 1255 ); 1256 1257 let endpoint_path = format!("/xrpc/{}", nsid); 1258 1259 Ok(quote! { 1260 #stream_resp_impl 1261 1262 impl #impl_generics jacquard_common::xrpc::XrpcSubscription for #impl_target { 1263 const NSID: &'static str = #nsid; 1264 const ENCODING: jacquard_common::xrpc::MessageEncoding = #encoding; 1265 1266 type Stream = #stream_ident; 1267 } 1268 1269 pub struct #endpoint_ident; 1270 1271 impl jacquard_common::xrpc::SubscriptionEndpoint for #endpoint_ident { 1272 const PATH: &'static str = #endpoint_path; 1273 const ENCODING: jacquard_common::xrpc::MessageEncoding = #encoding; 1274 1275 type Params<'de> = #endpoint_params_type; 1276 type Stream = #stream_ident; 1277 } 1278 }) 1279 } 1280}