A better Rust ATProto crate
at main 215 lines 6.7 kB view raw
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 = &params.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}