Skip to main content

ytil_noxi/
macros.rs

1//! Extension macros and helpers for bridging Rust and Nvim (`nvim_oxi`).
2//!
3//! Defines `dict!` for ergonomic [`nvim_oxi::Dictionary`] construction plus `fn_from!` to wrap Rust
4//! functions into Nvim callable `Function` objects.
5
6/// Construct a [`nvim_oxi::Dictionary`] from key-value pairs, supporting nested `dict!` usage.
7///
8/// Keys can be:
9/// - string literals,
10/// - identifiers (converted with [`stringify!`]), or
11/// - expressions yielding [`String`] or [`&str`].
12///
13/// Values: any type that implements [`Into<nvim_oxi::Object>`]
14#[macro_export]
15macro_rules! dict {
16    () => {{
17        ::nvim_oxi::Dictionary::default()
18    }};
19    ( $( $key:tt : $value:expr ),+ $(,)? ) => {{
20        let mut map: ::std::collections::BTreeMap<
21            ::std::borrow::Cow<'static, str>,
22            ::nvim_oxi::Object
23        > = ::std::collections::BTreeMap::new();
24        $(
25            let k: ::std::borrow::Cow<'static, str> = $crate::__dict_key_to_cow!($key);
26            let v: ::nvim_oxi::Object = ::nvim_oxi::Object::from($value);
27            map.insert(k, v);
28        )+
29        ::nvim_oxi::Dictionary::from_iter(map)
30    }};
31}
32
33#[doc(hidden)]
34#[macro_export]
35macro_rules! __dict_key_to_cow {
36    ($k:literal) => {
37        ::std::borrow::Cow::Borrowed($k)
38    };
39    ($k:ident) => {
40        ::std::borrow::Cow::Borrowed(::std::stringify!($k))
41    };
42    ($k:expr) => {
43        ::std::borrow::Cow::Owned(::std::convert::Into::<::std::string::String>::into($k))
44    };
45}
46
47/// Implements [`nvim_oxi::conversion::FromObject`] and [`nvim_oxi::lua::Poppable`]
48/// for a type that derives [`serde::Deserialize`].
49///
50/// Eliminates the repeated boilerplate of deserializing Lua objects via `nvim_oxi::serde::Deserializer`.
51#[macro_export]
52macro_rules! impl_nvim_deserializable {
53    ($ty:ty) => {
54        impl ::nvim_oxi::conversion::FromObject for $ty {
55            fn from_object(obj: ::nvim_oxi::Object) -> ::std::result::Result<Self, ::nvim_oxi::conversion::Error> {
56                <Self as ::serde::Deserialize>::deserialize(::nvim_oxi::serde::Deserializer::new(obj))
57                    .map_err(::std::convert::Into::into)
58            }
59        }
60
61        impl ::nvim_oxi::lua::Poppable for $ty {
62            unsafe fn pop(
63                lstate: *mut ::nvim_oxi::lua::ffi::State,
64            ) -> ::std::result::Result<Self, ::nvim_oxi::lua::Error> {
65                // SAFETY: The caller (nvim-oxi framework) guarantees that:
66                // 1. `lstate` is a valid pointer to an initialized Lua state
67                // 2. The Lua stack has at least one value to pop
68                unsafe {
69                    let obj = ::nvim_oxi::Object::pop(lstate)?;
70                    <Self as ::nvim_oxi::conversion::FromObject>::from_object(obj)
71                        .map_err(::nvim_oxi::lua::Error::pop_error_from_err::<Self, _>)
72                }
73            }
74        }
75    };
76}
77
78/// Turns a Rust function into a [`nvim_oxi::Object`] [`nvim_oxi::Function`].
79#[macro_export]
80macro_rules! fn_from {
81    // Plain function path
82    ($path:path) => {
83        ::nvim_oxi::Object::from(::nvim_oxi::Function::from_fn($path))
84    };
85    // Fallback: forward any tokens (supports calls like `Type::method(())`)
86    ($($tokens:tt)+) => {
87        ::nvim_oxi::Object::from(::nvim_oxi::Function::from_fn($($tokens)+))
88    };
89}
90
91#[cfg(test)]
92mod tests {
93    use nvim_oxi::Dictionary;
94    use nvim_oxi::Object;
95
96    use crate::dict::DictionaryExt as _;
97
98    #[test]
99    fn dict_macro_empty_creates_empty_dictionary() {
100        let actual = dict!();
101        assert_eq!(actual.len(), 0);
102    }
103
104    #[test]
105    fn dict_macro_creates_a_dictionary_with_basic_key_value_pairs() {
106        let actual = dict! { "foo": 1, bar: "baz", "num": 3_i64 };
107        let expected = Dictionary::from_iter([
108            ("bar", Object::from("baz")),
109            ("foo", Object::from(1)),
110            ("num", Object::from(3_i64)),
111        ]);
112        assert_eq!(actual, expected);
113    }
114
115    #[test]
116    fn dict_macro_creates_nested_dictionaries() {
117        let k = String::from("alpha");
118        let inner = dict! { inner_key: "value" };
119        let actual = dict! { (k): 10_i64, "beta": inner.clone() };
120        let expected = Dictionary::from_iter([("alpha", Object::from(10_i64)), ("beta", Object::from(inner))]);
121        assert_eq!(actual, expected);
122    }
123
124    #[test]
125    fn dictionary_ext_get_t_works_as_expected() {
126        let dict = dict! { "foo": "42" };
127        assert2::assert!(let Err(err) = dict.get_t::<nvim_oxi::String>("bar"));
128        assert_eq!(err.format_current_context().to_string(), "missing dict value");
129        assert_eq!(dict.get_t::<nvim_oxi::String>("foo").unwrap(), "42");
130
131        let dict = dict! { "foo": 42 };
132        assert2::assert!(let Err(err) = dict.get_t::<nvim_oxi::String>("foo"));
133        assert_eq!(err.format_current_context().to_string(), "unexpected object kind");
134    }
135
136    #[test]
137    fn dictionary_ext_get_dict_works_as_expected() {
138        let dict = dict! { "foo": "42" };
139        assert_eq!(dict.get_dict(&["bar"]).unwrap(), None);
140
141        let dict = dict! { "foo": 42 };
142        assert2::assert!(let Err(err) = dict.get_dict(&["foo"]));
143        assert_eq!(err.format_current_context().to_string(), "unexpected object kind");
144
145        let expected = dict! { "bar": "42" };
146        let dict = dict! { "foo": expected.clone() };
147        assert_eq!(dict.get_dict(&["foo"]).unwrap(), Some(expected));
148    }
149}