A better Rust ATProto crate
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 = ¶ms.properties[field_name];
192 super::builder_struct::get_params_rust_type(codegen, field_type)
193 }
194 }
195}