1use nvim_oxi::Dictionary;
4use nvim_oxi::ObjectKind;
5use rootcause::prelude::ResultExt;
6use rootcause::report;
7
8use crate::extract::OxiExtract;
9
10pub trait DictionaryExt {
12 fn get_t<T: OxiExtract>(&self, key: &str) -> rootcause::Result<T::Out>;
21
22 fn get_opt_t<T: OxiExtract>(&self, key: &str) -> rootcause::Result<Option<T::Out>>;
30
31 fn get_dict(&self, keys: &[&str]) -> rootcause::Result<Option<Dictionary>>;
39
40 fn get_required_dict(&self, keys: &[&str]) -> rootcause::Result<Dictionary>;
49}
50
51impl 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, ¤t, 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
84fn 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}