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