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::smol_str::SmolStr;
7use proc_macro2::TokenStream;
8use quote::quote;
9use std::collections::BTreeMap;
10
11use super::CodeGenerator;
12use super::utils::{known_value_to_variant_name, make_ident, value_to_variant_name};
13
14/// Enum variant kind for IntoStatic generation
15#[derive(Debug, Clone)]
16#[allow(dead_code)]
17pub(super) enum EnumVariantKind {
18 Unit,
19 Tuple,
20 Struct(Vec<String>),
21}
22
23impl<'c> CodeGenerator<'c> {
24 /// Generate all nested type definitions (unions, objects) for an object's properties.
25 /// This consolidates the pattern of iterating properties to find unions and nested objects
26 /// that need their own type definitions.
27 ///
28 /// # Parameters
29 /// - `include_nested_objects`: If false, skips generating nested object types (used by XRPC)
30 pub(super) fn generate_nested_types(
31 &self,
32 nsid: &str,
33 parent_type_name: &str,
34 properties: &BTreeMap<SmolStr, LexObjectProperty<'static>>,
35 include_nested_objects: bool,
36 ) -> Result<Vec<TokenStream>> {
37 let mut nested = Vec::new();
38
39 for (field_name, field_type) in properties {
40 match field_type {
41 LexObjectProperty::Union(union) => {
42 // Skip empty, single-variant unions unless they're self-referential
43 if !union.refs.is_empty()
44 && (union.refs.len() > 1
45 || self.is_self_referential_union(nsid, parent_type_name, &union))
46 {
47 let union_name =
48 self.generate_field_type_name(nsid, parent_type_name, field_name, "");
49 let refs: Vec<_> = union.refs.iter().cloned().collect();
50 let union_def =
51 self.generate_union(nsid, &union_name, &refs, None, union.closed)?;
52 nested.push(union_def);
53 }
54 }
55 LexObjectProperty::Object(nested_obj) if include_nested_objects => {
56 let object_name =
57 self.generate_field_type_name(nsid, parent_type_name, field_name, "");
58 let obj_def = self.generate_object(nsid, &object_name, &nested_obj)?;
59 nested.push(obj_def);
60 }
61 LexObjectProperty::Array(array) => {
62 if let LexArrayItem::Union(union) = &array.items {
63 // Skip single-variant array unions
64 if union.refs.len() > 1 {
65 let union_name = self.generate_field_type_name(
66 nsid,
67 parent_type_name,
68 field_name,
69 "Item",
70 );
71 let refs: Vec<_> = union.refs.iter().cloned().collect();
72 let union_def =
73 self.generate_union(nsid, &union_name, &refs, None, union.closed)?;
74 nested.push(union_def);
75 }
76 }
77 }
78 LexObjectProperty::String(s) if s.known_values.is_some() => {
79 let enum_name =
80 self.generate_field_type_name(nsid, parent_type_name, field_name, "");
81 let enum_def = self.generate_inline_known_values_enum(&enum_name, s)?;
82 nested.push(enum_def);
83 }
84 _ => {}
85 }
86 }
87
88 Ok(nested)
89 }
90
91 pub(super) fn generate_record(
92 &self,
93 nsid: &str,
94 def_name: &str,
95 record: &LexRecord<'static>,
96 ) -> Result<TokenStream> {
97 match &record.record {
98 crate::lexicon::LexRecordRecord::Object(obj) => {
99 let type_name = self.def_to_type_name(nsid, def_name);
100 let ident = syn::Ident::new(&type_name, proc_macro2::Span::call_site());
101
102 // Records always get a lifetime since they have the #[lexicon] attribute
103 // which adds extra_data: BTreeMap<..., Data<'a>>
104 // Skip custom builder for types that conflict with the macro's unqualified type references
105 let has_builder =
106 !super::builder_heuristics::conflicts_with_builder_macro(&type_name);
107
108 // Generate main struct fields
109 let fields = self.generate_object_fields(nsid, &type_name, obj, has_builder)?;
110 let doc = self.generate_doc_comment(record.description.as_ref());
111
112 let struct_def = quote! {
113 #doc
114 #[jacquard_derive::lexicon]
115 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic)]
116 #[serde(rename_all = "camelCase")]
117 pub struct #ident<'a> {
118 #fields
119 }
120 };
121
122 // Generate custom builder if needed
123 let builder = if has_builder {
124 let ctx = super::builder_gen::BuilderGenContext::from_object(
125 self, nsid, &type_name, obj, true, // records always have lifetime
126 );
127 ctx.generate()
128 } else {
129 quote! {}
130 };
131
132 // Generate union types and nested object types for this record
133 let unions = self.generate_nested_types(nsid, &type_name, &obj.properties, true)?;
134
135 // Generate typed GetRecordOutput wrapper
136 let output_type_name = format!("{}GetRecordOutput", type_name);
137 let output_type_ident =
138 syn::Ident::new(&output_type_name, proc_macro2::Span::call_site());
139
140 let output_wrapper = quote! {
141 /// Typed wrapper for GetRecord response with this collection's record type.
142 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic)]
143 #[serde(rename_all = "camelCase")]
144 pub struct #output_type_ident<'a> {
145 #[serde(skip_serializing_if = "std::option::Option::is_none")]
146 #[serde(borrow)]
147 pub cid: std::option::Option<jacquard_common::types::string::Cid<'a>>,
148 #[serde(borrow)]
149 pub uri: jacquard_common::types::string::AtUri<'a>,
150 #[serde(borrow)]
151 pub value: #ident<'a>,
152 }
153 };
154
155 // Generate marker struct for XrpcResp
156 let record_marker_name = format!("{}Record", type_name);
157 let record_marker_ident =
158 syn::Ident::new(&record_marker_name, proc_macro2::Span::call_site());
159
160 let record_marker = quote! {
161 /// Marker type for deserializing records from this collection.
162 #[derive(Debug, serde::Serialize, serde::Deserialize)]
163 pub struct #record_marker_ident;
164
165 impl jacquard_common::xrpc::XrpcResp for #record_marker_ident {
166 const NSID: &'static str = #nsid;
167 const ENCODING: &'static str = "application/json";
168 type Output<'de> = #output_type_ident<'de>;
169 type Err<'de> = jacquard_common::types::collection::RecordError<'de>;
170 }
171
172
173 };
174 let from_impl = quote! {
175 impl From<#output_type_ident<'_>> for #ident<'_> {
176 fn from(output: #output_type_ident<'_>) -> Self {
177 use jacquard_common::IntoStatic;
178 output.value.into_static()
179 }
180 }
181 };
182
183 // Generate Collection trait impl
184 let collection_impl = quote! {
185 impl jacquard_common::types::collection::Collection for #ident<'_> {
186 const NSID: &'static str = #nsid;
187 type Record = #record_marker_ident;
188 }
189 };
190
191 // Generate collection impl for the marker struct to drive fetch_record()
192 let collection_marker_impl = quote! {
193 impl jacquard_common::types::collection::Collection for #record_marker_ident {
194 const NSID: &'static str = #nsid;
195 type Record = #record_marker_ident;
196 }
197 };
198
199 // Generate LexiconSchema impl with shared lexicon_doc function
200 let (shared_fn, schema_impl) =
201 self.generate_schema_impl_with_shared(&type_name, nsid, "main", true);
202
203 Ok(quote! {
204 #struct_def
205 #builder
206
207 impl<'a> #ident<'a> {
208 pub fn uri(uri: impl Into<jacquard_common::CowStr<'a>>) -> Result<jacquard_common::types::uri::RecordUri<'a, #record_marker_ident>, jacquard_common::types::uri::UriError> {
209 jacquard_common::types::uri::RecordUri::try_from_uri(jacquard_common::types::string::AtUri::new_cow(uri.into())?)
210 }
211 }
212 #(#unions)*
213 #output_wrapper
214 #from_impl
215 #collection_impl
216 #record_marker
217 #collection_marker_impl
218 #schema_impl
219 #shared_fn
220 })
221 }
222 }
223 }
224
225 /// Generate an object type
226 pub(super) fn generate_object(
227 &self,
228 nsid: &str,
229 def_name: &str,
230 obj: &LexObject<'static>,
231 ) -> Result<TokenStream> {
232 let type_name = self.def_to_type_name(nsid, def_name);
233 let ident = syn::Ident::new(&type_name, proc_macro2::Span::call_site());
234
235 // Objects always get a lifetime since they have the #[lexicon] attribute
236 // which adds extra_data: BTreeMap<..., Data<'a>>
237
238 // Smart heuristics for builder generation:
239 // - 0 required fields: Default instead of builder
240 // - All required fields are bare strings: Default instead of builder
241 // - 1+ required fields (not all strings): custom builder (but not if name conflicts)
242 let decision = super::builder_heuristics::should_generate_builder(&type_name, obj);
243 let has_builder = decision.has_builder;
244 let has_default = decision.has_default;
245
246 let fields = self.generate_object_fields(nsid, &type_name, obj, has_builder)?;
247 let doc = self.generate_doc_comment(obj.description.as_ref());
248
249 let struct_def = if has_default {
250 quote! {
251 #doc
252 #[jacquard_derive::lexicon]
253 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic, Default)]
254 #[serde(rename_all = "camelCase")]
255 pub struct #ident<'a> {
256 #fields
257 }
258 }
259 } else {
260 quote! {
261 #doc
262 #[jacquard_derive::lexicon]
263 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic)]
264 #[serde(rename_all = "camelCase")]
265 pub struct #ident<'a> {
266 #fields
267 }
268 }
269 };
270
271 // Generate custom builder if needed
272 let builder = if has_builder {
273 let ctx = super::builder_gen::BuilderGenContext::from_object(
274 self, nsid, &type_name, obj, true, // objects always have lifetime
275 );
276 ctx.generate()
277 } else {
278 quote! {}
279 };
280
281 // Generate union types and nested object types for this object
282 let unions = self.generate_nested_types(nsid, &type_name, &obj.properties, true)?;
283
284 // Generate LexiconSchema impl with shared lexicon_doc function
285 let (shared_fn, schema_impl) =
286 self.generate_schema_impl_with_shared(&type_name, nsid, def_name, true);
287
288 Ok(quote! {
289 #struct_def
290 #builder
291 #(#unions)*
292 #shared_fn
293 #schema_impl
294 })
295 }
296
297 /// Generate fields for an object
298 pub(super) fn generate_object_fields(
299 &self,
300 nsid: &str,
301 parent_type_name: &str,
302 obj: &LexObject<'static>,
303 _is_builder: bool,
304 ) -> Result<TokenStream> {
305 let required = obj.required.as_ref().map(|r| r.as_slice()).unwrap_or(&[]);
306 let nullable = obj.nullable.as_ref().map(|n| n.as_slice()).unwrap_or(&[]);
307
308 let mut fields = Vec::new();
309 for (field_name, field_type) in &obj.properties {
310 let is_required = required.contains(field_name);
311 let is_nullable = nullable.contains(field_name);
312 let field_tokens = self.generate_field(
313 nsid,
314 parent_type_name,
315 field_name,
316 field_type,
317 is_required,
318 is_nullable,
319 )?;
320 fields.push(field_tokens);
321 }
322
323 Ok(quote! { #(#fields)* })
324 }
325
326 /// Generate a single field
327 pub(super) fn generate_field(
328 &self,
329 nsid: &str,
330 parent_type_name: &str,
331 field_name: &str,
332 field_type: &LexObjectProperty<'static>,
333 is_required: bool,
334 is_nullable: bool,
335 ) -> Result<TokenStream> {
336 if field_name.is_empty() {
337 eprintln!(
338 "Warning: Empty field name in lexicon '{}' type '{}', using 'unknown' as fallback",
339 nsid, parent_type_name
340 );
341 }
342 let field_ident = make_ident(&field_name.to_snake_case());
343
344 let rust_type =
345 self.property_to_rust_type(nsid, parent_type_name, field_name, field_type)?;
346 let needs_lifetime = self.property_needs_lifetime(field_type);
347
348 let rust_type = if is_required && !is_nullable {
349 rust_type
350 } else {
351 quote! { std::option::Option<#rust_type> }
352 };
353
354 // Extract description from field type
355 let description = match field_type {
356 LexObjectProperty::Ref(r) => r.description.as_ref(),
357 LexObjectProperty::Union(u) => u.description.as_ref(),
358 LexObjectProperty::Bytes(b) => b.description.as_ref(),
359 LexObjectProperty::CidLink(c) => c.description.as_ref(),
360 LexObjectProperty::Array(a) => a.description.as_ref(),
361 LexObjectProperty::Blob(b) => b.description.as_ref(),
362 LexObjectProperty::Object(o) => o.description.as_ref(),
363 LexObjectProperty::Boolean(b) => b.description.as_ref(),
364 LexObjectProperty::Integer(i) => i.description.as_ref(),
365 LexObjectProperty::String(s) => s.description.as_ref(),
366 LexObjectProperty::Unknown(u) => u.description.as_ref(),
367 };
368 let doc = self.generate_doc_comment(description);
369
370 let mut attrs = Vec::new();
371
372 if !is_required {
373 attrs.push(quote! { #[serde(skip_serializing_if = "std::option::Option::is_none")] });
374 }
375
376 // Add serde(borrow) to all fields with lifetimes
377 if needs_lifetime {
378 attrs.push(quote! { #[serde(borrow)] });
379 }
380
381 if matches!(field_type, LexObjectProperty::Bytes(_)) {
382 if is_required {
383 attrs.push(quote! { #[serde(with = "jacquard_common::serde_bytes_helper")] });
384 } else {
385 attrs.push(
386 quote! {#[serde(default, with = "jacquard_common::opt_serde_bytes_helper")] },
387 );
388 }
389 }
390
391 Ok(quote! {
392 #doc
393 #(#attrs)*
394 pub #field_ident: #rust_type,
395 })
396 }
397
398 /// Generate a union enum for refs
399 pub fn generate_union(
400 &self,
401 current_nsid: &str,
402 union_name: &str,
403 refs: &[jacquard_common::CowStr<'static>],
404 description: Option<&str>,
405 closed: Option<bool>,
406 ) -> Result<TokenStream> {
407 let enum_ident = syn::Ident::new(union_name, proc_macro2::Span::call_site());
408
409 // Build variants using the union_codegen module
410 let ctx = super::union_codegen::UnionGenContext {
411 corpus: self.corpus,
412 namespace_deps: &self.namespace_deps,
413 current_nsid,
414 };
415
416 let union_variants =
417 ctx.build_union_variants(refs, |ref_str| self.ref_to_rust_type(ref_str))?;
418 let variants = super::union_codegen::generate_variant_tokens(&union_variants);
419
420 let doc = description
421 .map(|d| quote! { #[doc = #d] })
422 .unwrap_or_else(|| quote! {});
423
424 // Only add open_union if not closed
425 let is_open = closed != Some(true);
426
427 if is_open {
428 Ok(quote! {
429 #doc
430 #[jacquard_derive::open_union]
431 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic)]
432 #[serde(tag = "$type")]
433 #[serde(bound(deserialize = "'de: 'a"))]
434 pub enum #enum_ident<'a> {
435 #(#variants,)*
436 }
437 })
438 } else {
439 Ok(quote! {
440 #doc
441 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic)]
442 #[serde(tag = "$type")]
443 #[serde(bound(deserialize = "'de: 'a"))]
444 pub enum #enum_ident<'a> {
445 #(#variants,)*
446 }
447 })
448 }
449 }
450
451 /// Generate enum for string with known values
452 pub(super) fn generate_known_values_enum(
453 &self,
454 nsid: &str,
455 def_name: &str,
456 string: &LexString<'static>,
457 ) -> Result<TokenStream> {
458 let type_name = self.def_to_type_name(nsid, def_name);
459 let ident = syn::Ident::new(&type_name, proc_macro2::Span::call_site());
460
461 let known_values = string.known_values.as_ref().unwrap();
462 let mut variants = Vec::new();
463 let mut from_str_arms = Vec::new();
464 let mut as_str_arms = Vec::new();
465
466 for value in known_values {
467 // Convert value to valid Rust identifier
468 let value_str = value.as_ref();
469 let variant_name = value_to_variant_name(value_str);
470 let variant_ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site());
471
472 variants.push(quote! {
473 #variant_ident
474 });
475
476 from_str_arms.push(quote! {
477 #value_str => Self::#variant_ident
478 });
479
480 as_str_arms.push(quote! {
481 Self::#variant_ident => #value_str
482 });
483 }
484
485 let doc = self.generate_doc_comment(string.description.as_ref());
486
487 // Generate IntoStatic impl
488 let variant_info: Vec<(String, EnumVariantKind)> = known_values
489 .iter()
490 .map(|value| {
491 let variant_name = value_to_variant_name(value.as_ref());
492 (variant_name, EnumVariantKind::Unit)
493 })
494 .chain(std::iter::once((
495 "Other".to_string(),
496 EnumVariantKind::Tuple,
497 )))
498 .collect();
499 let into_static_impl =
500 self.generate_into_static_for_enum(&type_name, &variant_info, true, false);
501
502 Ok(quote! {
503 #doc
504 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
505 pub enum #ident<'a> {
506 #(#variants,)*
507 Other(jacquard_common::CowStr<'a>),
508 }
509
510 impl<'a> #ident<'a> {
511 pub fn as_str(&self) -> &str {
512 match self {
513 #(#as_str_arms,)*
514 Self::Other(s) => s.as_ref(),
515 }
516 }
517 }
518
519 impl<'a> From<&'a str> for #ident<'a> {
520 fn from(s: &'a str) -> Self {
521 match s {
522 #(#from_str_arms,)*
523 _ => Self::Other(jacquard_common::CowStr::from(s)),
524 }
525 }
526 }
527
528 impl<'a> From<String> for #ident<'a> {
529 fn from(s: String) -> Self {
530 match s.as_str() {
531 #(#from_str_arms,)*
532 _ => Self::Other(jacquard_common::CowStr::from(s)),
533 }
534 }
535 }
536
537 impl<'a> AsRef<str> for #ident<'a> {
538 fn as_ref(&self) -> &str {
539 self.as_str()
540 }
541 }
542
543
544 impl<'a> core::fmt::Display for #ident<'a> {
545 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
546 write!(f, "{}", self.as_str())
547 }
548 }
549
550 impl<'a> serde::Serialize for #ident<'a> {
551 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
552 where
553 S: serde::Serializer,
554 {
555 serializer.serialize_str(self.as_str())
556 }
557 }
558
559 impl<'de, 'a> serde::Deserialize<'de> for #ident<'a>
560 where
561 'de: 'a,
562 {
563 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
564 where
565 D: serde::Deserializer<'de>,
566 {
567 let s = <&'de str>::deserialize(deserializer)?;
568 Ok(Self::from(s))
569 }
570 }
571
572 #into_static_impl
573 })
574 }
575
576 /// Generate enum for inline string property with known values.
577 /// Unlike `generate_known_values_enum`, this takes the type name directly
578 /// and uses fragment extraction for NSID#fragment values.
579 pub(super) fn generate_inline_known_values_enum(
580 &self,
581 type_name: &str,
582 string: &LexString<'static>,
583 ) -> Result<TokenStream> {
584 let ident = syn::Ident::new(type_name, proc_macro2::Span::call_site());
585
586 let known_values = string.known_values.as_ref().unwrap();
587 let mut variants = Vec::new();
588 let mut from_str_arms = Vec::new();
589 let mut as_str_arms = Vec::new();
590
591 for value in known_values {
592 let value_str = value.as_ref();
593 // Use known_value_to_variant_name to extract fragment from NSID#fragment
594 let variant_name = known_value_to_variant_name(value_str);
595 let variant_ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site());
596
597 variants.push(quote! {
598 #variant_ident
599 });
600
601 from_str_arms.push(quote! {
602 #value_str => Self::#variant_ident
603 });
604
605 as_str_arms.push(quote! {
606 Self::#variant_ident => #value_str
607 });
608 }
609
610 let doc = self.generate_doc_comment(string.description.as_ref());
611
612 // Generate IntoStatic impl
613 let variant_info: Vec<(String, EnumVariantKind)> = known_values
614 .iter()
615 .map(|value| {
616 let variant_name = known_value_to_variant_name(value.as_ref());
617 (variant_name, EnumVariantKind::Unit)
618 })
619 .chain(std::iter::once((
620 "Other".to_string(),
621 EnumVariantKind::Tuple,
622 )))
623 .collect();
624 let into_static_impl =
625 self.generate_into_static_for_enum(type_name, &variant_info, true, false);
626
627 Ok(quote! {
628 #doc
629 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
630 pub enum #ident<'a> {
631 #(#variants,)*
632 Other(jacquard_common::CowStr<'a>),
633 }
634
635 impl<'a> #ident<'a> {
636 pub fn as_str(&self) -> &str {
637 match self {
638 #(#as_str_arms,)*
639 Self::Other(s) => s.as_ref(),
640 }
641 }
642 }
643
644 impl<'a> From<&'a str> for #ident<'a> {
645 fn from(s: &'a str) -> Self {
646 match s {
647 #(#from_str_arms,)*
648 _ => Self::Other(jacquard_common::CowStr::from(s)),
649 }
650 }
651 }
652
653 impl<'a> From<String> for #ident<'a> {
654 fn from(s: String) -> Self {
655 match s.as_str() {
656 #(#from_str_arms,)*
657 _ => Self::Other(jacquard_common::CowStr::from(s)),
658 }
659 }
660 }
661
662 impl<'a> core::fmt::Display for #ident<'a> {
663 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
664 write!(f, "{}", self.as_str())
665 }
666 }
667
668 impl<'a> AsRef<str> for #ident<'a> {
669 fn as_ref(&self) -> &str {
670 self.as_str()
671 }
672 }
673
674 impl<'a> serde::Serialize for #ident<'a> {
675 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
676 where
677 S: serde::Serializer,
678 {
679 serializer.serialize_str(self.as_str())
680 }
681 }
682
683 impl<'de, 'a> serde::Deserialize<'de> for #ident<'a>
684 where
685 'de: 'a,
686 {
687 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
688 where
689 D: serde::Deserializer<'de>,
690 {
691 let s = <&'de str>::deserialize(deserializer)?;
692 Ok(Self::from(s))
693 }
694 }
695
696 impl<'a> Default for #ident<'a> {
697 fn default() -> Self {
698 Self::Other(Default::default())
699 }
700 }
701
702 #into_static_impl
703 })
704 }
705
706 /// Generate enum for integer with enum values
707 pub(super) fn generate_integer_enum(
708 &self,
709 nsid: &str,
710 def_name: &str,
711 integer: &LexInteger<'static>,
712 ) -> Result<TokenStream> {
713 let type_name = self.def_to_type_name(nsid, def_name);
714 let ident = syn::Ident::new(&type_name, proc_macro2::Span::call_site());
715
716 let enum_values = integer.r#enum.as_ref().unwrap();
717 let mut variants = Vec::new();
718 let mut from_i64_arms = Vec::new();
719 let mut to_i64_arms = Vec::new();
720
721 for value in enum_values {
722 let variant_name = format!("Value{}", value.abs());
723 let variant_ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site());
724
725 variants.push(quote! {
726 #[serde(rename = #value)]
727 #variant_ident
728 });
729
730 from_i64_arms.push(quote! {
731 #value => Self::#variant_ident
732 });
733
734 to_i64_arms.push(quote! {
735 Self::#variant_ident => #value
736 });
737 }
738
739 let doc = self.generate_doc_comment(integer.description.as_ref());
740
741 Ok(quote! {
742 #doc
743 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
744 pub enum #ident {
745 #(#variants,)*
746 #[serde(untagged)]
747 Other(i64),
748 }
749
750 impl #ident {
751 pub fn as_i64(&self) -> i64 {
752 match self {
753 #(#to_i64_arms,)*
754 Self::Other(n) => *n,
755 }
756 }
757 }
758
759 impl From<i64> for #ident {
760 fn from(n: i64) -> Self {
761 match n {
762 #(#from_i64_arms,)*
763 _ => Self::Other(n),
764 }
765 }
766 }
767
768 impl serde::Serialize for #ident {
769 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
770 where
771 S: serde::Serializer,
772 {
773 serializer.serialize_i64(self.as_i64())
774 }
775 }
776
777 impl<'de> serde::Deserialize<'de> for #ident {
778 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
779 where
780 D: serde::Deserializer<'de>,
781 {
782 let n = i64::deserialize(deserializer)?;
783 Ok(Self::from(n))
784 }
785 }
786 })
787 }
788
789 /// Generate IntoStatic impl for a struct
790 #[allow(dead_code)]
791 pub(super) fn generate_into_static_for_struct(
792 &self,
793 type_name: &str,
794 field_names: &[&str],
795 has_lifetime: bool,
796 has_extra_data: bool,
797 ) -> TokenStream {
798 let ident = syn::Ident::new(type_name, proc_macro2::Span::call_site());
799
800 let field_idents: Vec<_> = field_names
801 .iter()
802 .map(|name| make_ident(&name.to_snake_case()))
803 .collect();
804
805 if has_lifetime {
806 let field_conversions: Vec<_> = field_idents
807 .iter()
808 .map(|field| quote! { #field: self.#field.into_static() })
809 .collect();
810
811 let extra_data_conversion = if has_extra_data {
812 quote! { extra_data: self.extra_data.into_static(), }
813 } else {
814 quote! {}
815 };
816
817 quote! {
818 impl jacquard_common::IntoStatic for #ident<'_> {
819 type Output = #ident<'static>;
820
821 fn into_static(self) -> Self::Output {
822 #ident {
823 #(#field_conversions,)*
824 #extra_data_conversion
825 }
826 }
827 }
828 }
829 } else {
830 quote! {
831 impl jacquard_common::IntoStatic for #ident {
832 type Output = #ident;
833
834 fn into_static(self) -> Self::Output {
835 self
836 }
837 }
838 }
839 }
840 }
841
842 /// Generate IntoStatic impl for an enum
843 pub(super) fn generate_into_static_for_enum(
844 &self,
845 type_name: &str,
846 variant_info: &[(String, EnumVariantKind)],
847 has_lifetime: bool,
848 is_open: bool,
849 ) -> TokenStream {
850 let ident = syn::Ident::new(type_name, proc_macro2::Span::call_site());
851
852 if has_lifetime {
853 let variant_conversions: Vec<_> = variant_info
854 .iter()
855 .map(|(variant_name, kind)| {
856 let variant_ident = syn::Ident::new(variant_name, proc_macro2::Span::call_site());
857 match kind {
858 EnumVariantKind::Unit => {
859 quote! {
860 #ident::#variant_ident => #ident::#variant_ident
861 }
862 }
863 EnumVariantKind::Tuple => {
864 quote! {
865 #ident::#variant_ident(v) => #ident::#variant_ident(v.into_static())
866 }
867 }
868 EnumVariantKind::Struct(fields) => {
869 let field_idents: Vec<_> = fields
870 .iter()
871 .map(|f| make_ident(&f.to_snake_case()))
872 .collect();
873 let field_conversions: Vec<_> = field_idents
874 .iter()
875 .map(|f| quote! { #f: #f.into_static() })
876 .collect();
877 quote! {
878 #ident::#variant_ident { #(#field_idents,)* } => #ident::#variant_ident {
879 #(#field_conversions,)*
880 }
881 }
882 }
883 }
884 })
885 .collect();
886
887 let unknown_conversion = if is_open {
888 quote! {
889 #ident::Unknown(v) => #ident::Unknown(v.into_static()),
890 }
891 } else {
892 quote! {}
893 };
894
895 quote! {
896 impl jacquard_common::IntoStatic for #ident<'_> {
897 type Output = #ident<'static>;
898
899 fn into_static(self) -> Self::Output {
900 match self {
901 #(#variant_conversions,)*
902 #unknown_conversion
903 }
904 }
905 }
906 }
907 } else {
908 quote! {
909 impl jacquard_common::IntoStatic for #ident {
910 type Output = #ident;
911
912 fn into_static(self) -> Self::Output {
913 self
914 }
915 }
916 }
917 }
918 }
919}