xabi-macros 0.1.1

Procedural macros for xabi
Documentation
use syn::{
    Error, GenericArgument, Pat, PathArguments, ReturnType, TraitItemFn, Type, TypeParamBound,
    parse_quote,
};

use crate::type_shape::{XabiValueContext, validate_return_type, validate_xabi_value_type};

use super::{ArgKind, MethodArg, MethodRet};

pub(super) fn parse_arg(arg: &syn::PatType) -> syn::Result<MethodArg> {
    let Pat::Ident(name) = arg.pat.as_ref() else {
        return Err(Error::new_spanned(
            &arg.pat,
            "argument must be an identifier",
        ));
    };
    let ty = (*arg.ty).clone();
    let kind = if is_bytes_ref(&ty) {
        ArgKind::Bytes
    } else if is_str_ref(&ty) {
        ArgKind::Str
    } else {
        validate_xabi_value_type(&ty, XabiValueContext::MethodArgument)?;
        ArgKind::Value
    };
    Ok(MethodArg {
        name: name.ident.clone(),
        ty,
        kind,
    })
}

pub(super) fn parse_ret(output: &ReturnType) -> syn::Result<MethodRet> {
    let ty = validate_return_type(output)?;
    if is_ident_type(ty, "String") {
        return Ok(MethodRet::String);
    }
    if is_ident_type(ty, "u32") {
        return Ok(MethodRet::U32);
    }
    if is_ident_type(ty, "bool") {
        return Ok(MethodRet::Bool);
    }
    if is_result_type(ty) {
        parse_result_ret(ty)
    } else {
        validate_xabi_value_type(ty, XabiValueContext::MethodReturn)?;
        Ok(MethodRet::Value(ty.clone()))
    }
}

pub(super) fn validate_shape(
    method: &TraitItemFn,
    args: &[MethodArg],
    ret: &MethodRet,
    asyncness: bool,
) -> syn::Result<()> {
    if matches!(
        ret,
        MethodRet::String | MethodRet::U32 | MethodRet::Bool | MethodRet::Value(_)
    ) && !args.is_empty()
    {
        return Err(Error::new_spanned(
            method,
            "non-Result xabi methods cannot take arguments",
        ));
    }
    if asyncness
        && matches!(
            ret,
            MethodRet::String | MethodRet::U32 | MethodRet::Bool | MethodRet::Value(_)
        )
    {
        return Err(Error::new_spanned(
            method,
            "async xabi methods must return Result",
        ));
    }
    Ok(())
}

fn parse_result_ret(ty: &Type) -> syn::Result<MethodRet> {
    let Type::Path(path) = ty else {
        return Err(Error::new_spanned(ty, "unsupported return type"));
    };
    let Some(segment) = path.path.segments.last() else {
        return Err(Error::new_spanned(ty, "unsupported return type"));
    };
    if segment.ident != "Result" {
        return Err(Error::new_spanned(ty, "unsupported return type"));
    }
    let PathArguments::AngleBracketed(args) = &segment.arguments else {
        return Err(Error::new_spanned(ty, "Result must have a payload type"));
    };
    let type_args = args
        .args
        .iter()
        .filter_map(|arg| match arg {
            GenericArgument::Type(ty) => Some(ty),
            _ => None,
        })
        .collect::<Vec<_>>();
    if type_args.len() > 2 {
        return Err(Error::new_spanned(
            ty,
            "Result must have one payload type and at most one error type",
        ));
    }
    let Some(payload) = type_args.first() else {
        return Err(Error::new_spanned(ty, "Result must have a payload type"));
    };
    let error_ty = type_args
        .get(1)
        .map(|ty| (*ty).clone())
        .unwrap_or_else(|| parse_quote!(::xabi::Error));
    validate_xabi_value_type(&error_ty, XabiValueContext::ResultError)?;

    if is_unit_type(payload) {
        return Ok(MethodRet::ResultUnit(error_ty));
    }
    if is_ident_type(payload, "String") {
        return Ok(MethodRet::ResultString(error_ty));
    }
    if is_vec_u8(payload) {
        return Ok(MethodRet::ResultBytes(error_ty));
    }
    if let Some(trait_path) = xabi_object_trait(payload)? {
        return Ok(MethodRet::ResultObject {
            trait_path,
            error: error_ty,
        });
    }
    if let Some((ok, trait_path)) = xabi_object_pair(payload)? {
        validate_xabi_value_type(&ok, XabiValueContext::ResultPayload)?;
        return Ok(MethodRet::ResultObjectPair {
            ok,
            trait_path,
            error: error_ty,
        });
    }
    if matches!(payload, Type::Tuple(_)) {
        return Err(Error::new_spanned(
            payload,
            "xabi Result tuple payloads are only supported for object pairs `(T, impl SomeXabiTrait + 'static)`; use `#[xabi::data]` for structured values",
        ));
    }
    validate_xabi_value_type(payload, XabiValueContext::ResultPayload)?;
    Ok(MethodRet::ResultValue {
        ok: (*payload).clone(),
        error: error_ty,
    })
}

fn is_result_type(ty: &Type) -> bool {
    let Type::Path(path) = ty else {
        return false;
    };
    path.path
        .segments
        .last()
        .map(|segment| segment.ident == "Result")
        .unwrap_or(false)
}

fn is_ident_type(ty: &Type, expected: &str) -> bool {
    let Type::Path(path) = ty else {
        return false;
    };
    path.path
        .segments
        .last()
        .map(|segment| segment.ident == expected)
        .unwrap_or(false)
}

fn is_unit_type(ty: &Type) -> bool {
    matches!(ty, Type::Tuple(tuple) if tuple.elems.is_empty())
}

fn is_bytes_ref(ty: &Type) -> bool {
    let Type::Reference(reference) = ty else {
        return false;
    };
    if reference.mutability.is_some() {
        return false;
    }
    let Type::Slice(slice) = reference.elem.as_ref() else {
        return false;
    };
    is_ident_type(&slice.elem, "u8")
}

fn is_str_ref(ty: &Type) -> bool {
    let Type::Reference(reference) = ty else {
        return false;
    };
    if reference.mutability.is_some() {
        return false;
    }
    is_ident_type(&reference.elem, "str")
}

fn is_vec_u8(ty: &Type) -> bool {
    let Type::Path(path) = ty else {
        return false;
    };
    let Some(segment) = path.path.segments.last() else {
        return false;
    };
    if segment.ident != "Vec" {
        return false;
    }
    let PathArguments::AngleBracketed(args) = &segment.arguments else {
        return false;
    };
    matches!(args.args.first(), Some(GenericArgument::Type(ty)) if is_ident_type(ty, "u8"))
}

fn xabi_object_trait(ty: &Type) -> syn::Result<Option<syn::Path>> {
    let Type::ImplTrait(impl_trait) = ty else {
        return Ok(None);
    };
    let has_static = impl_trait.bounds.iter().any(
        |bound| matches!(bound, TypeParamBound::Lifetime(lifetime) if lifetime.ident == "static"),
    );
    if !has_static {
        return Err(Error::new_spanned(
            impl_trait,
            "xabi object returns must use `impl SomeXabiTrait + 'static`",
        ));
    }
    let object_bounds = impl_trait
        .bounds
        .iter()
        .filter_map(|bound| match bound {
            TypeParamBound::Trait(bound) if !is_marker_trait_bound(bound) => Some(bound),
            _ => None,
        })
        .collect::<Vec<_>>();
    let [bound] = object_bounds.as_slice() else {
        return Err(Error::new_spanned(
            impl_trait,
            "xabi object returns must name exactly one xabi trait plus optional auto trait bounds",
        ));
    };
    if bound
        .path
        .segments
        .iter()
        .any(|segment| !matches!(segment.arguments, PathArguments::None))
    {
        return Err(Error::new_spanned(
            &bound.path,
            "xabi object return trait bounds must be plain xabi trait paths without generic arguments or associated type constraints",
        ));
    }
    Ok(Some(bound.path.clone()))
}

fn xabi_object_pair(ty: &Type) -> syn::Result<Option<(Type, syn::Path)>> {
    let Type::Tuple(tuple) = ty else {
        return Ok(None);
    };
    if tuple.elems.len() != 2 {
        return Ok(None);
    }
    let mut elems = tuple.elems.iter();
    let ok = elems.next().expect("tuple len checked").clone();
    let object = elems.next().expect("tuple len checked");
    let Some(trait_path) = xabi_object_trait(object)? else {
        return Ok(None);
    };
    Ok(Some((ok, trait_path)))
}

fn is_marker_trait_bound(bound: &syn::TraitBound) -> bool {
    bound
        .path
        .segments
        .last()
        .map(|segment| {
            segment.ident == "Send"
                || segment.ident == "Sync"
                || segment.ident == "Unpin"
                || segment.ident == "Sized"
        })
        .unwrap_or(false)
}