df-derive-macros 0.3.0

Procedural derive macro implementation for df-derive.
Documentation
use syn::{Ident, PathArguments, Type, TypePath};

use super::known_types::is_bare_str_type;
use super::path_match::{PathView, path_prefix_is_no_args};

pub(super) fn reject_unsupported_collection_type(current_type: &Type) -> Result<(), syn::Error> {
    if let Type::Path(type_path) = current_type
        && let Some(collection) = unsupported_collection_kind(type_path)
    {
        return Err(collection.diagnostic(current_type));
    }
    Ok(())
}

pub(super) fn reject_bare_duration(
    current_type: &Type,
    generic_params: &[Ident],
) -> Result<(), syn::Error> {
    if let Type::Path(type_path) = current_type
        && type_path.qself.is_none()
        && type_path.path.segments.len() == 1
        && let Some(segment) = type_path.path.segments.last()
        && segment.ident == "Duration"
        && matches!(segment.arguments, PathArguments::None)
        && !generic_params.iter().any(|p| p == &segment.ident)
    {
        return Err(syn::Error::new_spanned(
            current_type,
            "bare `Duration` is ambiguous; use `std::time::Duration`, \
             `core::time::Duration`, or `chrono::Duration` to disambiguate",
        ));
    }
    Ok(())
}

pub(super) fn reject_bare_unsized_leaf(current_type: &Type) -> Result<(), syn::Error> {
    if is_bare_str_type(current_type) {
        return Err(syn::Error::new_spanned(
            current_type,
            "df-derive does not support bare or smart-pointer-wrapped `str` leaves; \
             use `String`, `&str`, `Cow<'_, str>`, or a sized wrapper such as \
             `Box<String>`",
        ));
    }
    if matches!(current_type, Type::Slice(_)) {
        return Err(syn::Error::new_spanned(
            current_type,
            "df-derive does not support bare or smart-pointer-wrapped `[T]` slice \
             leaves; use `Vec<T>` for list columns, or use `&[u8]`/`Cow<'_, [u8]>` \
             with `#[df_derive(as_binary)]` for borrowed binary data",
        ));
    }
    Ok(())
}

#[derive(Clone, Copy)]
enum UnsupportedCollection {
    HashMap,
    BTreeMap,
    HashSet,
    BTreeSet,
    VecDeque,
    LinkedList,
}

impl UnsupportedCollection {
    const ALL: [Self; 6] = [
        Self::HashMap,
        Self::BTreeMap,
        Self::HashSet,
        Self::BTreeSet,
        Self::VecDeque,
        Self::LinkedList,
    ];

    const fn name(self) -> &'static str {
        match self {
            Self::HashMap => "HashMap",
            Self::BTreeMap => "BTreeMap",
            Self::HashSet => "HashSet",
            Self::BTreeSet => "BTreeSet",
            Self::VecDeque => "VecDeque",
            Self::LinkedList => "LinkedList",
        }
    }

    fn diagnostic(self, current_type: &Type) -> syn::Error {
        let message = match self {
            Self::HashMap => "df-derive does not support `HashMap` fields. Convert to \
                 `Vec<(K, V)>` or pre-flatten into named columns before assignment."
                .to_owned(),
            Self::BTreeMap => "df-derive does not support `BTreeMap` fields. Convert to \
                 `Vec<(K, V)>` or pre-flatten into named columns before assignment."
                .to_owned(),
            Self::HashSet => "df-derive does not support `HashSet` fields. Convert to \
                 `Vec<T>` (order will be set-defined, not insertion-defined)."
                .to_owned(),
            Self::BTreeSet => "df-derive does not support `BTreeSet` fields. Convert to \
                 `Vec<T>` (order will follow the set's sorted iteration order)."
                .to_owned(),
            Self::VecDeque | Self::LinkedList => {
                let collection = self.name();
                format!(
                    "df-derive does not support `{collection}` fields. Convert to `Vec<T>` before assignment."
                )
            }
        };
        syn::Error::new_spanned(current_type, message)
    }
}

fn unsupported_collection_kind(type_path: &TypePath) -> Option<UnsupportedCollection> {
    UnsupportedCollection::ALL
        .into_iter()
        .find(|collection| path_is_bare_or_std_collection(type_path, collection.name()))
}

fn path_is_bare_or_std_collection(type_path: &TypePath, leaf: &str) -> bool {
    let Some(path) = PathView::from_type_path(type_path) else {
        return false;
    };
    let Some(segment) = path.leaf() else {
        return false;
    };

    segment.ident == leaf
        && (path.len() == 1
            || (path.len() == 3 && path_prefix_is_no_args(type_path, &["std", "collections"])))
}