A better Rust ATProto crate
at main 195 lines 6.2 kB view raw
1//! Setter generation for builder fields 2//! 3//! Generates typestate-constrained setters for required fields and 4//! flexible setters for optional fields. 5 6use crate::codegen::builder_gen::BuilderSchema; 7use crate::codegen::builder_gen::state_mod::RequiredField; 8use crate::codegen::utils::make_ident; 9use heck::{ToPascalCase, ToSnakeCase}; 10use jacquard_common::smol_str::SmolStr; 11use proc_macro2::TokenStream; 12use quote::{format_ident, quote}; 13 14/// Generate all setters for a builder 15pub fn generate_setters( 16 codegen: &crate::codegen::CodeGenerator, 17 nsid: &str, 18 type_name: &str, 19 schema: &BuilderSchema, 20 required_fields: &[RequiredField], 21 has_lifetime: bool, 22) -> TokenStream { 23 let builder_name = format_ident!("{}Builder", type_name); 24 let state_mod_name = format_ident!("{}_state", type_name.to_snake_case()); 25 26 let required_set: std::collections::HashSet<&SmolStr> = 27 required_fields.iter().map(|f| &f.name_snake).collect(); 28 29 let property_names = schema.property_names(); 30 let mut setters = Vec::new(); 31 32 for (index, field_name) in property_names.iter().enumerate() { 33 let is_required = required_set.contains(&SmolStr::new(field_name.to_snake_case())); 34 35 if is_required { 36 // Generate required field setter with typestate transition 37 let setter = generate_required_setter( 38 &builder_name, 39 &state_mod_name, 40 codegen, 41 nsid, 42 type_name, 43 field_name, 44 schema, 45 has_lifetime, 46 index, 47 ); 48 setters.push(setter); 49 } else { 50 // Generate optional field setters (.field() and .maybe_field()) 51 let setter = generate_optional_setter( 52 &builder_name, 53 &state_mod_name, 54 codegen, 55 nsid, 56 type_name, 57 field_name, 58 schema, 59 has_lifetime, 60 index, 61 ); 62 setters.push(setter); 63 } 64 } 65 66 quote! { 67 #(#setters)* 68 } 69} 70 71/// Generate a setter for a required field with typestate transition 72fn generate_required_setter( 73 builder_name: &syn::Ident, 74 state_mod_name: &syn::Ident, 75 codegen: &crate::codegen::CodeGenerator, 76 nsid: &str, 77 type_name: &str, 78 field_name: &SmolStr, 79 schema: &BuilderSchema, 80 has_lifetime: bool, 81 index: usize, 82) -> TokenStream { 83 let field_snake = make_ident(&field_name.to_snake_case()); 84 let field_pascal = format_ident!("{}", field_name.to_pascal_case()); 85 let transition_type = format_ident!("Set{}", field_name.to_pascal_case()); 86 let index = syn::Index::from(index); 87 88 // Get the Rust type for this field 89 let rust_type = get_field_rust_type(codegen, nsid, type_name, field_name, schema); 90 91 let lifetime_param = if has_lifetime { 92 quote! { 'a, } 93 } else { 94 quote! {} 95 }; 96 97 let phantom_lifetime = if has_lifetime { 98 quote! { _phantom: ::core::marker::PhantomData, } 99 } else { 100 quote! {} 101 }; 102 103 let doc = format!(" Set the `{}` field (required)", field_name); 104 105 quote! { 106 impl<#lifetime_param S> #builder_name<#lifetime_param S> 107 where 108 S: #state_mod_name::State, 109 S::#field_pascal: #state_mod_name::IsUnset, 110 { 111 #[doc = #doc] 112 pub fn #field_snake( 113 mut self, 114 value: impl Into<#rust_type>, 115 ) -> #builder_name<#lifetime_param #state_mod_name::#transition_type<S>> { 116 self.__unsafe_private_named.#index = ::core::option::Option::Some(value.into()); 117 #builder_name { 118 _phantom_state: ::core::marker::PhantomData, 119 __unsafe_private_named: self.__unsafe_private_named, 120 #phantom_lifetime 121 } 122 } 123 } 124 } 125} 126 127/// Generate setters for an optional field 128fn generate_optional_setter( 129 builder_name: &syn::Ident, 130 state_mod_name: &syn::Ident, 131 codegen: &crate::codegen::CodeGenerator, 132 nsid: &str, 133 type_name: &str, 134 field_name: &SmolStr, 135 schema: &BuilderSchema, 136 has_lifetime: bool, 137 index: usize, 138) -> TokenStream { 139 let field_snake = make_ident(&field_name.to_snake_case()); 140 let maybe_field_snake = format_ident!("maybe_{}", field_name.to_snake_case()); 141 let index = syn::Index::from(index); 142 143 // Get the Rust type for this field 144 let rust_type = get_field_rust_type(codegen, nsid, type_name, field_name, schema); 145 146 let lifetime_param = if has_lifetime { 147 quote! { 'a, } 148 } else { 149 quote! {} 150 }; 151 152 let doc_field = format!(" Set the `{}` field (optional)", field_name); 153 let doc_maybe = format!( 154 " Set the `{}` field to an Option value (optional)", 155 field_name 156 ); 157 158 quote! { 159 impl<#lifetime_param S: #state_mod_name::State> #builder_name<#lifetime_param S> { 160 #[doc = #doc_field] 161 pub fn #field_snake(mut self, value: impl Into<Option<#rust_type>>) -> Self { 162 self.__unsafe_private_named.#index = value.into(); 163 self 164 } 165 166 #[doc = #doc_maybe] 167 pub fn #maybe_field_snake(mut self, value: Option<#rust_type>) -> Self { 168 self.__unsafe_private_named.#index = value; 169 self 170 } 171 } 172 } 173} 174 175/// Get the Rust type for a field 176fn get_field_rust_type( 177 codegen: &crate::codegen::CodeGenerator, 178 nsid: &str, 179 type_name: &str, 180 field_name: &SmolStr, 181 schema: &BuilderSchema, 182) -> TokenStream { 183 match schema { 184 BuilderSchema::Object(obj) => { 185 let field_type = &obj.properties[field_name]; 186 codegen 187 .property_to_rust_type(nsid, type_name, field_name, field_type) 188 .unwrap_or_else(|_| quote! { () }) 189 } 190 BuilderSchema::Parameters(params) => { 191 let field_type = &params.properties[field_name]; 192 super::builder_struct::get_params_rust_type(codegen, field_type) 193 } 194 } 195}