A better Rust ATProto crate
1//! Builder struct generation
2//!
3//! Generates the builder struct with State generic parameter and constructor methods.
4
5use crate::codegen::builder_gen::BuilderSchema;
6use heck::ToSnakeCase;
7use proc_macro2::TokenStream;
8use quote::{format_ident, quote};
9
10/// Generate the complete builder struct including constructors
11pub fn generate_builder_struct(
12 codegen: &crate::codegen::CodeGenerator,
13 nsid: &str,
14 type_name: &str,
15 schema: &BuilderSchema,
16 has_lifetime: bool,
17 resolved: &crate::codegen::prettify::ResolvedImports,
18) -> TokenStream {
19 let builder_name = format_ident!("{}Builder", type_name);
20 let state_mod_name = format_ident!("{}_state", type_name.to_snake_case());
21 let type_ident = format_ident!("{}", type_name);
22
23 // Generate field declarations
24 let field_decls = generate_field_declarations(codegen, nsid, type_name, schema, resolved);
25
26 // Generate lifetime generic if needed
27 let lifetime_generic = if has_lifetime {
28 quote! { <'a> }
29 } else {
30 quote! {}
31 };
32
33 let lifetime_param = if has_lifetime {
34 quote! { 'a, }
35 } else {
36 quote! {}
37 };
38
39 let phantom = resolved.phantom_data();
40 let phantom_field = if has_lifetime {
41 quote! {
42 _lifetime: #phantom<&'a ()>,
43 }
44 } else {
45 quote! {}
46 };
47
48 // Generate Struct::new() constructor on original type
49 let struct_constructor = {
50 quote! {
51 impl #lifetime_generic #type_ident #lifetime_generic {
52 /// Create a new builder for this type
53 pub fn new() -> #builder_name<#lifetime_param #state_mod_name::Empty> {
54 #builder_name::new()
55 }
56 }
57 }
58 };
59
60 // Generate Builder::new() constructor
61 let builder_constructor = generate_builder_constructor(
62 &builder_name,
63 schema,
64 has_lifetime,
65 &state_mod_name,
66 resolved,
67 );
68
69 quote! {
70 /// Builder for constructing an instance of this type
71 pub struct #builder_name<#lifetime_param S: #state_mod_name::State> {
72 #field_decls
73 #phantom_field
74 }
75
76 #struct_constructor
77 #builder_constructor
78 }
79}
80
81/// Generate field declarations for the builder struct
82/// All fields are stored in a single tuple of Options
83fn generate_field_declarations(
84 codegen: &crate::codegen::CodeGenerator,
85 nsid: &str,
86 type_name: &str,
87 schema: &BuilderSchema,
88 resolved: &crate::codegen::prettify::ResolvedImports,
89) -> TokenStream {
90 let property_names = schema.property_names();
91 let field_types: Vec<_> = property_names
92 .iter()
93 .map(|field_name| {
94 let field_name_str: &str = field_name.as_ref();
95 let rust_type = match schema {
96 BuilderSchema::Object(obj) => {
97 let field_type = &obj.properties[field_name_str];
98 codegen
99 .property_to_rust_type(
100 nsid,
101 type_name,
102 field_name_str,
103 field_type,
104 &resolved,
105 )
106 .unwrap_or_else(|_| quote! { () })
107 }
108 BuilderSchema::Parameters(params) => {
109 let field_type = ¶ms.properties[field_name_str];
110 get_params_rust_type(codegen, field_type, &resolved)
111 }
112 };
113
114 {
115 let opt = resolved.option_type(rust_type);
116 quote! { #opt, }
117 }
118 })
119 .collect();
120
121 let phantom = resolved.phantom_data();
122 if field_types.is_empty() {
123 // No fields - empty tuple
124 quote! {}
125 } else {
126 quote! {
127 _state: #phantom<fn() -> S>,
128 _fields: ( #(#field_types)* ),
129 }
130 }
131}
132
133/// Get Rust type for XRPC parameter property
134pub(super) fn get_params_rust_type(
135 codegen: &crate::codegen::CodeGenerator,
136 field_type: &crate::lexicon::LexXrpcParametersProperty<'static>,
137 resolved: &crate::codegen::prettify::ResolvedImports,
138) -> TokenStream {
139 use crate::codegen::prettify::CommonType;
140 use crate::lexicon::LexXrpcParametersProperty;
141
142 match field_type {
143 LexXrpcParametersProperty::Boolean(_) => quote! { bool },
144 LexXrpcParametersProperty::Integer(_) => quote! { i64 },
145 LexXrpcParametersProperty::String(s) => codegen.string_to_rust_type(s, resolved),
146 LexXrpcParametersProperty::Unknown(_) => resolved.type_tokens(&CommonType::Data),
147 LexXrpcParametersProperty::Array(arr) => {
148 let item_type = match &arr.items {
149 crate::lexicon::LexPrimitiveArrayItem::Boolean(_) => quote! { bool },
150 crate::lexicon::LexPrimitiveArrayItem::Integer(_) => quote! { i64 },
151 crate::lexicon::LexPrimitiveArrayItem::String(s) => {
152 codegen.string_to_rust_type(s, resolved)
153 }
154 crate::lexicon::LexPrimitiveArrayItem::Unknown(_) => {
155 resolved.type_tokens(&CommonType::Data)
156 }
157 };
158 quote! { Vec<#item_type> }
159 }
160 }
161}
162
163/// Generate Builder::new() constructor with field initialization
164fn generate_builder_constructor(
165 builder_name: &syn::Ident,
166 schema: &BuilderSchema,
167 has_lifetime: bool,
168 state_mod_name: &syn::Ident,
169 resolved: &crate::codegen::prettify::ResolvedImports,
170) -> TokenStream {
171 let phantom = resolved.phantom_data();
172 let lifetime_param = if has_lifetime {
173 quote! { 'a, }
174 } else {
175 quote! {}
176 };
177
178 // Initialize all fields as None in the tuple
179 let property_names = schema.property_names();
180 let none_values = property_names.iter().map(|_| quote! { None, });
181
182 let (phantom_init, tuple_init) = if property_names.is_empty() {
183 (quote! {}, quote! {})
184 } else {
185 (
186 quote! {
187 _state: #phantom,
188 },
189 quote! {
190 _fields: ( #(#none_values)* ),
191 },
192 )
193 };
194
195 let phantom_lifetime = if has_lifetime {
196 quote! {
197 _lifetime: #phantom,
198 }
199 } else {
200 quote! {}
201 };
202
203 quote! {
204 impl<#lifetime_param> #builder_name<#lifetime_param #state_mod_name::Empty> {
205 /// Create a new builder with all fields unset
206 pub fn new() -> Self {
207 #builder_name {
208 #phantom_init
209 #tuple_init
210 #phantom_lifetime
211 }
212 }
213 }
214 }
215}