elvwf

8 commits
Updated 2026-06-13 11:19:44
derive/src/struct
derive/src/struct/classify.rs
//! Warning: this file has been mostly generated by Claude.
//! I checked for its correctness but I'm not familiar with syn API
//! so things might not work as expected at some point. I'll try to add
//! enough test cases to be sure everything is ok

use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use syn::{spanned::Spanned, *};

use crate::utils::strip_lifetimes;

use super::OptSpec;

#[derive(Debug)]
pub(crate) enum FieldKind {
    Optional(FieldShape),
    Required(FieldShape),
}

impl FieldKind {
    pub fn sub(&self, opt: &Option<OptSpec>) -> TokenStream {
        match self {
            Self::Optional(_) => quote!(::opt),
            Self::Required(_) if opt.is_some() => quote!(::cond),
            _ => quote!(),
        }
    }

    pub fn opt(&self, opt: &Option<OptSpec>, ident_opt: &Ident) -> TokenStream {
        match self {
            Self::Optional(_) => quote!(, #ident_opt),
            Self::Required(_) if opt.is_some() => quote!(, #ident_opt),
            _ => quote!(),
        }
    }

    pub fn extra_val(&self, opt: &Option<OptSpec>) -> TokenStream {
        match self {
            Self::Required(_)
                if let Some(OptSpec {
                    condition: Some(c), ..
                }) = opt =>
            {
                quote!(, #c)
            }
            _ => quote!(),
        }
    }

    pub fn extra_ref(&self, opt: &Option<OptSpec>) -> TokenStream {
        match self {
            Self::Required(_)
                if let Some(OptSpec {
                    condition: Some(c), ..
                }) = opt =>
            {
                quote!(, &#c)
            }
            _ => quote!(),
        }
    }
}

#[derive(Debug)]
pub(crate) enum FieldShape {
    Scalar(Type),
    Slice(Type),
    Msg(Type),
}

impl FieldShape {
    pub fn ty(&self) -> &Type {
        match self {
            Self::Scalar(t) | Self::Slice(t) | Self::Msg(t) => t,
        }
    }

    pub fn root(&self) -> TokenStream {
        match self {
            Self::Scalar(_) => quote!(elvwf::scalar),
            Self::Slice(_) => quote!(elvwf::slice),
            Self::Msg(_) => quote!(elvwf::msg),
        }
    }

    pub fn by_ref(&self) -> bool {
        matches!(self, Self::Msg(_))
    }
}

impl ToTokens for FieldShape {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let (Self::Msg(ty) | Self::Slice(ty) | Self::Scalar(ty)) = self;
        ty.to_tokens(tokens);
    }
}

impl ToTokens for FieldKind {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let (Self::Optional(shape) | Self::Required(shape)) = self;
        shape.to_tokens(tokens);
    }
}

const SCALAR_IDENTS: &[&str] = &[
    "u8", "u16", "u32", "u64", "u128", "usize", "i8", "i16", "i32", "i64", "i128", "isize", "f32",
    "f64",
];

fn is_scalar(ty: &Type) -> bool {
    let Type::Path(TypePath { qself: None, path }) = ty else {
        return false;
    };

    let Some(ident) = path.get_ident() else {
        return false;
    };

    SCALAR_IDENTS.iter().any(|s| ident == s)
}

fn as_option(ty: &Type) -> Option<&Type> {
    let Type::Path(TypePath { qself: None, path }) = ty else {
        return None;
    };
    let seg = path.segments.last()?;
    if seg.ident != "Option" {
        return None;
    }

    let PathArguments::AngleBracketed(args) = &seg.arguments else {
        return None;
    };
    if args.args.len() != 1 {
        return None;
    }
    match args.args.first()? {
        GenericArgument::Type(inner) => Some(inner),
        _ => None,
    }
}

fn is_u8(ty: &Type) -> bool {
    matches!(ty, Type::Path(TypePath { qself: None, path }) if path.is_ident("u8"))
}

fn classify_shape(ty: &Type) -> Result<(FieldShape, FieldShape)> {
    if is_scalar(ty) {
        return Ok((
            FieldShape::Scalar(ty.clone()),
            FieldShape::Scalar(strip_lifetimes(ty)),
        ));
    }

    if let Type::Reference(TypeReference { elem, .. }) = ty {
        match &**elem {
            Type::Path(TypePath { qself: None, path })
                if path.is_ident("str") || path.is_ident("CStr") =>
            {
                return Ok((
                    FieldShape::Slice(ty.clone()),
                    FieldShape::Slice(strip_lifetimes(ty)),
                ));
            }
            Type::Slice(TypeSlice { elem, .. }) | Type::Array(TypeArray { elem, .. }) => {
                if !is_u8(elem) {
                    return Err(Error::new(
                        elem.span(),
                        "only `&[u8]` or `&[u8; N]` slices are supported",
                    ));
                }
                return Ok((
                    FieldShape::Slice(ty.clone()),
                    FieldShape::Slice(strip_lifetimes(ty)),
                ));
            }
            _ => {
                return Err(Error::new(
                    ty.span(),
                    "unsupported reference type: expected `&str`, `&CStr`, `&[u8]` or `&[u8; N]`",
                ));
            }
        }
    }

    Ok((
        FieldShape::Msg(ty.clone()),
        FieldShape::Msg(strip_lifetimes(ty)),
    ))
}

pub fn classify(ty: &Type) -> Result<(FieldKind, FieldKind)> {
    if let Some(inner) = as_option(ty) {
        if as_option(inner).is_some() {
            return Err(Error::new(
                inner.span(),
                "nested `Option<Option<…>>` is not supported",
            ));
        }
        Ok(classify_shape(inner)
            .map(|(kind, stripped)| (FieldKind::Optional(kind), FieldKind::Optional(stripped)))?)
    } else {
        Ok(classify_shape(ty)
            .map(|(kind, stripped)| (FieldKind::Required(kind), FieldKind::Required(stripped)))?)
    }
}