Skip to main content

ytil_noxi/
dict.rs

1//! Typed dictionary extraction helpers for Nvim objects.
2
3use nvim_oxi::Dictionary;
4use nvim_oxi::ObjectKind;
5use rootcause::prelude::ResultExt;
6use rootcause::report;
7
8use crate::extract::OxiExtract;
9
10/// Extension trait for [`Dictionary`] to provide typed getters.
11pub trait DictionaryExt {
12    /// Gets a required typed value from the dictionary using the [`OxiExtract`] trait.
13    ///
14    /// Fails if the key is absent.
15    ///
16    ///
17    /// # Errors
18    /// - The key is missing.
19    /// - The value exists but cannot be converted to the requested type (unexpected kind).
20    fn get_t<T: OxiExtract>(&self, key: &str) -> rootcause::Result<T::Out>;
21
22    /// Gets an optional typed value from the dictionary using the [`OxiExtract`] trait.
23    ///
24    /// Returns `Ok(None)` if the key is absent instead of treating it as an error.
25    ///
26    ///
27    /// # Errors
28    /// - The value exists but cannot be converted to the requested type (unexpected kind).
29    fn get_opt_t<T: OxiExtract>(&self, key: &str) -> rootcause::Result<Option<T::Out>>;
30
31    /// Gets an optional nested [`Dictionary`] by traversing a sequence of keys.
32    ///
33    /// Returns `Ok(None)` if any key in the path is absent.
34    ///
35    ///
36    /// # Errors
37    /// - A value is found for an intermediate key but it is not a [`Dictionary`] (unexpected kind).
38    fn get_dict(&self, keys: &[&str]) -> rootcause::Result<Option<Dictionary>>;
39
40    /// Gets a required nested [`Dictionary`] by traversing a sequence of keys.
41    ///
42    /// Fails if any key in the path is missing.
43    ///
44    ///
45    /// # Errors
46    /// - A key in the path is missing.
47    /// - A value is found for an intermediate key but it is not a [`Dictionary`] (unexpected kind).
48    fn get_required_dict(&self, keys: &[&str]) -> rootcause::Result<Dictionary>;
49}
50
51/// Implementation of [`DictionaryExt`] for [`Dictionary`] providing typed getters.
52impl DictionaryExt for Dictionary {
53    fn get_t<T: OxiExtract>(&self, key: &str) -> rootcause::Result<T::Out> {
54        let value = self.get(key).ok_or_else(|| no_value_matching(&[key], self))?;
55        T::extract_from_dict(key, value, self)
56    }
57
58    fn get_opt_t<T: OxiExtract>(&self, key: &str) -> rootcause::Result<Option<T::Out>> {
59        self.get(key)
60            .map(|value| T::extract_from_dict(key, value, self))
61            .transpose()
62    }
63
64    fn get_dict(&self, keys: &[&str]) -> rootcause::Result<Option<Dictionary>> {
65        let mut current = self.clone();
66
67        for key in keys {
68            let Some(obj) = current.get(key) else { return Ok(None) };
69            current = Self::try_from(obj.clone())
70                .context("unexpected object kind")
71                .attach_with(|| {
72                    crate::extract::unexpected_kind_error_msg(obj, key, &current, ObjectKind::Dictionary)
73                })?;
74        }
75
76        Ok(Some(current))
77    }
78
79    fn get_required_dict(&self, keys: &[&str]) -> rootcause::Result<Dictionary> {
80        self.get_dict(keys)?.ok_or_else(|| no_value_matching(keys, self))
81    }
82}
83
84/// Creates an error for missing value in [`Dictionary`].
85fn no_value_matching(query: &[&str], dict: &Dictionary) -> rootcause::Report {
86    report!("missing dict value").attach(format!("query={query:#?} dict={dict:#?}"))
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use crate::dict;
93
94    #[test]
95    fn get_t_missing_key_errors() {
96        let d = dict! { other: 1 };
97        assert2::assert!(let Err(err) = d.get_t::<nvim_oxi::String>("name"));
98        assert_eq!(err.format_current_context().to_string(), "missing dict value");
99    }
100
101    #[test]
102    fn get_opt_t_missing_key_ok_none() {
103        let d = dict! { other: 1 };
104        assert2::assert!(let Ok(v) = d.get_opt_t::<nvim_oxi::String>("name"));
105        assert!(v.is_none());
106    }
107
108    #[test]
109    fn get_dict_missing_intermediate_returns_none() {
110        let d = dict! { root: dict! { level: dict! { value: 1 } } };
111        assert2::assert!(let Ok(v) = d.get_dict(&["root", "missing", "value"]));
112        assert!(v.is_none());
113    }
114
115    #[test]
116    fn get_dict_intermediate_wrong_type_errors() {
117        let d = dict! { root: dict! { leaf: 1 } };
118        assert2::assert!(let Err(err) = d.get_dict(&["root", "leaf", "value"]));
119        assert_eq!(err.format_current_context().to_string(), "unexpected object kind");
120    }
121
122    #[test]
123    fn get_required_dict_missing_errors() {
124        let d = dict! { root: dict! { leaf: 1 } };
125        assert2::assert!(let Err(err) = d.get_required_dict(&["root", "branch"]));
126        assert_eq!(err.format_current_context().to_string(), "missing dict value");
127    }
128}