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/// Turns a Rust function into a [`nvim_oxi::Object`] [`nvim_oxi::Function`].
48#[macro_export]
49macro_rules! fn_from {
50    // Plain function path
51    ($path:path) => {
52        ::nvim_oxi::Object::from(::nvim_oxi::Function::from_fn($path))
53    };
54    // Fallback: forward any tokens (supports calls like `Type::method(())`)
55    ($($tokens:tt)+) => {
56        ::nvim_oxi::Object::from(::nvim_oxi::Function::from_fn($($tokens)+))
57    };
58}
59
60#[cfg(test)]
61mod tests {
62    use nvim_oxi::Dictionary;
63    use nvim_oxi::Object;
64
65    use crate::dict::DictionaryExt as _;
66
67    #[test]
68    fn dict_macro_empty_creates_empty_dictionary() {
69        let actual = dict!();
70        assert_eq!(actual.len(), 0);
71    }
72
73    #[test]
74    fn dict_macro_creates_a_dictionary_with_basic_key_value_pairs() {
75        let actual = dict! { "foo": 1, bar: "baz", "num": 3i64 };
76        let expected = Dictionary::from_iter([
77            ("bar", Object::from("baz")),
78            ("foo", Object::from(1)),
79            ("num", Object::from(3i64)),
80        ]);
81        assert_eq!(actual, expected);
82    }
83
84    #[test]
85    fn dict_macro_creates_nested_dictionaries() {
86        let k = String::from("alpha");
87        let inner = dict! { inner_key: "value" };
88        let actual = dict! { (k): 10i64, "beta": inner.clone() };
89        let expected = Dictionary::from_iter([("alpha", Object::from(10i64)), ("beta", Object::from(inner))]);
90        assert_eq!(actual, expected);
91    }
92
93    #[test]
94    fn dictionary_ext_get_t_works_as_expected() {
95        let dict = dict! { "foo": "42" };
96        let msg = dict.get_t::<nvim_oxi::String>("bar").unwrap_err().to_string();
97        assert!(msg.starts_with("missing dict value |"), "actual: {msg}");
98        assert!(msg.contains("query=[\n    \"bar\",\n]"), "actual: {msg}");
99        assert!(msg.contains("dict={ foo: \"42\" }"), "actual: {msg}");
100        assert_eq!(dict.get_t::<nvim_oxi::String>("foo").unwrap(), "42");
101
102        let dict = dict! { "foo": 42 };
103        assert_eq!(
104            dict.get_t::<nvim_oxi::String>("foo").unwrap_err().to_string(),
105            r#"value 42 of key "foo" in dict { foo: 42 } is Integer but String was expected"#
106        );
107    }
108
109    #[test]
110    fn dictionary_ext_get_dict_works_as_expected() {
111        let dict = dict! { "foo": "42" };
112        assert_eq!(dict.get_dict(&["bar"]).unwrap(), None);
113
114        let dict = dict! { "foo": 42 };
115        assert_eq!(
116            dict.get_dict(&["foo"]).unwrap_err().to_string(),
117            r#"value 42 of key "foo" in dict { foo: 42 } is Integer but Dictionary was expected"#
118        );
119
120        let expected = dict! { "bar": "42" };
121        let dict = dict! { "foo": expected.clone() };
122        assert_eq!(dict.get_dict(&["foo"]).unwrap(), Some(expected));
123    }
124}