codex_experimental_api_macros/
lib.rs1use proc_macro::TokenStream;
2use proc_macro2::Span;
3use quote::quote;
4use syn::Attribute;
5use syn::Data;
6use syn::DataEnum;
7use syn::DataStruct;
8use syn::DeriveInput;
9use syn::Field;
10use syn::Fields;
11use syn::Ident;
12use syn::LitStr;
13use syn::Type;
14use syn::parse_macro_input;
15
16#[proc_macro_derive(ExperimentalApi, attributes(experimental))]
17pub fn derive_experimental_api(input: TokenStream) -> TokenStream {
18 let input = parse_macro_input!(input as DeriveInput);
19 match &input.data {
20 Data::Struct(data) => derive_for_struct(&input, data),
21 Data::Enum(data) => derive_for_enum(&input, data),
22 Data::Union(_) => {
23 syn::Error::new_spanned(&input.ident, "ExperimentalApi does not support unions")
24 .to_compile_error()
25 .into()
26 }
27 }
28}
29
30fn derive_for_struct(input: &DeriveInput, data: &DataStruct) -> TokenStream {
31 let name = &input.ident;
32 let type_name_lit = LitStr::new(&name.to_string(), Span::call_site());
33
34 let (checks, experimental_fields, registrations) = match &data.fields {
35 Fields::Named(named) => {
36 let mut checks = Vec::new();
37 let mut experimental_fields = Vec::new();
38 let mut registrations = Vec::new();
39 for field in &named.named {
40 if let Some(reason) = experimental_reason(&field.attrs) {
41 let expr = experimental_presence_expr(field, false);
42 checks.push(quote! {
43 if #expr {
44 return Some(#reason);
45 }
46 });
47
48 if let Some(field_name) = field_serialized_name(field) {
49 let field_name_lit = LitStr::new(&field_name, Span::call_site());
50 experimental_fields.push(quote! {
51 crate::experimental_api::ExperimentalField {
52 type_name: #type_name_lit,
53 field_name: #field_name_lit,
54 reason: #reason,
55 }
56 });
57 registrations.push(quote! {
58 ::inventory::submit! {
59 crate::experimental_api::ExperimentalField {
60 type_name: #type_name_lit,
61 field_name: #field_name_lit,
62 reason: #reason,
63 }
64 }
65 });
66 }
67 } else if has_nested_experimental(field) {
68 let Some(ident) = field.ident.as_ref() else {
69 continue;
70 };
71 checks.push(quote! {
72 if let Some(reason) =
73 crate::experimental_api::ExperimentalApi::experimental_reason(&self.#ident)
74 {
75 return Some(reason);
76 }
77 });
78 }
79 }
80 (checks, experimental_fields, registrations)
81 }
82 Fields::Unnamed(unnamed) => {
83 let mut checks = Vec::new();
84 let mut experimental_fields = Vec::new();
85 let mut registrations = Vec::new();
86 for (index, field) in unnamed.unnamed.iter().enumerate() {
87 if let Some(reason) = experimental_reason(&field.attrs) {
88 let expr = index_presence_expr(index, &field.ty);
89 checks.push(quote! {
90 if #expr {
91 return Some(#reason);
92 }
93 });
94
95 let field_name_lit = LitStr::new(&index.to_string(), Span::call_site());
96 experimental_fields.push(quote! {
97 crate::experimental_api::ExperimentalField {
98 type_name: #type_name_lit,
99 field_name: #field_name_lit,
100 reason: #reason,
101 }
102 });
103 registrations.push(quote! {
104 ::inventory::submit! {
105 crate::experimental_api::ExperimentalField {
106 type_name: #type_name_lit,
107 field_name: #field_name_lit,
108 reason: #reason,
109 }
110 }
111 });
112 } else if has_nested_experimental(field) {
113 let index = syn::Index::from(index);
114 checks.push(quote! {
115 if let Some(reason) =
116 crate::experimental_api::ExperimentalApi::experimental_reason(&self.#index)
117 {
118 return Some(reason);
119 }
120 });
121 }
122 }
123 (checks, experimental_fields, registrations)
124 }
125 Fields::Unit => (Vec::new(), Vec::new(), Vec::new()),
126 };
127
128 let checks = if checks.is_empty() {
129 quote! { None }
130 } else {
131 quote! {
132 #(#checks)*
133 None
134 }
135 };
136
137 let experimental_fields = if experimental_fields.is_empty() {
138 quote! { &[] }
139 } else {
140 quote! { &[ #(#experimental_fields,)* ] }
141 };
142
143 let expanded = quote! {
144 #(#registrations)*
145
146 impl #name {
147 pub(crate) const EXPERIMENTAL_FIELDS: &'static [crate::experimental_api::ExperimentalField] =
148 #experimental_fields;
149 }
150
151 impl crate::experimental_api::ExperimentalApi for #name {
152 fn experimental_reason(&self) -> Option<&'static str> {
153 #checks
154 }
155 }
156 };
157 expanded.into()
158}
159
160fn derive_for_enum(input: &DeriveInput, data: &DataEnum) -> TokenStream {
161 let name = &input.ident;
162 let mut match_arms = Vec::new();
163
164 for variant in &data.variants {
165 let variant_name = &variant.ident;
166 let pattern = match &variant.fields {
167 Fields::Named(_) => quote!(Self::#variant_name { .. }),
168 Fields::Unnamed(_) => quote!(Self::#variant_name ( .. )),
169 Fields::Unit => quote!(Self::#variant_name),
170 };
171 let reason = experimental_reason(&variant.attrs);
172 if let Some(reason) = reason {
173 match_arms.push(quote! {
174 #pattern => Some(#reason),
175 });
176 } else {
177 match_arms.push(quote! {
178 #pattern => None,
179 });
180 }
181 }
182
183 let expanded = quote! {
184 impl crate::experimental_api::ExperimentalApi for #name {
185 fn experimental_reason(&self) -> Option<&'static str> {
186 match self {
187 #(#match_arms)*
188 }
189 }
190 }
191 };
192 expanded.into()
193}
194
195fn experimental_reason(attrs: &[Attribute]) -> Option<LitStr> {
196 attrs.iter().find_map(experimental_reason_attr)
197}
198
199fn experimental_reason_attr(attr: &Attribute) -> Option<LitStr> {
200 if !attr.path().is_ident("experimental") {
201 return None;
202 }
203
204 attr.parse_args::<LitStr>().ok()
205}
206
207fn has_nested_experimental(field: &Field) -> bool {
208 field.attrs.iter().any(experimental_nested_attr)
209}
210
211fn experimental_nested_attr(attr: &Attribute) -> bool {
212 if !attr.path().is_ident("experimental") {
213 return false;
214 }
215
216 attr.parse_args::<Ident>()
217 .is_ok_and(|ident| ident == "nested")
218}
219
220fn field_serialized_name(field: &Field) -> Option<String> {
221 let ident = field.ident.as_ref()?;
222 let name = ident.to_string();
223 Some(snake_to_camel(&name))
224}
225
226fn snake_to_camel(s: &str) -> String {
227 let mut out = String::with_capacity(s.len());
228 let mut upper = false;
229 for ch in s.chars() {
230 if ch == '_' {
231 upper = true;
232 continue;
233 }
234 if upper {
235 out.push(ch.to_ascii_uppercase());
236 upper = false;
237 } else {
238 out.push(ch);
239 }
240 }
241 out
242}
243
244fn experimental_presence_expr(
245 field: &Field,
246 tuple_struct: bool,
247) -> Option<proc_macro2::TokenStream> {
248 if tuple_struct {
249 return None;
250 }
251 let ident = field.ident.as_ref()?;
252 Some(presence_expr_for_access(quote!(self.#ident), &field.ty))
253}
254
255fn index_presence_expr(index: usize, ty: &Type) -> proc_macro2::TokenStream {
256 let index = syn::Index::from(index);
257 presence_expr_for_access(quote!(self.#index), ty)
258}
259
260fn presence_expr_for_access(
261 access: proc_macro2::TokenStream,
262 ty: &Type,
263) -> proc_macro2::TokenStream {
264 if option_inner(ty).is_some() {
265 return quote! { #access.is_some() };
266 }
267 if is_vec_like(ty) || is_map_like(ty) {
268 return quote! { !#access.is_empty() };
269 }
270 if is_bool(ty) {
271 return quote! { #access };
272 }
273 quote! { true }
274}
275
276fn option_inner(ty: &Type) -> Option<&Type> {
277 let Type::Path(type_path) = ty else {
278 return None;
279 };
280 let segment = type_path.path.segments.last()?;
281 if segment.ident != "Option" {
282 return None;
283 }
284 let syn::PathArguments::AngleBracketed(args) = &segment.arguments else {
285 return None;
286 };
287 args.args.iter().find_map(|arg| match arg {
288 syn::GenericArgument::Type(inner) => Some(inner),
289 _ => None,
290 })
291}
292
293fn is_vec_like(ty: &Type) -> bool {
294 type_last_ident(ty).is_some_and(|ident| ident == "Vec")
295}
296
297fn is_map_like(ty: &Type) -> bool {
298 type_last_ident(ty).is_some_and(|ident| ident == "HashMap" || ident == "BTreeMap")
299}
300
301fn is_bool(ty: &Type) -> bool {
302 type_last_ident(ty).is_some_and(|ident| ident == "bool")
303}
304
305fn type_last_ident(ty: &Type) -> Option<Ident> {
306 let Type::Path(type_path) = ty else {
307 return None;
308 };
309 type_path.path.segments.last().map(|seg| seg.ident.clone())
310}