ironic-macros 0.3.7

Procedural macros for the Ironic application framework
Documentation
use quote::quote;
use syn::{Data, DeriveInput, Fields, GenericArgument, LitStr, PathArguments, Type};

pub(crate) fn expand(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
    if !input.generics.params.is_empty() {
        return Err(syn::Error::new_spanned(
            input.generics,
            "OpenApiSchema does not yet support generic structs",
        ));
    }
    let name = input.ident;
    let Data::Struct(data) = input.data else {
        return Err(syn::Error::new_spanned(
            name,
            "OpenApiSchema can only be derived for structs",
        ));
    };
    let Fields::Named(fields) = data.fields else {
        return Err(syn::Error::new_spanned(
            name,
            "OpenApiSchema requires named fields",
        ));
    };

    let mut properties = Vec::new();
    let mut required = Vec::new();
    for field in fields.named {
        let ident = field.ident.expect("named fields have identifiers");
        let field_name = serde_rename(&field.attrs)?.unwrap_or_else(|| ident.to_string());
        let ty = field.ty;
        properties.push(quote! {
            properties.insert(
                #field_name.to_owned(),
                <#ty as ::ironic::OpenApiSchema>::openapi_schema(),
            );
        });
        if !is_option(&ty) {
            required.push(field_name);
        }
    }

    Ok(quote! {
        impl ::ironic::OpenApiSchema for #name {
            fn openapi_schema() -> ::ironic::__private::serde_json::Value {
                let mut properties = ::ironic::__private::serde_json::Map::new();
                #(#properties)*
                ::ironic::__private::serde_json::json!({
                    "type": "object",
                    "title": ::std::stringify!(#name),
                    "properties": properties,
                    "required": [#(#required),*]
                })
            }
        }
    })
}

fn is_option(ty: &Type) -> bool {
    let Type::Path(path) = ty else {
        return false;
    };
    path.path.segments.last().is_some_and(|segment| {
        segment.ident == "Option"
            && matches!(segment.arguments, PathArguments::AngleBracketed(ref arguments)
                if arguments.args.iter().any(|argument| matches!(argument, GenericArgument::Type(_))))
    })
}

fn serde_rename(attributes: &[syn::Attribute]) -> syn::Result<Option<String>> {
    let mut rename = None;
    for attribute in attributes {
        if !attribute.path().is_ident("serde") {
            continue;
        }
        attribute.parse_nested_meta(|meta| {
            if meta.path.is_ident("rename") {
                rename = Some(meta.value()?.parse::<LitStr>()?.value());
            }
            Ok(())
        })?;
    }
    Ok(rename)
}