A better Rust ATProto crate
1//! Build method generation for builders
2//!
3//! Generates the final build() method that constructs the target type
4//! once all required fields have been set.
5
6use crate::codegen::builder_gen::BuilderSchema;
7use crate::codegen::builder_gen::state_mod::RequiredField;
8use crate::codegen::utils::make_ident;
9use heck::ToSnakeCase;
10use jacquard_common::smol_str::SmolStr;
11use proc_macro2::TokenStream;
12use quote::{format_ident, quote};
13
14/// Generate the build() method for a builder
15pub fn generate_build_method(
16 type_name: &str,
17 schema: &BuilderSchema,
18 required_fields: &[RequiredField],
19 has_lifetime: bool,
20) -> TokenStream {
21 let builder_name = format_ident!("{}Builder", type_name);
22 let state_mod_name = format_ident!("{}_state", type_name.to_snake_case());
23 let type_ident = format_ident!("{}", type_name);
24
25 let lifetime_param = if has_lifetime {
26 quote! { 'a, }
27 } else {
28 quote! {}
29 };
30
31 let lifetime_generic = if has_lifetime {
32 quote! { <'a> }
33 } else {
34 quote! {}
35 };
36
37 // Generate where clauses for all required fields being IsSet
38 let where_clauses = required_fields.iter().map(|field| {
39 let field_pascal = format_ident!("{}", field.name_pascal.as_str());
40 quote! { S::#field_pascal: #state_mod_name::IsSet, }
41 });
42
43 let required_set: std::collections::HashSet<&SmolStr> =
44 required_fields.iter().map(|f| &f.name_snake).collect();
45
46 // Generate field extractions for struct construction
47 let property_names = schema.property_names();
48 let field_extractions: Vec<_> = property_names
49 .iter()
50 .enumerate()
51 .map(|(index, field_name)| {
52 let field_snake = make_ident(&field_name.to_snake_case());
53 let index = syn::Index::from(index);
54 let is_required = required_set.contains(&SmolStr::new(field_name.to_snake_case()));
55
56 if is_required {
57 // Required field: unwrap from Option (guaranteed Some by IsSet constraint)
58 quote! { #field_snake: self.__unsafe_private_named.#index.unwrap(), }
59 } else {
60 // Optional field: use directly
61 quote! { #field_snake: self.__unsafe_private_named.#index, }
62 }
63 })
64 .collect();
65
66 // For LexObject (records/objects), add extra_data field and build_with_data method
67 // LexXrpcParameters don't have extra_data
68 let (extra_data_field, build_with_data_method) = match schema {
69 BuilderSchema::Object(_) => {
70 let default_field = quote! { extra_data: Default::default(), };
71
72 let with_data_method = quote! {
73 /// Build the final struct with custom extra_data
74 pub fn build_with_data(
75 self,
76 extra_data: std::collections::BTreeMap<
77 jacquard_common::smol_str::SmolStr,
78 jacquard_common::types::value::Data #lifetime_generic
79 >,
80 ) -> #type_ident #lifetime_generic {
81 #type_ident {
82 #(#field_extractions)*
83 extra_data: Some(extra_data),
84 }
85 }
86 };
87
88 (default_field, with_data_method)
89 }
90 BuilderSchema::Parameters(_) => (quote! {}, quote! {}),
91 };
92
93 quote! {
94 impl<#lifetime_param S> #builder_name<#lifetime_param S>
95 where
96 S: #state_mod_name::State,
97 #(#where_clauses)*
98 {
99 /// Build the final struct
100 pub fn build(self) -> #type_ident #lifetime_generic {
101 #type_ident {
102 #(#field_extractions)*
103 #extra_data_field
104 }
105 }
106
107 #build_with_data_method
108 }
109 }
110}