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