luau 0.732.0

Safe lifetime-bound Rust embedding API for the Luau runtime
use core::any::type_name;

use super::{AnyUserdata, UserdataRegistry};
use crate::callback::{Arguments, CallbackReturn};
use crate::error::Error;
use crate::lua::LuaRef;
use crate::value::{IntoLua, Value};

/// Luau metamethods that userdata can define.
///
/// The protected `__gc` and `__metatable` fields are not configurable.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum MetaMethod {
    /// The `+` operator.
    Add,
    /// The `-` operator.
    Sub,
    /// The `*` operator.
    Mul,
    /// The `/` operator.
    Div,
    /// The `//` operator.
    IDiv,
    /// The `%` operator.
    Mod,
    /// The `^` operator.
    Pow,
    /// Unary `-`.
    Unm,
    /// The `==` operator.
    Eq,
    /// The `<` operator.
    Lt,
    /// The `<=` operator.
    Le,
    /// The `#` operator.
    Len,
    /// The `..` operator.
    Concat,
    /// Index access, `value[key]`.
    Index,
    /// Index assignment, `value[key] = new_value`.
    NewIndex,
    /// Function-call syntax, `value(...)`.
    Call,
    /// String conversion through `tostring`.
    ToString,
    /// Debug string formatting.
    ToDebugString,
    /// Luau method-call dispatch.
    NameCall,
    /// Generalized iteration.
    Iter,
    /// The type name returned by `typeof`.
    Type,
}

impl MetaMethod {
    /// Returns the Luau metatable field name.
    pub const fn name(self) -> &'static str {
        match self {
            Self::Add => "__add",
            Self::Sub => "__sub",
            Self::Mul => "__mul",
            Self::Div => "__div",
            Self::IDiv => "__idiv",
            Self::Mod => "__mod",
            Self::Pow => "__pow",
            Self::Unm => "__unm",
            Self::Eq => "__eq",
            Self::Lt => "__lt",
            Self::Le => "__le",
            Self::Len => "__len",
            Self::Concat => "__concat",
            Self::Index => "__index",
            Self::NewIndex => "__newindex",
            Self::Call => "__call",
            Self::ToString => "__tostring",
            Self::ToDebugString => "__todebugstring",
            Self::NameCall => "__namecall",
            Self::Iter => "__iter",
            Self::Type => "__type",
        }
    }

    pub(crate) fn validate(name: &[u8]) -> Result<(), Error> {
        if matches!(name, b"__gc" | b"__metatable") {
            return Err(Error::MetaMethodRestricted(
                String::from_utf8_lossy(name).into_owned(),
            ));
        }
        Ok(())
    }
}

impl AsRef<str> for MetaMethod {
    fn as_ref(&self) -> &str {
        self.name()
    }
}

impl AsRef<[u8]> for MetaMethod {
    fn as_ref(&self) -> &[u8] {
        self.name().as_bytes()
    }
}

/// Defines fields and methods for a Rust userdata type.
pub trait Userdata: Sized {
    /// Adds fields for this userdata type.
    fn add_fields<F: UserdataFields<Self>>(_fields: &mut F) {}

    /// Adds methods and metamethods for this userdata type.
    fn add_methods<M: UserdataMethods<Self>>(_methods: &mut M) {}

    /// Registers this userdata type.
    ///
    /// The default implementation calls [`Userdata::add_fields`] and
    /// [`Userdata::add_methods`].
    fn register(registry: &mut UserdataRegistry<'_, Self>)
    where
        Self: 'static,
    {
        Self::add_fields(registry);
        Self::add_methods(registry);
        #[cfg(feature = "macros")]
        super::register_userdata_impls(registry);
    }
}

/// Field registry used by [`Userdata::add_fields`].
pub trait UserdataFields<T> {
    /// Adds a value shared by every instance of this userdata type.
    fn add_field<V>(&mut self, name: impl AsRef<[u8]>, value: V)
    where
        V: for<'lua> IntoLua<'lua>;

    /// Adds a value computed when the userdata type is registered.
    fn add_field_with<F>(&mut self, name: impl AsRef<[u8]>, field: F)
    where
        F: for<'lua> FnOnce(LuaRef<'lua>) -> Result<Value<'lua>, Error> + 'static;

    /// Adds a field getter that immutably borrows the Rust payload.
    fn add_field_method_get<M>(&mut self, name: impl AsRef<[u8]>, method: M)
    where
        M: for<'call> Fn(
                LuaRef<'call>,
                &T,
                Arguments<'call>,
            ) -> Result<CallbackReturn<'call>, Error>
            + 'static;

    /// Adds a field setter that mutably borrows the Rust payload.
    fn add_field_method_set<M>(&mut self, name: impl AsRef<[u8]>, method: M)
    where
        M: for<'call> FnMut(
                LuaRef<'call>,
                &mut T,
                Arguments<'call>,
            ) -> Result<CallbackReturn<'call>, Error>
            + 'static;

    /// Adds a getter that receives the userdata object instead of borrowing `T`.
    fn add_field_function_get<F>(&mut self, name: impl AsRef<[u8]>, function: F)
    where
        F: for<'call> Fn(LuaRef<'call>, Arguments<'call>) -> Result<CallbackReturn<'call>, Error>
            + 'static;

    /// Adds a setter that receives the userdata object instead of borrowing `T`.
    fn add_field_function_set<F>(&mut self, name: impl AsRef<[u8]>, function: F)
    where
        F: for<'call> FnMut(
                LuaRef<'call>,
                Arguments<'call>,
            ) -> Result<CallbackReturn<'call>, Error>
            + 'static;

    /// Adds a fixed metatable field.
    fn add_meta_field<V>(&mut self, name: impl AsRef<[u8]>, value: V)
    where
        V: for<'lua> IntoLua<'lua>;

    /// Adds a metatable field computed when the type is registered.
    fn add_meta_field_with<F>(&mut self, name: impl AsRef<[u8]>, field: F)
    where
        F: for<'lua> FnOnce(LuaRef<'lua>) -> Result<Value<'lua>, Error> + 'static;
}

/// Method registry used by [`Userdata::add_methods`].
pub trait UserdataMethods<T> {
    /// Adds a method that immutably borrows the Rust payload.
    fn add_method<M>(&mut self, name: impl AsRef<[u8]>, method: M)
    where
        M: for<'call> Fn(
                LuaRef<'call>,
                &T,
                Arguments<'call>,
            ) -> Result<CallbackReturn<'call>, Error>
            + 'static;

    /// Adds a method that mutably borrows the Rust payload.
    fn add_method_mut<M>(&mut self, name: impl AsRef<[u8]>, method: M)
    where
        M: for<'call> FnMut(
                LuaRef<'call>,
                &mut T,
                Arguments<'call>,
            ) -> Result<CallbackReturn<'call>, Error>
            + 'static;

    /// Adds a method that moves the Rust payload out of the userdata.
    ///
    /// Each userdata instance can call this method only once.
    fn add_method_once<M>(&mut self, name: impl AsRef<[u8]>, method: M)
    where
        T: 'static,
        M: for<'call> Fn(
                LuaRef<'call>,
                T,
                Arguments<'call>,
            ) -> Result<CallbackReturn<'call>, Error>
            + 'static,
    {
        self.add_function(name, move |lua, mut arguments| {
            let userdata: AnyUserdata<'_> = arguments.next()?;
            let value = userdata
                .take::<T>()
                .map_err(|error| Error::bad_argument(1, error))?;
            method(lua, value, arguments)
        });
    }

    /// Adds a function that receives the complete Luau argument list.
    fn add_function<F>(&mut self, name: impl AsRef<[u8]>, function: F)
    where
        F: for<'call> Fn(LuaRef<'call>, Arguments<'call>) -> Result<CallbackReturn<'call>, Error>
            + 'static;

    /// Adds a mutable function that receives the complete Luau argument list.
    fn add_function_mut<F>(&mut self, name: impl AsRef<[u8]>, function: F)
    where
        F: for<'call> FnMut(
                LuaRef<'call>,
                Arguments<'call>,
            ) -> Result<CallbackReturn<'call>, Error>
            + 'static;

    /// Adds a metamethod that immutably borrows the Rust payload.
    ///
    /// For binary operators where `T` may appear on the right, use
    /// [`UserdataMethods::add_meta_function`] instead.
    fn add_meta_method<M>(&mut self, name: impl AsRef<[u8]>, method: M)
    where
        M: for<'call> Fn(
                LuaRef<'call>,
                &T,
                Arguments<'call>,
            ) -> Result<CallbackReturn<'call>, Error>
            + 'static;

    /// Adds a metamethod that mutably borrows the Rust payload.
    ///
    /// For binary operators where `T` may appear on the right, use
    /// [`UserdataMethods::add_meta_function_mut`] instead.
    fn add_meta_method_mut<M>(&mut self, name: impl AsRef<[u8]>, method: M)
    where
        M: for<'call> FnMut(
                LuaRef<'call>,
                &mut T,
                Arguments<'call>,
            ) -> Result<CallbackReturn<'call>, Error>
            + 'static;

    /// Adds a metamethod that receives the complete Luau argument list.
    fn add_meta_function<F>(&mut self, name: impl AsRef<[u8]>, function: F)
    where
        F: for<'call> Fn(LuaRef<'call>, Arguments<'call>) -> Result<CallbackReturn<'call>, Error>
            + 'static;

    /// Adds a mutable metamethod that receives the complete Luau argument list.
    fn add_meta_function_mut<F>(&mut self, name: impl AsRef<[u8]>, function: F)
    where
        F: for<'call> FnMut(
                LuaRef<'call>,
                Arguments<'call>,
            ) -> Result<CallbackReturn<'call>, Error>
            + 'static;
}

pub(super) fn short_type_name<T: ?Sized>() -> String {
    let full_name = type_name::<T>();
    let mut name = String::new();
    let mut index = 0;

    while index < full_name.len() {
        let remaining = &full_name[index..];
        if let Some(special_index) =
            remaining.find(|c: char| [' ', '<', '>', '(', ')', '[', ']', ',', ';'].contains(&c))
        {
            name.push_str(collapse_type_name(&remaining[..special_index]));
            name.push_str(&remaining[special_index..=special_index]);

            if name.ends_with("<'_>") || name.ends_with("<'_, ") {
                name.truncate(name.len() - 4);
            }

            let after_special = special_index + 1;
            if matches!(&remaining[special_index..=special_index], ">" | ")" | "]")
                && remaining[after_special..].starts_with("::")
            {
                name.push_str("::");
                index += after_special + 2;
            } else {
                index += after_special;
            }
        } else {
            name.push_str(collapse_type_name(remaining));
            break;
        }
    }

    name
}

fn collapse_type_name(segment: &str) -> &str {
    segment.rsplit("::").next().unwrap_or(segment)
}