codex_app_server_sdk_macros/
lib.rs1use proc_macro::TokenStream;
2use quote::{ToTokens, quote};
3use syn::parse_quote;
4use syn::punctuated::Punctuated;
5use syn::{Attribute, DeriveInput, Item, Meta, Path, Token, parse_macro_input};
6
7#[proc_macro_derive(OpenAiSerializable)]
8pub fn derive_openai_serializable(input: TokenStream) -> TokenStream {
9 let input = parse_macro_input!(input as DeriveInput);
10 let ident = input.ident;
11 let generics = input.generics;
12 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
13
14 TokenStream::from(quote! {
15 impl #impl_generics ::codex_app_server_sdk::OpenAiSerializable for #ident #ty_generics #where_clause {
16 fn openai_output_schema() -> ::codex_app_server_sdk::__private_serde_json::Value {
17 ::codex_app_server_sdk::openai_json_schema_for::<Self>()
18 }
19 }
20 })
21}
22
23#[proc_macro_attribute]
24pub fn openai_type(attr: TokenStream, item: TokenStream) -> TokenStream {
25 if !attr.is_empty() {
26 return syn::Error::new(
27 proc_macro2::Span::call_site(),
28 "openai_type does not accept arguments",
29 )
30 .to_compile_error()
31 .into();
32 }
33
34 let mut item = parse_macro_input!(item as Item);
35 let attrs = match &mut item {
36 Item::Struct(item) => &mut item.attrs,
37 Item::Enum(item) => &mut item.attrs,
38 _ => {
39 return syn::Error::new_spanned(
40 item,
41 "openai_type can only be used on structs or enums",
42 )
43 .to_compile_error()
44 .into();
45 }
46 };
47
48 ensure_derives(attrs);
49 ensure_helper_attr(
50 attrs,
51 "serde",
52 parse_quote!(#[serde(crate = "::codex_app_server_sdk::serde")]),
53 );
54 ensure_helper_attr(
55 attrs,
56 "schemars",
57 parse_quote!(#[schemars(crate = "::codex_app_server_sdk::schemars")]),
58 );
59
60 TokenStream::from(quote!(#item))
61}
62
63fn ensure_derives(attrs: &mut Vec<Attribute>) {
64 let required: [(&str, Path); 4] = [
65 (
66 "Serialize",
67 parse_quote!(::codex_app_server_sdk::serde::Serialize),
68 ),
69 (
70 "Deserialize",
71 parse_quote!(::codex_app_server_sdk::serde::Deserialize),
72 ),
73 (
74 "JsonSchema",
75 parse_quote!(::codex_app_server_sdk::schemars::JsonSchema),
76 ),
77 (
78 "OpenAiSerializable",
79 parse_quote!(::codex_app_server_sdk::OpenAiSerializable),
80 ),
81 ];
82
83 let missing: Vec<Path> = required
84 .into_iter()
85 .filter_map(|(needle, path)| (!has_derive(attrs, needle)).then_some(path))
86 .collect();
87
88 if !missing.is_empty() {
89 attrs.push(parse_quote!(#[derive(#(#missing),*)]));
90 }
91}
92
93fn has_derive(attrs: &[Attribute], needle: &str) -> bool {
94 attrs.iter().any(|attr| {
95 if !attr.path().is_ident("derive") {
96 return false;
97 }
98
99 match &attr.meta {
100 Meta::List(list) => list
101 .parse_args_with(Punctuated::<Path, Token![,]>::parse_terminated)
102 .map(|paths| {
103 paths.iter().any(|path| {
104 path.segments
105 .last()
106 .map(|segment| segment.ident == needle)
107 .unwrap_or(false)
108 })
109 })
110 .unwrap_or(false),
111 _ => false,
112 }
113 })
114}
115
116fn ensure_helper_attr(attrs: &mut Vec<Attribute>, helper: &str, attr: Attribute) {
117 let has_helper_crate = attrs.iter().any(|existing| {
118 existing.path().is_ident(helper)
119 && existing
120 .meta
121 .to_token_stream()
122 .to_string()
123 .contains("crate")
124 });
125
126 if !has_helper_crate {
127 attrs.push(attr);
128 }
129}