satay-codegen 0.1.1

Generate Rust client code from OpenAPI 3.1 documents
Documentation
use proc_macro2::{Literal, TokenStream};
use quote::quote;
use syn::parse_quote;

use crate::model::{ConstrainedType, FloatLimit, IntegerLimit, Validation};

use super::super::{doc_attrs, ident, lit_str, rust_type};

pub(super) fn render_constrained_type(constrained_type: &ConstrainedType) -> syn::ItemStruct {
    let name = ident(&constrained_type.rust_name);
    let inner = rust_type(&constrained_type.inner);
    let validation = render_validation(&constrained_type.validation);
    let derives = nutype_derives(&constrained_type.validation);
    let docs = doc_attrs(constrained_type.description.as_deref());

    parse_quote!(
        #(#docs)*
        #[nutype::nutype(
            #validation,
            derive(#(#derives),*),
            cfg_attr(feature = "serde", derive(Serialize, Deserialize)),
        )]
        pub struct #name(#inner);
    )
}

fn render_validation(validation: &Validation) -> TokenStream {
    match validation {
        Validation::String {
            min_length,
            max_length,
            pattern,
        } => {
            let mut validators = vec![];
            if let Some(pattern) = pattern {
                let pattern = lit_str(pattern);
                validators.push(quote!(regex = #pattern));
            }
            if let Some(min_length) = min_length {
                let min_length = Literal::u64_unsuffixed(*min_length);
                validators.push(quote!(len_char_min = #min_length));
            }
            if let Some(max_length) = max_length {
                let max_length = Literal::u64_unsuffixed(*max_length);
                validators.push(quote!(len_char_max = #max_length));
            }
            quote!(validate(#(#validators),*))
        }
        Validation::Integer { minimum, maximum } => {
            let mut validators = vec![];
            if let Some(minimum) = minimum {
                validators.push(integer_limit_validator(
                    "greater",
                    "greater_or_equal",
                    *minimum,
                ));
            }
            if let Some(maximum) = maximum {
                validators.push(integer_limit_validator("less", "less_or_equal", *maximum));
            }
            quote!(validate(#(#validators),*))
        }
        Validation::Number { minimum, maximum } => {
            let mut validators = vec![quote!(finite)];
            if let Some(minimum) = minimum {
                validators.push(float_limit_validator(
                    "greater",
                    "greater_or_equal",
                    *minimum,
                ));
            }
            if let Some(maximum) = maximum {
                validators.push(float_limit_validator("less", "less_or_equal", *maximum));
            }
            quote!(validate(#(#validators),*))
        }
        Validation::Array {
            min_items,
            max_items,
        } => {
            let predicate = array_predicate(*min_items, *max_items);
            quote!(validate(#predicate))
        }
    }
}

fn nutype_derives(validation: &Validation) -> Vec<TokenStream> {
    let mut derives = vec![quote!(Debug), quote!(Clone)];

    match validation {
        Validation::String { .. } => {
            derives.extend([
                quote!(PartialEq),
                quote!(Eq),
                quote!(PartialOrd),
                quote!(Ord),
                quote!(AsRef),
                quote!(Deref),
                quote!(TryFrom),
                quote!(Into),
                quote!(Display),
                quote!(Hash),
            ]);
        }
        Validation::Integer { .. } => {
            derives.extend([
                quote!(Copy),
                quote!(PartialEq),
                quote!(Eq),
                quote!(PartialOrd),
                quote!(Ord),
                quote!(AsRef),
                quote!(Deref),
                quote!(TryFrom),
                quote!(Into),
                quote!(Display),
            ]);
        }
        Validation::Number { .. } => {
            derives.extend([
                quote!(Copy),
                quote!(PartialEq),
                quote!(PartialOrd),
                quote!(AsRef),
                quote!(Deref),
                quote!(TryFrom),
                quote!(Into),
                quote!(Display),
            ]);
        }
        Validation::Array { .. } => {
            derives.extend([
                quote!(PartialEq),
                quote!(AsRef),
                quote!(Deref),
                quote!(TryFrom),
                quote!(Into),
            ]);
        }
    }

    derives
}

fn integer_limit_validator(
    exclusive_name: &str,
    inclusive_name: &str,
    limit: IntegerLimit,
) -> TokenStream {
    let name = ident(if limit.exclusive {
        exclusive_name
    } else {
        inclusive_name
    });
    let value = Literal::i128_unsuffixed(limit.value);
    quote!(#name = #value)
}

fn float_limit_validator(
    exclusive_name: &str,
    inclusive_name: &str,
    limit: FloatLimit,
) -> TokenStream {
    let name = ident(if limit.exclusive {
        exclusive_name
    } else {
        inclusive_name
    });
    let value = Literal::f64_unsuffixed(limit.value);
    quote!(#name = #value)
}

fn array_predicate(min_items: Option<u64>, max_items: Option<u64>) -> TokenStream {
    match (min_items, max_items) {
        (Some(min_items), Some(max_items)) => {
            let min_items = Literal::u64_unsuffixed(min_items);
            let max_items = Literal::u64_unsuffixed(max_items);
            quote!(predicate = |items| items.len() >= #min_items && items.len() <= #max_items)
        }
        (Some(min_items), None) => {
            let min_items = Literal::u64_unsuffixed(min_items);
            quote!(predicate = |items| items.len() >= #min_items)
        }
        (None, Some(max_items)) => {
            let max_items = Literal::u64_unsuffixed(max_items);
            quote!(predicate = |items| items.len() <= #max_items)
        }
        (None, None) => unreachable!("array validation requires at least one bound"),
    }
}