A better Rust ATProto crate
at main 193 lines 6.1 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) -> 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 = &params.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}