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}