A better Rust ATProto crate
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}