rydantic-derive 0.4.0

Derive macros for Rydantic
Documentation
use crate::{attrs, field_value, fields, Mode};
use heck::ToSnakeCase;
use proc_macro2::{Span, TokenStream as Tokens};
use quote::{format_ident, quote, quote_spanned};
use std::collections::HashSet;
use syn::{spanned::Spanned, Data, DeriveInput, Fields, LitStr};

#[derive(Default)]
struct Options {
    name: Option<String>,
    primary_key: bool,
    auto_increment: bool,
    unique: bool,
    index: bool,
    skip: bool,
    references: Option<(String, String)>,
    on_delete: Option<String>,
}
fn identifier(value: &str, span: Span) -> syn::Result<()> {
    let mut chars = value.chars();
    if !chars
        .next()
        .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
        || !chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
    {
        return Err(syn::Error::new(span,"SQL identifiers must contain ASCII letters, digits or underscores and start with a letter or underscore"));
    }
    Ok(())
}
fn options(attributes: &[syn::Attribute], container: bool) -> syn::Result<Options> {
    let mut result = Options::default();
    let mut seen = HashSet::new();
    for attr in attributes.iter().filter(|a| a.path().is_ident("sql")) {
        attr.parse_nested_meta(|meta| {
            let name = meta
                .path
                .get_ident()
                .ok_or_else(|| meta.error("expected a SQL attribute name"))?
                .to_string();
            if !seen.insert(name.clone()) {
                return Err(meta.error("duplicate SQL attribute"));
            }
            if container {
                if name != "table" {
                    return Err(meta.error("SQL model attributes support table = \"...\""));
                }
                let value = meta.value()?.parse::<LitStr>()?;
                identifier(&value.value(), value.span())?;
                result.name = Some(value.value());
                return Ok(());
            }
            match name.as_str() {
                "rename" => {
                    let value = meta.value()?.parse::<LitStr>()?;
                    identifier(&value.value(), value.span())?;
                    result.name = Some(value.value());
                }
                "primary_key" => result.primary_key = true,
                "auto_increment" => result.auto_increment = true,
                "unique" => result.unique = true,
                "index" => result.index = true,
                "skip" => result.skip = true,
                "references" => {
                    let value = meta.value()?.parse::<LitStr>()?;
                    let text = value.value();
                    let Some((table, column)) = text.split_once('.') else {
                        return Err(meta.error("references requires table.column"));
                    };
                    identifier(table, value.span())?;
                    identifier(column, value.span())?;
                    result.references = Some((table.into(), column.into()));
                }
                "on_delete" => {
                    let value = meta.value()?.parse::<LitStr>()?;
                    if !matches!(
                        value.value().as_str(),
                        "no_action" | "restrict" | "cascade" | "set_null"
                    ) {
                        return Err(meta
                            .error("on_delete supports no_action, restrict, cascade or set_null"));
                    }
                    result.on_delete = Some(value.value());
                }
                _ => return Err(meta.error("unsupported SQL field attribute")),
            }
            Ok(())
        })?;
    }
    if result.auto_increment && !result.primary_key {
        return Err(syn::Error::new(
            Span::call_site(),
            "auto_increment requires primary_key",
        ));
    }
    if result.on_delete.is_some() && result.references.is_none() {
        return Err(syn::Error::new(
            Span::call_site(),
            "on_delete requires references",
        ));
    }
    if result.skip
        && (result.primary_key
            || result.auto_increment
            || result.unique
            || result.index
            || result.references.is_some()
            || result.name.is_some())
    {
        return Err(syn::Error::new(
            Span::call_site(),
            "skipped SQL fields cannot declare database column attributes",
        ));
    }
    Ok(result)
}

pub(super) fn expand(input: DeriveInput) -> syn::Result<Tokens> {
    if !input.generics.params.is_empty() {
        return Err(syn::Error::new(
            input.generics.span(),
            "SqlModel requires a concrete owned struct",
        ));
    }
    let Data::Struct(data) = &input.data else {
        return Err(syn::Error::new(
            input.span(),
            "SqlModel requires a struct with named fields",
        ));
    };
    if !matches!(data.fields, Fields::Named(_)) {
        return Err(syn::Error::new(
            data.fields.span(),
            "SqlModel requires named fields",
        ));
    }
    let model_options = options(&input.attrs, true)?;
    let name = &input.ident;
    let table = model_options
        .name
        .unwrap_or_else(|| name.to_string().trim_start_matches("r#").to_snake_case());
    identifier(&table, name.span())?;
    let rd = match proc_macro_crate::crate_name("rydantic")
        .map_err(|e| syn::Error::new(input.span(), e))?
    {
        proc_macro_crate::FoundCrate::Itself => quote!(::rydantic),
        proc_macro_crate::FoundCrate::Name(name) => {
            let id = format_ident!("{name}");
            quote!(::#id)
        }
    };
    let container = attrs::container(&input.attrs)?;
    let fields = fields(&data.fields, None, &container)?;
    let mut columns = Vec::new();
    let mut reads = Vec::new();
    let mut bindings = Vec::new();
    let mut included = Vec::new();
    let mut checks = Vec::new();
    let mut names = HashSet::new();
    let mut primary = None;
    for (field, original) in fields.iter().zip(data.fields.iter()) {
        let options = options(&original.attrs, false)?;
        let ident = &field.ident;
        let ty = &field.ty;
        if options.skip {
            let location = &field.input;
            let value = field_value(
                field,
                quote!(::core::option::Option::<#rd::Input<'_>>::None),
                &Mode {
                    rd: &rd,
                    borrow: false,
                },
            );
            reads.push(quote!(#ident:{
                let mut ctx=#rd::Context::new();
                let value=ctx.at(#location,|__rydantic_ctx|{#value});
                ctx.finish(value).map_err(|error|#rd::sqlx::Error::Decode(::std::boxed::Box::new(error)))?
            }));
            continue;
        }
        let column = options
            .name
            .unwrap_or_else(|| ident.to_string().trim_start_matches("r#").to_owned());
        identifier(&column, ident.span())?;
        if !names.insert(column.to_ascii_lowercase()) {
            return Err(syn::Error::new(ident.span(), "duplicate SQL column name"));
        }
        let index = columns.len();
        let pk = options.primary_key;
        let auto = options.auto_increment;
        let unique = options.unique;
        let indexed = options.index;
        if options.on_delete.as_deref() == Some("set_null") {
            checks.push(quote_spanned!(ty.span()=>
                const _: () = assert!(<#ty as #rd::sql::SqlValue>::NULLABLE && !#pk,
                    "on_delete = set_null requires a nullable non-primary-key column");
            ));
        }
        if pk {
            if primary.is_some() {
                return Err(syn::Error::new(
                    ident.span(),
                    "SqlModel supports one primary key",
                ));
            }
            primary = Some((ident, ty));
        }
        let references = if let Some((table, column)) = options.references {
            let delete = format_ident!(
                "{}",
                match options.on_delete.as_deref().unwrap_or("no_action") {
                    "cascade" => "Cascade",
                    "restrict" => "Restrict",
                    "set_null" => "SetNull",
                    _ => "NoAction",
                }
            );
            quote!(::core::option::Option::Some(#rd::sql::Reference{table:#table,column:#column,on_delete:#rd::sql::OnDelete::#delete}))
        } else {
            quote!(::core::option::Option::None)
        };
        columns.push(quote!(#rd::sql::Column{name:#column,sql_type:<#ty as #rd::sql::SqlValue>::SQL_TYPE,nullable:<#ty as #rd::sql::SqlValue>::NULLABLE && !#pk,primary_key:#pk,auto_increment:#auto,unique:#unique,index:#indexed,references:#references}));
        reads.push(quote!(#ident:<#ty as #rd::sql::SqlValue>::read(row,#column).map_err(|error|#rd::sqlx::Error::ColumnDecode{index:#column.to_owned(),source: ::std::boxed::Box::new(error)})?));
        let location = &field.input;
        bindings.push(quote!(#index=><#ty as #rd::sql::SqlValue>::bind(&self.#ident,query).map_err(|error|error.at(#location))));
        let include = if auto {
            quote!(!<#ty as #rd::sql::AutoKey>::needs_generation(&self.#ident))
        } else {
            quote!(true)
        };
        included.push(quote!(#index=>#include));
    }
    let Some((key, key_ty)) = primary else {
        return Err(syn::Error::new(
            name.span(),
            "SqlModel requires one #[sql(primary_key)] field",
        ));
    };
    Ok(quote! {
        #(#checks)*
        impl #rd::SqlModel for #name {
            type Key=<#key_ty as #rd::sql::Key>::Value;
            const TABLE:&'static str=#table;
            const COLUMNS:&'static [#rd::sql::Column]=&[#(#columns),*];
            fn key(&self)->::core::option::Option<&Self::Key>{<#key_ty as #rd::sql::Key>::value(&self.#key)}
            fn decode_row(row:&#rd::sqlx::sqlite::SqliteRow)->::core::result::Result<Self,#rd::sqlx::Error>{Ok(Self{#(#reads),*})}
            fn bind_column(&self,index:usize,query:&mut #rd::sqlx::QueryBuilder<#rd::sqlx::Sqlite>)->::core::result::Result<(),#rd::SqlError>{match index{#(#bindings,)* _=>unreachable!("generated SQL column index")}}
            fn include_insert_column(&self,index:usize)->bool{match index{#(#included,)* _=>false}}
        }
        impl<'__rydantic_row> #rd::sqlx::FromRow<'__rydantic_row,#rd::sqlx::sqlite::SqliteRow> for #name {
            fn from_row(row:&'__rydantic_row #rd::sqlx::sqlite::SqliteRow)->::core::result::Result<Self,#rd::sqlx::Error>{
                let value=<Self as #rd::SqlModel>::decode_row(row)?;
                <Self as #rd::Model>::validate(&value).map_err(|error|#rd::sqlx::Error::Decode(::std::boxed::Box::new(error)))?;
                Ok(value)
            }
        }
    })
}