interoptopus_proc_impl 0.16.0-alpha.15

Macros to produce Interoptopus item info.
Documentation
use crate::docs::extract_docs;
use crate::service::args::{FfiServiceArgs, ServiceExportKind};
use crate::skip::has_ffi_skip_attribute;
use proc_macro2::Span;
use quote::ToTokens;
use syn::spanned::Spanned;
use syn::{FnArg, Generics, Ident, ImplItem, ItemImpl, Pat, PathSegment, ReturnType, Type, Visibility};

#[derive(Clone)]
#[allow(dead_code)]
pub struct ServiceModel {
    pub service_name: Ident,
    pub service_type: Type,
    pub generics: syn::Generics,
    pub args: FfiServiceArgs,
    pub constructors: Vec<ServiceMethod>,
    pub methods: Vec<ServiceMethod>,
    pub is_async: bool,
}

#[derive(Clone)]
#[allow(dead_code)]
pub struct ServiceMethod {
    pub name: Ident,
    pub docs: Vec<String>,
    pub inputs: Vec<ServiceParameter>,
    pub output: ReturnType,
    pub is_async: bool,
    pub receiver_kind: ReceiverKind,
    pub vis: Visibility,
    pub span: Span,
    pub skip: bool,
    pub generics: Generics,
}

#[derive(Clone)]
#[allow(dead_code)]
pub struct ServiceParameter {
    pub name: Ident,
    pub ty: Type,
    pub span: Span,
}

#[derive(Clone)]
pub enum ReceiverKind {
    None,                 // Constructor
    Shared,               // &self
    Mutable,              // &mut self
    AsyncThis,            // Async<Self>
    AsyncCtor(Box<Type>), // Async<Runtime> where Runtime != Self — async constructor
}

impl ServiceModel {
    #[allow(clippy::too_many_lines)]
    pub fn from_impl_item(input: ItemImpl, args: FfiServiceArgs) -> syn::Result<Self> {
        // Extract service type and name
        let service_type = match input.self_ty.as_ref() {
            Type::Path(path) => {
                if let Some(segment) = path.path.segments.last() {
                    (segment.ident.clone(), (*input.self_ty).clone())
                } else {
                    return Err(syn::Error::new_spanned(&input.self_ty, "Invalid service type"));
                }
            }
            _ => return Err(syn::Error::new_spanned(&input.self_ty, "Service type must be a path")),
        };

        let (service_name, service_type) = service_type;
        let generics = input.generics.clone();

        let mut constructors = Vec::new();
        let mut methods = Vec::new();
        let mut has_async = false;

        // Process each method in the impl block
        for item in &input.items {
            if let ImplItem::Fn(method) = item {
                if has_ffi_skip_attribute(&method.attrs) {
                    continue;
                }

                let docs = extract_docs(&method.attrs);
                let method_name = method.sig.ident.clone();
                let is_async = method.sig.asyncness.is_some();
                let vis = method.vis.clone();
                let span = method.span();

                if is_async {
                    has_async = true;
                }

                // Parse parameters and determine receiver kind
                let mut inputs = Vec::new();
                let mut receiver_kind = ReceiverKind::None;
                let mut param_index = 0; // Track index for generated parameter names

                for (i, input_arg) in method.sig.inputs.iter().enumerate() {
                    match input_arg {
                        FnArg::Receiver(receiver) => {
                            receiver_kind = if receiver.mutability.is_some() {
                                ReceiverKind::Mutable
                            } else {
                                ReceiverKind::Shared
                            };
                        }
                        FnArg::Typed(typed_arg) => {
                            let param_type = (*typed_arg.ty).clone();

                            // Check for special Async<X> parameter (first parameter in async functions)
                            if i == 0
                                && is_async
                                && let Type::Path(path) = &param_type
                                && let Some(segment) = path.path.segments.last()
                                && segment.ident == "Async"
                            {
                                receiver_kind = receiver_kind_from_async_param(segment);
                                continue; // Don't add to inputs, regardless of pattern
                            }

                            let param_name = if let Pat::Ident(pat_ident) = typed_arg.pat.as_ref() {
                                pat_ident.ident.clone()
                            } else {
                                // Generate a synthetic name for non-identifier patterns (like `_`)
                                syn::Ident::new(&format!("_{param_index}"), typed_arg.pat.span())
                            };

                            inputs.push(ServiceParameter { name: param_name, ty: param_type, span: typed_arg.span() });
                            param_index += 1;
                        }
                    }
                }

                let service_method = ServiceMethod {
                    name: method_name,
                    docs,
                    inputs,
                    output: method.sig.output.clone(),
                    is_async,
                    receiver_kind: receiver_kind.clone(),
                    vis,
                    span,
                    skip: false,
                    generics: method.sig.generics.clone(),
                };

                // Validate async methods
                if is_async {
                    match receiver_kind {
                        ReceiverKind::None => {
                            return Err(syn::Error::new_spanned(method.sig.inputs.first(), "Async methods must use Async<Self> as their first parameter"));
                        }
                        ReceiverKind::Shared | ReceiverKind::Mutable => {
                            // Find the receiver to point the error at it
                            let receiver_span = method.sig.inputs.first().map_or_else(|| method.sig.span(), Spanned::span);
                            return Err(syn::Error::new(receiver_span, "Async methods cannot use &self or &mut self. Use Async<Self> as the first parameter instead."));
                        }
                        ReceiverKind::AsyncThis => {
                            // Valid async method
                        }
                        ReceiverKind::AsyncCtor(_) => {
                            // Valid async constructor
                        }
                    }
                }

                // Classify: sync constructors and async constructors go to constructors, everything else to methods
                match (&receiver_kind, is_async) {
                    (ReceiverKind::None, false) => constructors.push(service_method),
                    (ReceiverKind::AsyncCtor(_), true) => constructors.push(service_method),
                    _ => methods.push(service_method),
                }
            }
        }

        // Note: We now support lifetime generics, but not type generics
        for param in &generics.params {
            if let syn::GenericParam::Type(_) = param {
                return Err(syn::Error::new_spanned(param, "Generic services are not supported by #[ffi], only lifetime work."));
            }
        }

        let model = Self { service_name, service_type, generics, args, constructors, methods, is_async: has_async };

        Ok(model)
    }

    pub fn service_name_snake_case(&self) -> String {
        // Check if a manual prefix is provided
        if let Some(ref prefix) = self.args.prefix {
            // Remove trailing underscore if present, we'll add it back when needed
            prefix.trim_end_matches('_').to_string()
        } else {
            // Convert CamelCase to snake_case
            let name = self.service_name.to_string();
            let mut result = String::new();
            let mut chars = name.chars().peekable();

            while let Some(ch) = chars.next() {
                if ch.is_uppercase() && !result.is_empty() {
                    // Check if next char is lowercase (to handle acronyms correctly)
                    if let Some(&next_ch) = chars.peek()
                        && next_ch.is_lowercase()
                    {
                        result.push('_');
                    }
                }
                result.push(ch.to_lowercase().next().unwrap_or(ch));
            }

            result
        }
    }

    /// Computes a deterministic hash for this service impl block, incorporating
    /// the service name and all constructor/method signatures.
    fn compute_service_hash(&self) -> u64 {
        use std::collections::hash_map::DefaultHasher;
        use std::hash::{Hash, Hasher};

        let mut hasher = DefaultHasher::new();

        self.service_name.to_string().hash(&mut hasher);

        for ctor in &self.constructors {
            ctor.name.to_string().hash(&mut hasher);
            for param in &ctor.inputs {
                param.ty.to_token_stream().to_string().hash(&mut hasher);
            }
            match &ctor.output {
                ReturnType::Default => "()".hash(&mut hasher),
                ReturnType::Type(_, ty) => ty.to_token_stream().to_string().hash(&mut hasher),
            }
        }

        for method in &self.methods {
            method.name.to_string().hash(&mut hasher);
            for param in &method.inputs {
                param.ty.to_token_stream().to_string().hash(&mut hasher);
            }
            match &method.output {
                ReturnType::Default => "()".hash(&mut hasher),
                ReturnType::Type(_, ty) => ty.to_token_stream().to_string().hash(&mut hasher),
            }
        }

        for param in &self.generics.params {
            param.to_token_stream().to_string().hash(&mut hasher);
        }

        hasher.finish()
    }

    /// Returns the export name for a generated FFI function, or `None` if no
    /// `export` attribute was set on the service.
    pub fn generate_export_name(&self, base_function_name: &str) -> Option<String> {
        match &self.args.export {
            Some(ServiceExportKind::Unique) => {
                let hash = self.compute_service_hash();
                Some(format!("{}_{}", base_function_name, hash % 100000))
            }
            None => None,
        }
    }
}

/// Determine the [`ReceiverKind`] for an `Async<X>` first parameter.
///
/// Returns [`ReceiverKind::AsyncThis`] when the inner type is `Self` (i.e. a
/// regular async method), or [`ReceiverKind::AsyncCtor`] when it is any other
/// type (i.e. an async constructor that borrows a foreign runtime).
fn receiver_kind_from_async_param(segment: &PathSegment) -> ReceiverKind {
    let Some(inner_type) = extract_async_inner_type(segment) else {
        // Could not extract a type argument — default to async method.
        return ReceiverKind::AsyncThis;
    };

    if let Type::Path(inner_path) = inner_type
        && let Some(inner_segment) = inner_path.path.segments.last()
        && inner_segment.ident == "Self"
    {
        ReceiverKind::AsyncThis
    } else {
        ReceiverKind::AsyncCtor(Box::new(inner_type.clone()))
    }
}

/// Extract the first type argument from `Async<T>`, returning `Some(&T)`.
fn extract_async_inner_type(segment: &PathSegment) -> Option<&Type> {
    if let syn::PathArguments::AngleBracketed(args) = &segment.arguments
        && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
    {
        Some(inner)
    } else {
        None
    }
}