mool-macros-impl 0.1.1

Internal macro implementation shared by Mool and framework wrappers
Documentation
//! Parsed model for `SqlEnum` code generation.

use std::collections::HashSet;

use syn::{Data, DeriveInput, Error, Fields, Ident, Result};

use super::attrs::{IntRepr, Storage, parse_enum_attrs, parse_variant_attrs};
use super::rename::{RenameRule, default_sql_name};

#[derive(Debug)]
pub struct ParsedSqlEnum {
    pub ident: Ident,
    pub storage: Storage,
    pub repr: IntRepr,
    pub sql_name: String,
    pub variants: Vec<SqlEnumVariant>,
}

#[derive(Debug)]
pub struct SqlEnumVariant {
    pub ident: Ident,
    pub label: String,
    pub code: Option<i64>,
}

impl ParsedSqlEnum {
    pub fn from_input(input: &DeriveInput) -> Result<Self> {
        let enum_attrs = parse_enum_attrs(&input.attrs)?;
        let storage = enum_attrs.storage.unwrap_or(Storage::Text);
        validate_container_attrs(storage, enum_attrs.repr)?;
        let repr = enum_attrs.repr.unwrap_or(IntRepr::I32);
        let rename_all = enum_attrs.rename_all.unwrap_or(RenameRule::Snake);
        let sql_name = enum_attrs
            .name
            .unwrap_or_else(|| default_sql_name(&input.ident.to_string()));
        let variants = parse_variants(input, storage, rename_all)?;
        validate_variants(&variants, storage)?;
        Ok(Self {
            ident: input.ident.clone(),
            storage,
            repr,
            sql_name,
            variants,
        })
    }
}

fn validate_container_attrs(storage: Storage, repr: Option<IntRepr>) -> Result<()> {
    if repr.is_some() && storage != Storage::Int {
        return Err(Error::new(
            proc_macro2::Span::call_site(),
            "repr is only valid with storage = \"int\"",
        ));
    }
    Ok(())
}

fn parse_variants(
    input: &DeriveInput,
    storage: Storage,
    rename_all: RenameRule,
) -> Result<Vec<SqlEnumVariant>> {
    let Data::Enum(data) = &input.data else {
        return Err(Error::new_spanned(
            &input.ident,
            "SqlEnum can only be derived for enums",
        ));
    };
    if data.variants.is_empty() {
        return Err(Error::new_spanned(
            &input.ident,
            "SqlEnum requires at least one variant",
        ));
    }
    data.variants
        .iter()
        .map(|variant| {
            if !matches!(variant.fields, Fields::Unit) {
                return Err(Error::new_spanned(
                    variant,
                    "SqlEnum supports fieldless variants only",
                ));
            }
            let attrs = parse_variant_attrs(&variant.attrs)?;
            validate_variant_attrs(storage, attrs.value.as_ref(), attrs.code)?;
            let label = attrs
                .value
                .unwrap_or_else(|| rename_all.apply(&variant.ident.to_string()));
            Ok(SqlEnumVariant {
                ident: variant.ident.clone(),
                label,
                code: attrs.code,
            })
        })
        .collect()
}

fn validate_variant_attrs(
    storage: Storage,
    value: Option<&String>,
    code: Option<i64>,
) -> Result<()> {
    if code.is_some() && storage != Storage::Int {
        return Err(Error::new(
            proc_macro2::Span::call_site(),
            "code is only valid with storage = \"int\"",
        ));
    }
    if value.is_some() && storage == Storage::Int {
        return Err(Error::new(
            proc_macro2::Span::call_site(),
            "value is not valid with storage = \"int\"",
        ));
    }
    Ok(())
}

fn validate_variants(variants: &[SqlEnumVariant], storage: Storage) -> Result<()> {
    let mut labels = HashSet::new();
    for variant in variants {
        if !labels.insert(variant.label.as_str()) {
            return Err(Error::new_spanned(
                &variant.ident,
                format!("duplicate SQL enum label '{}'", variant.label),
            ));
        }
    }
    if storage == Storage::Int {
        let mut codes = HashSet::new();
        for variant in variants {
            let Some(code) = variant.code else {
                return Err(Error::new_spanned(
                    &variant.ident,
                    "storage = \"int\" requires every variant to define code",
                ));
            };
            if !codes.insert(code) {
                return Err(Error::new_spanned(
                    &variant.ident,
                    format!("duplicate SQL enum code '{code}'"),
                ));
            }
        }
    }
    Ok(())
}