A better Rust ATProto crate
at main 42 kB view raw
1use crate::error::Result; 2use crate::lexicon::{ 3 LexArrayItem, LexObjectProperty, LexXrpcBody, LexXrpcBodySchema, LexXrpcError, 4 LexXrpcProcedure, LexXrpcQuery, LexXrpcSubscription, LexXrpcSubscriptionMessageSchema, 5}; 6use heck::{ToPascalCase, ToSnakeCase}; 7use proc_macro2::TokenStream; 8use quote::quote; 9 10use super::utils::make_ident; 11use super::CodeGenerator; 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, 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 xrpc_impl = self.generate_xrpc_request_impl( 59 nsid, 60 &type_base, 61 quote! { jacquard_common::xrpc::XrpcMethod::Query }, 62 output_encoding, 63 has_params, 64 params_has_lifetime, 65 has_output, 66 has_errors, 67 false, // queries never have binary inputs 68 )?; 69 output.push(xrpc_impl); 70 71 Ok(quote! { 72 #(#output)* 73 }) 74 } 75 76 /// Generate procedure type 77 pub(super) fn generate_procedure( 78 &self, 79 nsid: &str, 80 def_name: &str, 81 proc: &LexXrpcProcedure<'static>, 82 ) -> Result<TokenStream> { 83 let type_base = self.def_to_type_name(nsid, def_name); 84 let mut output = Vec::new(); 85 86 // Check if input is a binary body (no schema) 87 let is_binary_input = proc 88 .input 89 .as_ref() 90 .map(|i| i.schema.is_none()) 91 .unwrap_or(false); 92 93 // Input bodies with schemas have lifetimes (they get #[lexicon] attribute) 94 // Binary inputs don't have lifetimes 95 let params_has_lifetime = proc.input.is_some() && !is_binary_input; 96 let has_input = proc.input.is_some(); 97 let has_output = proc.output.is_some(); 98 let has_errors = proc.errors.is_some(); 99 100 if let Some(params) = &proc.parameters { 101 let params_struct = self.generate_params_struct_proc(&type_base, params)?; 102 output.push(params_struct); 103 } 104 105 if let Some(body) = &proc.input { 106 let input_struct = self.generate_input_struct(nsid, &type_base, body)?; 107 output.push(input_struct); 108 } 109 110 if let Some(body) = &proc.output { 111 let output_struct = self.generate_output_struct(nsid, &type_base, body)?; 112 output.push(output_struct); 113 } 114 115 if let Some(errors) = &proc.errors { 116 let error_enum = self.generate_error_enum(&type_base, errors)?; 117 output.push(error_enum); 118 } 119 120 // Generate XrpcRequest impl 121 let input_encoding = proc 122 .input 123 .as_ref() 124 .map(|i| i.encoding.as_ref()) 125 .unwrap_or("application/json"); 126 let output_encoding = proc 127 .output 128 .as_ref() 129 .map(|o| o.encoding.as_ref()) 130 .unwrap_or("application/json"); 131 let xrpc_impl = self.generate_xrpc_request_impl( 132 nsid, 133 &type_base, 134 quote! { jacquard_common::xrpc::XrpcMethod::Procedure(#input_encoding) }, 135 output_encoding, 136 has_input, 137 params_has_lifetime, 138 has_output, 139 has_errors, 140 is_binary_input, 141 )?; 142 output.push(xrpc_impl); 143 144 Ok(quote! { 145 #(#output)* 146 }) 147 } 148 149 pub(super) fn generate_subscription( 150 &self, 151 nsid: &str, 152 def_name: &str, 153 sub: &LexXrpcSubscription<'static>, 154 ) -> Result<TokenStream> { 155 let type_base = self.def_to_type_name(nsid, def_name); 156 let mut output = Vec::new(); 157 158 if let Some(params) = &sub.parameters { 159 // Extract LexXrpcParameters from the enum 160 match params { 161 crate::lexicon::LexXrpcSubscriptionParameter::Params(params_inner) => { 162 let params_struct = 163 self.generate_params_struct_inner(&type_base, params_inner)?; 164 output.push(params_struct); 165 } 166 } 167 } 168 169 if let Some(message) = &sub.message { 170 if let Some(schema) = &message.schema { 171 let message_type = self.generate_subscription_message(nsid, &type_base, schema)?; 172 output.push(message_type); 173 } 174 } 175 176 if let Some(errors) = &sub.errors { 177 let error_enum = self.generate_error_enum(&type_base, errors)?; 178 output.push(error_enum); 179 } 180 181 Ok(quote! { 182 #(#output)* 183 }) 184 } 185 186 pub(super) fn generate_subscription_message( 187 &self, 188 nsid: &str, 189 type_base: &str, 190 schema: &LexXrpcSubscriptionMessageSchema<'static>, 191 ) -> Result<TokenStream> { 192 use crate::lexicon::LexXrpcSubscriptionMessageSchema; 193 194 match schema { 195 LexXrpcSubscriptionMessageSchema::Union(union) => { 196 // Generate a union enum for the message 197 let enum_name = format!("{}Message", type_base); 198 let enum_ident = syn::Ident::new(&enum_name, proc_macro2::Span::call_site()); 199 200 let mut variants = Vec::new(); 201 for ref_str in &union.refs { 202 let ref_str_s = ref_str.as_ref(); 203 // Parse ref to get NSID and def name 204 let (ref_nsid, ref_def) = 205 if let Some((nsid, fragment)) = ref_str.split_once('#') { 206 (nsid, fragment) 207 } else { 208 (ref_str.as_ref(), "main") 209 }; 210 211 let variant_name = if ref_def == "main" { 212 ref_nsid.split('.').last().unwrap().to_pascal_case() 213 } else { 214 ref_def.to_pascal_case() 215 }; 216 let variant_ident = 217 syn::Ident::new(&variant_name, proc_macro2::Span::call_site()); 218 let type_path = self.ref_to_rust_type(ref_str)?; 219 220 variants.push(quote! { 221 #[serde(rename = #ref_str_s)] 222 #variant_ident(Box<#type_path>) 223 }); 224 } 225 226 let doc = self.generate_doc_comment(union.description.as_ref()); 227 228 Ok(quote! { 229 #doc 230 #[jacquard_derive::open_union] 231 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic)] 232 #[serde(tag = "$type")] 233 #[serde(bound(deserialize = "'de: 'a"))] 234 pub enum #enum_ident<'a> { 235 #(#variants,)* 236 } 237 }) 238 } 239 LexXrpcSubscriptionMessageSchema::Object(obj) => { 240 // Generate a struct for the message 241 let struct_name = format!("{}Message", type_base); 242 let struct_ident = syn::Ident::new(&struct_name, proc_macro2::Span::call_site()); 243 244 let fields = self.generate_object_fields("", &struct_name, obj, false)?; 245 let doc = self.generate_doc_comment(obj.description.as_ref()); 246 247 // Subscription message structs always get a lifetime since they have the #[lexicon] attribute 248 // which adds extra_data: BTreeMap<..., Data<'a>> 249 let struct_def = quote! { 250 #doc 251 #[jacquard_derive::lexicon] 252 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic)] 253 #[serde(rename_all = "camelCase")] 254 pub struct #struct_ident<'a> { 255 #fields 256 } 257 }; 258 259 // Generate union types for this message 260 let mut unions = Vec::new(); 261 for (field_name, field_type) in &obj.properties { 262 match field_type { 263 LexObjectProperty::Union(union) => { 264 // Skip empty, single-variant unions unless they're self-referential 265 if !union.refs.is_empty() && (union.refs.len() > 1 || self.is_self_referential_union(nsid, &struct_name, union)) { 266 let union_name = self.generate_field_type_name(nsid, &struct_name, field_name, ""); 267 let refs: Vec<_> = union.refs.iter().cloned().collect(); 268 let union_def = 269 self.generate_union(nsid, &union_name, &refs, None, union.closed)?; 270 unions.push(union_def); 271 } 272 } 273 LexObjectProperty::Array(array) => { 274 if let LexArrayItem::Union(union) = &array.items { 275 // Skip single-variant array unions 276 if union.refs.len() > 1 { 277 let union_name = self.generate_field_type_name(nsid, &struct_name, field_name, "Item"); 278 let refs: Vec<_> = union.refs.iter().cloned().collect(); 279 let union_def = self.generate_union(nsid, &union_name, &refs, None, union.closed)?; 280 unions.push(union_def); 281 } 282 } 283 } 284 _ => {} 285 } 286 } 287 288 Ok(quote! { 289 #struct_def 290 #(#unions)* 291 }) 292 } 293 LexXrpcSubscriptionMessageSchema::Ref(ref_type) => { 294 // Just a type alias to the referenced type 295 // Refs generally have lifetimes, so always add <'a> 296 let type_name = format!("{}Message", type_base); 297 let ident = syn::Ident::new(&type_name, proc_macro2::Span::call_site()); 298 let rust_type = self.ref_to_rust_type(&ref_type.r#ref)?; 299 let doc = self.generate_doc_comment(ref_type.description.as_ref()); 300 301 Ok(quote! { 302 #doc 303 pub type #ident<'a> = #rust_type; 304 }) 305 } 306 } 307 } 308 309 /// Generate params struct from XRPC query parameters 310 pub(super) fn generate_params_struct( 311 &self, 312 type_base: &str, 313 params: &crate::lexicon::LexXrpcQueryParameter<'static>, 314 ) -> Result<TokenStream> { 315 use crate::lexicon::LexXrpcQueryParameter; 316 match params { 317 LexXrpcQueryParameter::Params(p) => self.generate_params_struct_inner(type_base, p), 318 } 319 } 320 321 /// Generate params struct from XRPC procedure parameters (query string params) 322 pub(super) fn generate_params_struct_proc( 323 &self, 324 type_base: &str, 325 params: &crate::lexicon::LexXrpcProcedureParameter<'static>, 326 ) -> Result<TokenStream> { 327 use crate::lexicon::LexXrpcProcedureParameter; 328 match params { 329 // For procedures, query string params still get "Params" suffix since the main struct is the input 330 LexXrpcProcedureParameter::Params(p) => { 331 let struct_name = format!("{}Params", type_base); 332 let ident = syn::Ident::new(&struct_name, proc_macro2::Span::call_site()); 333 self.generate_params_struct_inner_with_name(&ident, p) 334 } 335 } 336 } 337 338 /// Generate params struct inner (shared implementation) 339 pub(super) fn generate_params_struct_inner( 340 &self, 341 type_base: &str, 342 p: &crate::lexicon::LexXrpcParameters<'static>, 343 ) -> Result<TokenStream> { 344 let ident = syn::Ident::new(type_base, proc_macro2::Span::call_site()); 345 self.generate_params_struct_inner_with_name(&ident, p) 346 } 347 348 /// Generate params struct with custom name 349 pub(super) fn generate_params_struct_inner_with_name( 350 &self, 351 ident: &syn::Ident, 352 p: &crate::lexicon::LexXrpcParameters<'static>, 353 ) -> Result<TokenStream> { 354 let required = p.required.as_ref().map(|r| r.as_slice()).unwrap_or(&[]); 355 let mut fields = Vec::new(); 356 let mut default_fns = Vec::new(); 357 358 for (field_name, field_type) in &p.properties { 359 let is_required = required.contains(field_name); 360 let (field_tokens, default_fn) = 361 self.generate_param_field_with_default("", field_name, field_type, is_required)?; 362 fields.push(field_tokens); 363 if let Some(fn_def) = default_fn { 364 default_fns.push(fn_def); 365 } 366 } 367 368 let doc = self.generate_doc_comment(p.description.as_ref()); 369 let needs_lifetime = self.params_need_lifetime(p); 370 371 let derives = quote! { 372 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, bon::Builder, jacquard_derive::IntoStatic)] 373 #[builder(start_fn = new)] 374 }; 375 376 if needs_lifetime { 377 Ok(quote! { 378 #(#default_fns)* 379 380 #doc 381 #derives 382 #[serde(rename_all = "camelCase")] 383 pub struct #ident<'a> { 384 #(#fields)* 385 } 386 }) 387 } else { 388 Ok(quote! { 389 #(#default_fns)* 390 391 #doc 392 #derives 393 #[serde(rename_all = "camelCase")] 394 pub struct #ident { 395 #(#fields)* 396 } 397 }) 398 } 399 } 400 401 /// Generate input struct from XRPC body 402 pub(super) fn generate_input_struct( 403 &self, 404 nsid: &str, 405 type_base: &str, 406 body: &LexXrpcBody<'static>, 407 ) -> Result<TokenStream> { 408 let ident = syn::Ident::new(type_base, proc_macro2::Span::call_site()); 409 410 // Check if this is a binary body (no schema, just raw bytes) 411 let is_binary_body = body.schema.is_none(); 412 413 // Determine if we should derive Default or bon::Builder 414 // Binary bodies always get builder, schema-based inputs use heuristics 415 let (has_default, has_builder) = if is_binary_body { 416 (false, true) 417 } else if let Some(crate::lexicon::LexXrpcBodySchema::Object(obj)) = &body.schema { 418 use crate::codegen::structs::{count_required_fields, all_required_are_defaultable_strings, conflicts_with_builder_macro}; 419 let required_count = count_required_fields(obj); 420 let can_default = required_count == 0 || all_required_are_defaultable_strings(obj); 421 let can_builder = required_count >= 1 && !can_default && !conflicts_with_builder_macro(type_base); 422 (can_default, can_builder) 423 } else { 424 (false, false) 425 }; 426 427 let fields = if let Some(schema) = &body.schema { 428 self.generate_body_fields("", type_base, schema, has_builder)? 429 } else { 430 // Binary body: just a bytes field 431 quote! { 432 pub body: bytes::Bytes, 433 } 434 }; 435 436 let doc = self.generate_doc_comment(body.description.as_ref()); 437 438 // Binary bodies don't need #[lexicon] attribute or lifetime 439 let struct_def = if is_binary_body { 440 quote! { 441 #doc 442 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, bon::Builder, jacquard_derive::IntoStatic)] 443 #[builder(start_fn = new)] 444 #[serde(rename_all = "camelCase")] 445 pub struct #ident { 446 #fields 447 } 448 } 449 } else if has_builder { 450 // Input structs with schemas and builders: manually add extra_data field with #[builder(default)] 451 // for bon compatibility. The #[lexicon] macro will see it exists and skip adding it. 452 quote! { 453 #doc 454 #[jacquard_derive::lexicon] 455 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, bon::Builder, jacquard_derive::IntoStatic)] 456 #[serde(rename_all = "camelCase")] 457 #[builder(start_fn = new)] 458 pub struct #ident<'a> { 459 #fields 460 #[serde(flatten)] 461 #[serde(borrow)] 462 #[builder(default)] 463 pub extra_data: ::std::collections::BTreeMap< 464 ::jacquard_common::smol_str::SmolStr, 465 ::jacquard_common::types::value::Data<'a> 466 >, 467 } 468 } 469 } else if has_default { 470 quote! { 471 #doc 472 #[jacquard_derive::lexicon] 473 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic, Default)] 474 #[serde(rename_all = "camelCase")] 475 pub struct #ident<'a> { 476 #fields 477 } 478 } 479 } else { 480 quote! { 481 #doc 482 #[jacquard_derive::lexicon] 483 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic)] 484 #[serde(rename_all = "camelCase")] 485 pub struct #ident<'a> { 486 #fields 487 } 488 } 489 }; 490 491 // Generate union types if schema is an Object 492 let mut unions = Vec::new(); 493 if let Some(crate::lexicon::LexXrpcBodySchema::Object(obj)) = &body.schema { 494 for (field_name, field_type) in &obj.properties { 495 match field_type { 496 LexObjectProperty::Union(union) => { 497 // Skip empty, single-variant unions unless they're self-referential 498 if !union.refs.is_empty() && (union.refs.len() > 1 || self.is_self_referential_union(nsid, type_base, union)) { 499 let union_name = self.generate_field_type_name(nsid, type_base, field_name, ""); 500 let refs: Vec<_> = union.refs.iter().cloned().collect(); 501 let union_def = 502 self.generate_union(nsid, &union_name, &refs, None, union.closed)?; 503 unions.push(union_def); 504 } 505 } 506 LexObjectProperty::Array(array) => { 507 if let LexArrayItem::Union(union) = &array.items { 508 // Skip single-variant array unions 509 if union.refs.len() > 1 { 510 let union_name = self.generate_field_type_name(nsid, type_base, field_name, "Item"); 511 let refs: Vec<_> = union.refs.iter().cloned().collect(); 512 let union_def = self.generate_union(nsid, &union_name, &refs, None, union.closed)?; 513 unions.push(union_def); 514 } 515 } 516 } 517 _ => {} 518 } 519 } 520 } 521 522 Ok(quote! { 523 #struct_def 524 #(#unions)* 525 }) 526 } 527 528 /// Generate output struct from XRPC body 529 pub(super) fn generate_output_struct( 530 &self, 531 nsid: &str, 532 type_base: &str, 533 body: &LexXrpcBody<'static>, 534 ) -> Result<TokenStream> { 535 let struct_name = format!("{}Output", type_base); 536 let ident = syn::Ident::new(&struct_name, proc_macro2::Span::call_site()); 537 538 let fields = if let Some(schema) = &body.schema { 539 self.generate_body_fields("", &struct_name, schema, false)? 540 } else { 541 quote! {} 542 }; 543 544 let doc = self.generate_doc_comment(body.description.as_ref()); 545 546 // Determine if we should derive Default 547 // Check if schema is an Object and apply heuristics 548 let has_default = if let Some(crate::lexicon::LexXrpcBodySchema::Object(obj)) = &body.schema { 549 use crate::codegen::structs::{count_required_fields, all_required_are_defaultable_strings}; 550 let required_count = count_required_fields(obj); 551 required_count == 0 || all_required_are_defaultable_strings(obj) 552 } else { 553 false 554 }; 555 556 // Output structs always get a lifetime since they have the #[lexicon] attribute 557 // which adds extra_data: BTreeMap<..., Data<'a>> 558 let struct_def = if has_default { 559 quote! { 560 #doc 561 #[jacquard_derive::lexicon] 562 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic, Default)] 563 #[serde(rename_all = "camelCase")] 564 pub struct #ident<'a> { 565 #fields 566 } 567 } 568 } else { 569 quote! { 570 #doc 571 #[jacquard_derive::lexicon] 572 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic)] 573 #[serde(rename_all = "camelCase")] 574 pub struct #ident<'a> { 575 #fields 576 } 577 } 578 }; 579 580 // Generate union types if schema is an Object 581 let mut unions = Vec::new(); 582 if let Some(crate::lexicon::LexXrpcBodySchema::Object(obj)) = &body.schema { 583 for (field_name, field_type) in &obj.properties { 584 match field_type { 585 LexObjectProperty::Union(union) => { 586 // Skip single-variant unions unless they're self-referential 587 if union.refs.len() > 1 || self.is_self_referential_union(nsid, &struct_name, union) { 588 let union_name = self.generate_field_type_name(nsid, &struct_name, field_name, ""); 589 let refs: Vec<_> = union.refs.iter().cloned().collect(); 590 let union_def = 591 self.generate_union(nsid, &union_name, &refs, None, union.closed)?; 592 unions.push(union_def); 593 } 594 } 595 LexObjectProperty::Array(array) => { 596 if let LexArrayItem::Union(union) = &array.items { 597 // Skip single-variant array unions 598 if union.refs.len() > 1 { 599 let union_name = self.generate_field_type_name(nsid, &struct_name, field_name, "Item"); 600 let refs: Vec<_> = union.refs.iter().cloned().collect(); 601 let union_def = self.generate_union(nsid, &union_name, &refs, None, union.closed)?; 602 unions.push(union_def); 603 } 604 } 605 } 606 _ => {} 607 } 608 } 609 } 610 611 Ok(quote! { 612 #struct_def 613 #(#unions)* 614 }) 615 } 616 617 /// Generate fields from XRPC body schema 618 pub(super) fn generate_body_fields( 619 &self, 620 nsid: &str, 621 parent_type_name: &str, 622 schema: &LexXrpcBodySchema<'static>, 623 is_builder: bool, 624 ) -> Result<TokenStream> { 625 use crate::lexicon::LexXrpcBodySchema; 626 627 match schema { 628 LexXrpcBodySchema::Object(obj) => { 629 self.generate_object_fields(nsid, parent_type_name, obj, is_builder) 630 } 631 LexXrpcBodySchema::Ref(ref_type) => { 632 let rust_type = self.ref_to_rust_type(&ref_type.r#ref)?; 633 Ok(quote! { 634 #[serde(flatten)] 635 #[serde(borrow)] 636 pub value: #rust_type, 637 }) 638 } 639 LexXrpcBodySchema::Union(_union) => { 640 let rust_type = quote! { jacquard_common::types::value::Data<'a> }; 641 Ok(quote! { 642 #[serde(flatten)] 643 #[serde(borrow)] 644 pub value: #rust_type, 645 }) 646 } 647 } 648 } 649 650 /// Generate a field for XRPC parameters 651 pub(super) fn generate_param_field( 652 &self, 653 _nsid: &str, 654 field_name: &str, 655 field_type: &crate::lexicon::LexXrpcParametersProperty<'static>, 656 is_required: bool, 657 ) -> Result<TokenStream> { 658 use crate::lexicon::LexXrpcParametersProperty; 659 660 let field_ident = make_ident(&field_name.to_snake_case()); 661 662 let (rust_type, needs_lifetime, is_cowstr) = match field_type { 663 LexXrpcParametersProperty::Boolean(_) => (quote! { bool }, false, false), 664 LexXrpcParametersProperty::Integer(_) => (quote! { i64 }, false, false), 665 LexXrpcParametersProperty::String(s) => { 666 let is_cowstr = s.format.is_none(); // CowStr for plain strings 667 ( 668 self.string_to_rust_type(s), 669 self.string_needs_lifetime(s), 670 is_cowstr, 671 ) 672 } 673 LexXrpcParametersProperty::Unknown(_) => ( 674 quote! { jacquard_common::types::value::Data<'a> }, 675 true, 676 false, 677 ), 678 LexXrpcParametersProperty::Array(arr) => { 679 let needs_lifetime = match &arr.items { 680 crate::lexicon::LexPrimitiveArrayItem::Boolean(_) 681 | crate::lexicon::LexPrimitiveArrayItem::Integer(_) => false, 682 crate::lexicon::LexPrimitiveArrayItem::String(s) => { 683 self.string_needs_lifetime(s) 684 } 685 crate::lexicon::LexPrimitiveArrayItem::Unknown(_) => true, 686 }; 687 let item_type = match &arr.items { 688 crate::lexicon::LexPrimitiveArrayItem::Boolean(_) => quote! { bool }, 689 crate::lexicon::LexPrimitiveArrayItem::Integer(_) => quote! { i64 }, 690 crate::lexicon::LexPrimitiveArrayItem::String(s) => self.string_to_rust_type(s), 691 crate::lexicon::LexPrimitiveArrayItem::Unknown(_) => { 692 quote! { jacquard_common::types::value::Data<'a> } 693 } 694 }; 695 (quote! { Vec<#item_type> }, needs_lifetime, false) 696 } 697 }; 698 699 let rust_type = if is_required { 700 rust_type 701 } else { 702 quote! { std::option::Option<#rust_type> } 703 }; 704 705 let mut attrs = Vec::new(); 706 707 if !is_required { 708 attrs.push(quote! { #[serde(skip_serializing_if = "std::option::Option::is_none")] }); 709 } 710 711 // Add serde(borrow) to all fields with lifetimes 712 if needs_lifetime { 713 attrs.push(quote! { #[serde(borrow)] }); 714 } 715 716 // Add builder(into) for CowStr fields to allow String, &str, etc. 717 if is_cowstr { 718 attrs.push(quote! { #[builder(into)] }); 719 } 720 721 Ok(quote! { 722 #(#attrs)* 723 pub #field_ident: #rust_type, 724 }) 725 } 726 727 /// Generate param field with serde default if present 728 /// Returns (field_tokens, optional_default_function) 729 pub(super) fn generate_param_field_with_default( 730 &self, 731 nsid: &str, 732 field_name: &str, 733 field_type: &crate::lexicon::LexXrpcParametersProperty<'static>, 734 is_required: bool, 735 ) -> Result<(TokenStream, Option<TokenStream>)> { 736 use crate::lexicon::LexXrpcParametersProperty; 737 use heck::ToSnakeCase; 738 739 // Get base field 740 let base_field = self.generate_param_field(nsid, field_name, field_type, is_required)?; 741 742 // Generate default function and attribute for required fields with defaults 743 // For optional fields, just add doc comments 744 let (doc_comment, serde_attr, default_fn) = if is_required { 745 match field_type { 746 LexXrpcParametersProperty::Boolean(b) if b.default.is_some() => { 747 let v = b.default.unwrap(); 748 let fn_name = format!("_default_{}", field_name.to_snake_case()); 749 let fn_ident = syn::Ident::new(&fn_name, proc_macro2::Span::call_site()); 750 ( 751 Some(format!("Defaults to `{}`", v)), 752 Some(quote! { #[serde(default = #fn_name)] }), 753 Some(quote! { 754 fn #fn_ident() -> bool { #v } 755 }), 756 ) 757 } 758 LexXrpcParametersProperty::Integer(i) if i.default.is_some() => { 759 let v = i.default.unwrap(); 760 let fn_name = format!("_default_{}", field_name.to_snake_case()); 761 let fn_ident = syn::Ident::new(&fn_name, proc_macro2::Span::call_site()); 762 ( 763 Some(format!("Defaults to `{}`", v)), 764 Some(quote! { #[serde(default = #fn_name)] }), 765 Some(quote! { 766 fn #fn_ident() -> i64 { #v } 767 }), 768 ) 769 } 770 LexXrpcParametersProperty::String(s) if s.default.is_some() => { 771 let v = s.default.as_ref().unwrap().as_ref(); 772 let fn_name = format!("_default_{}", field_name.to_snake_case()); 773 let fn_ident = syn::Ident::new(&fn_name, proc_macro2::Span::call_site()); 774 ( 775 Some(format!("Defaults to `\"{}\"`", v)), 776 Some(quote! { #[serde(default = #fn_name)] }), 777 Some(quote! { 778 fn #fn_ident() -> jacquard_common::CowStr<'static> { 779 jacquard_common::CowStr::from(#v) 780 } 781 }), 782 ) 783 } 784 _ => (None, None, None), 785 } 786 } else { 787 // Optional fields - just doc comments, no serde defaults 788 let doc = match field_type { 789 LexXrpcParametersProperty::Integer(i) => { 790 let mut parts = Vec::new(); 791 if let Some(def) = i.default { 792 parts.push(format!("default: {}", def)); 793 } 794 if let Some(min) = i.minimum { 795 parts.push(format!("min: {}", min)); 796 } 797 if let Some(max) = i.maximum { 798 parts.push(format!("max: {}", max)); 799 } 800 if !parts.is_empty() { 801 Some(format!("({})", parts.join(", "))) 802 } else { 803 None 804 } 805 } 806 LexXrpcParametersProperty::String(s) => { 807 let mut parts = Vec::new(); 808 if let Some(def) = s.default.as_ref() { 809 parts.push(format!("default: \"{}\"", def.as_ref())); 810 } 811 if let Some(min) = s.min_length { 812 parts.push(format!("min length: {}", min)); 813 } 814 if let Some(max) = s.max_length { 815 parts.push(format!("max length: {}", max)); 816 } 817 if !parts.is_empty() { 818 Some(format!("({})", parts.join(", "))) 819 } else { 820 None 821 } 822 } 823 LexXrpcParametersProperty::Boolean(b) => { 824 b.default.map(|v| format!("(default: {})", v)) 825 } 826 _ => None, 827 }; 828 (doc, None, None) 829 }; 830 831 let doc = doc_comment.as_ref().map(|d| quote! { #[doc = #d] }); 832 let field_with_attrs = match (doc, serde_attr) { 833 (Some(doc), Some(attr)) => quote! { 834 #doc 835 #attr 836 #base_field 837 }, 838 (Some(doc), None) => quote! { 839 #doc 840 #base_field 841 }, 842 (None, Some(attr)) => quote! { 843 #attr 844 #base_field 845 }, 846 (None, None) => base_field, 847 }; 848 849 Ok((field_with_attrs, default_fn)) 850 } 851 852 /// Generate error enum from XRPC errors 853 pub(super) fn generate_error_enum( 854 &self, 855 type_base: &str, 856 errors: &[LexXrpcError<'static>], 857 ) -> Result<TokenStream> { 858 let enum_name = format!("{}Error", type_base); 859 let ident = syn::Ident::new(&enum_name, proc_macro2::Span::call_site()); 860 861 let mut variants = Vec::new(); 862 let mut display_arms = Vec::new(); 863 864 for error in errors { 865 let variant_name = error.name.to_pascal_case(); 866 let variant_ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site()); 867 868 let error_name = error.name.as_ref(); 869 let doc = self.generate_doc_comment(error.description.as_ref()); 870 871 variants.push(quote! { 872 #doc 873 #[serde(rename = #error_name)] 874 #variant_ident(std::option::Option<String>) 875 }); 876 877 display_arms.push(quote! { 878 Self::#variant_ident(msg) => { 879 write!(f, #error_name)?; 880 if let Some(msg) = msg { 881 write!(f, ": {}", msg)?; 882 } 883 Ok(()) 884 } 885 }); 886 } 887 888 // IntoStatic impl is generated by the derive macro now 889 890 Ok(quote! { 891 #[jacquard_derive::open_union] 892 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, thiserror::Error, miette::Diagnostic, jacquard_derive::IntoStatic)] 893 #[serde(tag = "error", content = "message")] 894 #[serde(bound(deserialize = "'de: 'a"))] 895 pub enum #ident<'a> { 896 #(#variants,)* 897 } 898 899 impl std::fmt::Display for #ident<'_> { 900 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 901 match self { 902 #(#display_arms)* 903 Self::Unknown(err) => write!(f, "Unknown error: {:?}", err), 904 } 905 } 906 } 907 }) 908 } 909 910 /// Generate XrpcRequest trait impl for a query or procedure 911 pub(super) fn generate_xrpc_request_impl( 912 &self, 913 nsid: &str, 914 type_base: &str, 915 method: TokenStream, 916 output_encoding: &str, 917 has_params: bool, 918 params_has_lifetime: bool, 919 has_output: bool, 920 has_errors: bool, 921 is_binary_input: bool, 922 ) -> Result<TokenStream> { 923 let output_type = if has_output { 924 let output_ident = syn::Ident::new( 925 &format!("{}Output", type_base), 926 proc_macro2::Span::call_site(), 927 ); 928 quote! { #output_ident<'de> } 929 } else { 930 quote! { () } 931 }; 932 933 let error_type = if has_errors { 934 let error_ident = syn::Ident::new( 935 &format!("{}Error", type_base), 936 proc_macro2::Span::call_site(), 937 ); 938 quote! { #error_ident<'de> } 939 } else { 940 quote! { jacquard_common::xrpc::GenericError<'de> } 941 }; 942 943 // Generate the response type that implements XrpcResp 944 let response_ident = syn::Ident::new( 945 &format!("{}Response", type_base), 946 proc_macro2::Span::call_site(), 947 ); 948 949 // Generate the endpoint type that implements XrpcEndpoint 950 let endpoint_ident = syn::Ident::new( 951 &format!("{}Request", type_base), 952 proc_macro2::Span::call_site(), 953 ); 954 955 let response_type = quote! { 956 #[doc = "Response type for "] 957 #[doc = #nsid] 958 pub struct #response_ident; 959 960 impl jacquard_common::xrpc::XrpcResp for #response_ident { 961 const NSID: &'static str = #nsid; 962 const ENCODING: &'static str = #output_encoding; 963 type Output<'de> = #output_type; 964 type Err<'de> = #error_type; 965 } 966 }; 967 968 // Generate encode_body() method for binary inputs 969 let encode_body_method = if is_binary_input { 970 quote! { 971 fn encode_body(&self) -> Result<Vec<u8>, jacquard_common::xrpc::EncodeError> { 972 Ok(self.body.to_vec()) 973 } 974 } 975 } else { 976 quote! {} 977 }; 978 979 // Generate decode_body() method for binary inputs 980 let decode_body_method = if is_binary_input { 981 quote! { 982 fn decode_body<'de>( 983 body: &'de [u8], 984 ) -> Result<Box<Self>, jacquard_common::error::DecodeError> 985 where 986 Self: serde::Deserialize<'de>, 987 { 988 Ok(Box::new(Self { 989 body: bytes::Bytes::copy_from_slice(body), 990 })) 991 } 992 } 993 } else { 994 quote! {} 995 }; 996 997 let endpoint_path = format!("/xrpc/{}", nsid); 998 999 if has_params { 1000 // Implement on the params/input struct itself 1001 let request_ident = syn::Ident::new(type_base, proc_macro2::Span::call_site()); 1002 1003 let (impl_generics, impl_target, endpoint_request_type) = if params_has_lifetime { 1004 ( 1005 quote! { <'a> }, 1006 quote! { #request_ident<'a> }, 1007 quote! { #request_ident<'de> }, 1008 ) 1009 } else { 1010 ( 1011 quote! {}, 1012 quote! { #request_ident }, 1013 quote! { #request_ident }, 1014 ) 1015 }; 1016 1017 Ok(quote! { 1018 #response_type 1019 1020 impl #impl_generics jacquard_common::xrpc::XrpcRequest for #impl_target { 1021 const NSID: &'static str = #nsid; 1022 const METHOD: jacquard_common::xrpc::XrpcMethod = #method; 1023 1024 type Response = #response_ident; 1025 1026 #encode_body_method 1027 #decode_body_method 1028 } 1029 1030 #[doc = "Endpoint type for "] 1031 #[doc = #nsid] 1032 pub struct #endpoint_ident; 1033 1034 impl jacquard_common::xrpc::XrpcEndpoint for #endpoint_ident { 1035 const PATH: &'static str = #endpoint_path; 1036 const METHOD: jacquard_common::xrpc::XrpcMethod = #method; 1037 1038 type Request<'de> = #endpoint_request_type; 1039 type Response = #response_ident; 1040 } 1041 }) 1042 } else { 1043 // No params - generate a marker struct 1044 let request_ident = syn::Ident::new(type_base, proc_macro2::Span::call_site()); 1045 1046 Ok(quote! { 1047 /// XRPC request marker type 1048 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, jacquard_derive::IntoStatic)] 1049 pub struct #request_ident; 1050 1051 #response_type 1052 1053 impl jacquard_common::xrpc::XrpcRequest for #request_ident { 1054 const NSID: &'static str = #nsid; 1055 const METHOD: jacquard_common::xrpc::XrpcMethod = #method; 1056 1057 type Response = #response_ident; 1058 } 1059 1060 #[doc = "Endpoint type for "] 1061 #[doc = #nsid] 1062 pub struct #endpoint_ident; 1063 1064 impl jacquard_common::xrpc::XrpcEndpoint for #endpoint_ident { 1065 const PATH: &'static str = #endpoint_path; 1066 const METHOD: jacquard_common::xrpc::XrpcMethod = #method; 1067 1068 type Request<'de> = #request_ident; 1069 type Response = #response_ident; 1070 } 1071 }) 1072 } 1073 } 1074}