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::new();
21        $(
22            let k: ::std::borrow::Cow<'static, str> = $crate::__dict_key_to_cow!($key);
23            let v: ::nvim_oxi::Object = ::nvim_oxi::Object::from($value);
24            map.insert(k, v);
25        )+
26        ::nvim_oxi::Dictionary::from_iter(map)
27    }};
28}
29
30#[doc(hidden)]
31#[macro_export]
32macro_rules! __dict_key_to_cow {
33    ($k:literal) => {
34        ::std::borrow::Cow::Borrowed($k)
35    };
36    ($k:ident) => {
37        ::std::borrow::Cow::Borrowed(::std::stringify!($k))
38    };
39    ($k:expr) => {
40        ::std::borrow::Cow::Owned(::std::convert::Into::<::std::string::String>::into($k))
41    };
42}
43
44/// Implements [`nvim_oxi::conversion::FromObject`] and [`nvim_oxi::lua::Poppable`]
45/// for a type that derives [`serde::Deserialize`].
46///
47/// Eliminates the repeated boilerplate of deserializing Lua objects via `nvim_oxi::serde::Deserializer`.
48#[macro_export]
49macro_rules! impl_nvim_deserializable {
50    ($ty:ty) => {
51        impl ::nvim_oxi::conversion::FromObject for $ty {
52            fn from_object(obj: ::nvim_oxi::Object) -> ::std::result::Result<Self, ::nvim_oxi::conversion::Error> {
53                <Self as ::serde::Deserialize>::deserialize(::nvim_oxi::serde::Deserializer::new(obj))
54                    .map_err(::std::convert::Into::into)
55            }
56        }
57
58        impl ::nvim_oxi::lua::Poppable for $ty {
59            unsafe fn pop(
60                lstate: *mut ::nvim_oxi::lua::ffi::State,
61            ) -> ::std::result::Result<Self, ::nvim_oxi::lua::Error> {
62                // SAFETY: The caller (nvim-oxi framework) guarantees that:
63                // 1. `lstate` is a valid pointer to an initialized Lua state
64                // 2. The Lua stack has at least one value to pop
65                unsafe {
66                    let obj = ::nvim_oxi::Object::pop(lstate)?;
67                    <Self as ::nvim_oxi::conversion::FromObject>::from_object(obj)
68                        .map_err(::nvim_oxi::lua::Error::pop_error_from_err::<Self, _>)
69                }
70            }
71        }
72    };
73}
74
75/// Turns a Rust function into a [`nvim_oxi::Object`] [`nvim_oxi::Function`].
76#[macro_export]
77macro_rules! fn_from {
78    // Plain function path
79    ($path:path) => {
80        ::nvim_oxi::Object::from(::nvim_oxi::Function::from_fn($path))
81    };
82    // Fallback: forward any tokens (supports calls like `Type::method(())`)
83    ($($tokens:tt)+) => {
84        ::nvim_oxi::Object::from(::nvim_oxi::Function::from_fn($($tokens)+))
85    };
86}
87
88#[cfg(test)]
89mod tests {
90    use nvim_oxi::Dictionary;
91    use nvim_oxi::Object;
92    use test_that::prelude::*;
93
94    use crate::dict::DictionaryExt;
95
96    #[test]
97    fn test_dict_macro_empty_creates_empty_dictionary() {
98        let actual = dict!();
99        assert_eq!(actual.len(), 0);
100    }
101
102    #[test]
103    fn test_dict_macro_creates_a_dictionary_with_basic_key_value_pairs() {
104        let actual = dict! { "foo": 1, bar: "baz", "num": 3_i64 };
105        let expected = Dictionary::from_iter([
106            ("bar", Object::from("baz")),
107            ("foo", Object::from(1)),
108            ("num", Object::from(3_i64)),
109        ]);
110        assert_eq!(actual, expected);
111    }
112
113    #[test]
114    fn test_dict_macro_creates_nested_dictionaries() {
115        let k = String::from("alpha");
116        let inner = dict! { inner_key: "value" };
117        let actual = dict! { (k): 10_i64, "beta": inner.clone() };
118        let expected = Dictionary::from_iter([("alpha", Object::from(10_i64)), ("beta", Object::from(inner))]);
119        assert_eq!(actual, expected);
120    }
121
122    #[test]
123    fn test_dictionary_ext_get_t_when_key_exists_returns_typed_value() {
124        let dict = dict! { "foo": "42" };
125        assert_that!(
126            (dict.get_t::<nvim_oxi::String>("bar")).map(|_| ()),
127            err(result_of!(
128                |err: &rootcause::Report| err.format_current_context().to_string(),
129                eq("missing dict value")
130            ))
131        );
132        assert_eq!(dict.get_t::<nvim_oxi::String>("foo").unwrap(), "42");
133
134        let dict = dict! { "foo": 42 };
135        assert_that!(
136            (dict.get_t::<nvim_oxi::String>("foo")).map(|_| ()),
137            err(result_of!(
138                |err: &rootcause::Report| err.format_current_context().to_string(),
139                eq("unexpected object kind")
140            ))
141        );
142    }
143
144    #[test]
145    fn test_dictionary_ext_get_dict_when_key_exists_returns_dictionary() {
146        let dict = dict! { "foo": "42" };
147        assert_eq!(dict.get_dict(&["bar"]).unwrap(), None);
148
149        let dict = dict! { "foo": 42 };
150        assert_that!(
151            (dict.get_dict(&["foo"])).map(|_| ()),
152            err(result_of!(
153                |err: &rootcause::Report| err.format_current_context().to_string(),
154                eq("unexpected object kind")
155            ))
156        );
157
158        let expected = dict! { "bar": "42" };
159        let dict = dict! { "foo": expected.clone() };
160        assert_eq!(dict.get_dict(&["foo"]).unwrap(), Some(expected));
161    }
162}