satay-codegen 0.12.0

Generate Rust client code from OpenAPI 3.1 documents
Documentation
use crate::model::{Field, TypeRef};
use syn::parse_quote;

use super::super::{
    doc_attrs, ident, lit_str, parse_as_integer_serde_module, parse_as_string_serde_module,
    rust_field_type, rust_type,
};

pub fn render_struct(
    name: &str,
    description: Option<&str>,
    fields: &[Field],
    serde: bool,
) -> syn::ItemStruct {
    let attrs = struct_attrs(description, serde);
    let fields = fields
        .iter()
        .map(|field| render_struct_field(name, field, serde))
        .collect::<Vec<_>>();
    let name = ident(name);

    parse_quote!(
        #(#attrs)*
        pub struct #name {
            #(#fields),*
        }
    )
}

fn struct_attrs(description: Option<&str>, serde: bool) -> Vec<syn::Attribute> {
    let mut attrs = doc_attrs(description);
    attrs.push(parse_quote!(#[derive(Debug, Clone, PartialEq)]));
    if serde {
        attrs.push(parse_quote!(
            #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
        ));
    }
    attrs
}

fn render_struct_field(struct_name: &str, field: &Field, serde: bool) -> syn::Field {
    let name = ident(&field.rust_name);
    let ty = rust_field_type(
        &field.ty,
        field.required,
        field.treat_error_as_none || !field.none_if.is_empty(),
    );
    let attrs = field_attrs(struct_name, field, serde);

    parse_quote!(#(#attrs)* pub #name: #ty)
}

fn field_attrs(struct_name: &str, field: &Field, serde: bool) -> Vec<syn::Attribute> {
    let mut attrs = doc_attrs(field.description.as_deref());
    if !serde {
        return attrs;
    }

    let mut serde_attrs = vec![];
    let serde_default_name = field
        .rust_name
        .strip_prefix("r#")
        .unwrap_or(&field.rust_name);
    if serde_default_name != field.wire_name {
        let wire_name = lit_str(&field.wire_name);
        serde_attrs.push(quote::quote!(rename = #wire_name));
    }
    if !field.none_if.is_empty() {
        let deserialize = lit_str(&format!(
            "{struct_name}::{}",
            none_if_deserialize_name(field)
        ));
        let serialize = lit_str(&format!("{struct_name}::{}", none_if_serialize_name(field)));
        serde_attrs.push(quote::quote!(deserialize_with = #deserialize));
        serde_attrs.push(quote::quote!(serialize_with = #serialize));
    } else if field.treat_error_as_none {
        serde_attrs.push(quote::quote!(
            deserialize_with = "satay_runtime::treat_error_as_none::deserialize"
        ));
        serde_attrs.push(quote::quote!(
            serialize_with = "satay_runtime::treat_error_as_none::serialize"
        ));
    } else if let Some(module) = parsed_serde_module(field) {
        serde_attrs.push(quote::quote!(with = #module));
    }
    if !field.required || field.treat_error_as_none {
        serde_attrs.push(quote::quote!(default));
        serde_attrs.push(quote::quote!(skip_serializing_if = "Option::is_none"));
    }
    if !serde_attrs.is_empty() {
        attrs.push(parse_quote!(#[cfg_attr(feature = "serde", serde(#(#serde_attrs),*))]));
    }
    attrs
}

pub fn render_none_if_impl(name: &str, fields: &[Field]) -> Option<syn::ItemImpl> {
    let functions = fields
        .iter()
        .filter(|field| !field.none_if.is_empty())
        .flat_map(render_none_if_functions)
        .collect::<Vec<_>>();
    if functions.is_empty() {
        return None;
    }

    let name = ident(name);
    Some(parse_quote!(
        #[cfg(feature = "serde")]
        impl #name {
            #(#functions)*
        }
    ))
}

fn render_none_if_functions(field: &Field) -> [syn::ImplItemFn; 2] {
    let deserialize_name = ident(&none_if_deserialize_name(field));
    let serialize_name = ident(&none_if_serialize_name(field));
    let inner_ty = rust_type(field.ty.non_option());
    let module = match field.ty.non_option() {
        TypeRef::ParsedString(parse_as) => parse_as_string_serde_module(*parse_as),
        _ => unreachable!("validated none-if field must use string-backed parse-as"),
    };
    let deserialize_module = if !field.required || field.ty.is_option() {
        format!("{module}::option")
    } else {
        module.to_owned()
    };
    let deserialize_module = syn::parse_str::<syn::Path>(&deserialize_module)
        .expect("runtime serde module path is valid");
    let serialize_module =
        syn::parse_str::<syn::Path>(module).expect("runtime serde module path is valid");
    let none_if = field
        .none_if
        .iter()
        .map(|value| lit_str(value))
        .collect::<Vec<_>>();
    let canonical = none_if
        .first()
        .expect("validated none-if list is non-empty");

    [
        parse_quote!(
            fn #deserialize_name<'de, D>(
                deserializer: D,
            ) -> Result<Option<#inner_ty>, D::Error>
            where
                D: serde::Deserializer<'de>,
            {
                #deserialize_module::deserialize_none_if(
                    deserializer,
                    &[#(#none_if),*],
                )
            }
        ),
        parse_quote!(
            #[allow(clippy::ref_option)]
            fn #serialize_name<S>(
                value: &Option<#inner_ty>,
                serializer: S,
            ) -> Result<S::Ok, S::Error>
            where
                S: serde::Serializer,
            {
                #serialize_module::serialize_none_if(value, #canonical, serializer)
            }
        ),
    ]
}

fn none_if_deserialize_name(field: &Field) -> String {
    format!(
        "__satay_deserialize_{}_none_if",
        field
            .rust_name
            .strip_prefix("r#")
            .unwrap_or(&field.rust_name)
    )
}

fn none_if_serialize_name(field: &Field) -> String {
    format!(
        "__satay_serialize_{}_none_if",
        field
            .rust_name
            .strip_prefix("r#")
            .unwrap_or(&field.rust_name)
    )
}

fn parsed_serde_module(field: &Field) -> Option<syn::LitStr> {
    let module = match field.ty.non_option() {
        TypeRef::ParsedString(parse_as) => parse_as_string_serde_module(*parse_as),
        TypeRef::ParsedInteger(parse_as) => parse_as_integer_serde_module(*parse_as),
        _ => return None,
    };
    let module = if !field.required || field.ty.is_option() {
        format!("{module}::option")
    } else {
        module.to_owned()
    };
    Some(lit_str(&module))
}