actrpc-core-macros 0.1.0

Procedural macros for ActRPC core.
Documentation
use proc_macro2::TokenStream;
use quote::quote;
use syn::{
    Data, DataEnum, DataStruct, DeriveInput, Error, Field, Fields, FieldsNamed, GenericArgument,
    PathArguments, Result, Type, TypePath, Variant,
};

pub fn extract_named_struct_fields(input: &DeriveInput) -> Result<&FieldsNamed> {
    match &input.data {
        Data::Struct(DataStruct {
            fields: Fields::Named(fields),
            ..
        }) => Ok(fields),
        Data::Struct(_) => Err(Error::new_spanned(
            input,
            "only structs with named fields are supported",
        )),
        Data::Enum(_) => Err(Error::new_spanned(
            input,
            "enums are not supported by this derive",
        )),
        Data::Union(_) => Err(Error::new_spanned(input, "unions are not supported")),
    }
}

pub fn extract_enum_variants(
    input: &DeriveInput,
) -> Result<&syn::punctuated::Punctuated<Variant, syn::token::Comma>> {
    match &input.data {
        Data::Enum(DataEnum { variants, .. }) => Ok(variants),
        Data::Struct(_) => Err(Error::new_spanned(
            input,
            "only enums are supported by this derive path",
        )),
        Data::Union(_) => Err(Error::new_spanned(input, "unions are not supported")),
    }
}

pub fn field_name(field: &Field) -> Result<String> {
    let ident = field
        .ident
        .as_ref()
        .ok_or_else(|| Error::new_spanned(field, "only named fields are supported"))?;
    Ok(ident.to_string())
}

pub fn is_option(ty: &Type) -> Option<&Type> {
    let Type::Path(TypePath { path, .. }) = ty else {
        return None;
    };

    let segment = path.segments.last()?;
    if segment.ident != "Option" {
        return None;
    }

    let PathArguments::AngleBracketed(args) = &segment.arguments else {
        return None;
    };

    if args.args.len() != 1 {
        return None;
    }

    match args.args.first()? {
        GenericArgument::Type(inner) => Some(inner),
        _ => None,
    }
}

pub fn is_vec(ty: &Type) -> Option<&Type> {
    let Type::Path(TypePath { path, .. }) = ty else {
        return None;
    };

    let segment = path.segments.last()?;
    if segment.ident != "Vec" {
        return None;
    }

    let PathArguments::AngleBracketed(args) = &segment.arguments else {
        return None;
    };

    if args.args.len() != 1 {
        return None;
    }

    match args.args.first()? {
        GenericArgument::Type(inner) => Some(inner),
        _ => None,
    }
}

pub fn is_map(ty: &Type) -> Option<&Type> {
    let Type::Path(TypePath { path, .. }) = ty else {
        return None;
    };

    let segment = path.segments.last()?;
    let ident = segment.ident.to_string();
    if ident != "HashMap" && ident != "BTreeMap" {
        return None;
    }

    let PathArguments::AngleBracketed(args) = &segment.arguments else {
        return None;
    };

    if args.args.len() != 2 {
        return None;
    }

    let mut iter = args.args.iter();
    let key = match iter.next()? {
        GenericArgument::Type(inner) => inner,
        _ => return None,
    };
    let value = match iter.next()? {
        GenericArgument::Type(inner) => inner,
        _ => return None,
    };

    if !is_string_type(key) {
        return None;
    }

    Some(value)
}

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

    path.segments
        .last()
        .map(|segment| segment.ident == "String")
        .unwrap_or(false)
}

fn primitive_descriptor_tokens(ty: &Type) -> Option<TokenStream> {
    let Type::Path(TypePath { path, .. }) = ty else {
        return None;
    };

    let ident = &path.segments.last()?.ident;
    let primitive = match ident.to_string().as_str() {
        "bool" => quote! {
            ::actrpc_core::descriptor::types::PrimitiveDescriptor::Bool
        },
        "String" => quote! {
            ::actrpc_core::descriptor::types::PrimitiveDescriptor::String
        },
        "u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64" | "i128"
        | "isize" => quote! {
            ::actrpc_core::descriptor::types::PrimitiveDescriptor::Integer
        },
        "f32" | "f64" => quote! {
            ::actrpc_core::descriptor::types::PrimitiveDescriptor::Number
        },
        _ => return None,
    };

    Some(quote! {
        ::actrpc_core::descriptor::types::ValueDescriptor::Primitive(#primitive)
    })
}

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

    let segments: Vec<String> = path.segments.iter().map(|s| s.ident.to_string()).collect();

    matches!(
        segments.as_slice(),
        [single] if single == "Value"
    ) || matches!(
        segments.as_slice(),
        [a, b] if a == "serde_json" && b == "Value"
    )
}

pub fn value_descriptor_tokens(ty: &Type, allow_option: bool) -> Result<TokenStream> {
    if let Some(inner) = is_option(ty) {
        if allow_option {
            return value_descriptor_tokens(inner, false);
        }

        return Err(Error::new_spanned(ty, "Option<T> is not allowed here"));
    }

    if is_serde_json_value(ty) {
        return Ok(quote! {
            ::actrpc_core::descriptor::types::ValueDescriptor::Any
        });
    }

    if let Some(primitive) = primitive_descriptor_tokens(ty) {
        return Ok(primitive);
    }

    if let Some(inner) = is_vec(ty) {
        let inner_tokens = value_descriptor_tokens(inner, false)?;
        return Ok(quote! {
            ::actrpc_core::descriptor::types::ValueDescriptor::Array(
                Box::new(#inner_tokens)
            )
        });
    }

    if let Some(inner) = is_map(ty) {
        let inner_tokens = value_descriptor_tokens(inner, false)?;
        return Ok(quote! {
            ::actrpc_core::descriptor::types::ValueDescriptor::Map(
                Box::new(#inner_tokens)
            )
        });
    }

    Ok(quote! {
        <#ty as ::actrpc_core::descriptor::traits::DescribeValue>::describe_value()
    })
}