Skip to main content

codex_app_server_sdk_macros/
lib.rs

1use 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}