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 test_that::prelude::*;
92
93 use super::*;
94 use crate::dict;
95
96 #[test]
97 fn test_get_t_missing_key_errors() {
98 let d = dict! { other: 1 };
99 assert_that!(
100 (d.get_t::<nvim_oxi::String>("name")).map(|_| ()),
101 err(result_of!(
102 |err: &rootcause::Report| err.format_current_context().to_string(),
103 eq("missing dict value")
104 ))
105 );
106 }
107
108 #[test]
109 fn test_get_opt_t_missing_key_ok_none() {
110 let d = dict! { other: 1 };
111 assert_that!(d.get_opt_t::<nvim_oxi::String>("name"), ok(none()));
112 }
113
114 #[test]
115 fn test_get_dict_missing_intermediate_returns_none() {
116 let d = dict! { root: dict! { level: dict! { value: 1 } } };
117 assert_that!(d.get_dict(&["root", "missing", "value"]), ok(none()));
118 }
119
120 #[test]
121 fn test_get_dict_intermediate_wrong_type_errors() {
122 let d = dict! { root: dict! { leaf: 1 } };
123 assert_that!(
124 (d.get_dict(&["root", "leaf", "value"])).map(|_| ()),
125 err(result_of!(
126 |err: &rootcause::Report| err.format_current_context().to_string(),
127 eq("unexpected object kind")
128 ))
129 );
130 }
131
132 #[test]
133 fn test_get_required_dict_missing_errors() {
134 let d = dict! { root: dict! { leaf: 1 } };
135 assert_that!(
136 (d.get_required_dict(&["root", "branch"])).map(|_| ()),
137 err(result_of!(
138 |err: &rootcause::Report| err.format_current_context().to_string(),
139 eq("missing dict value")
140 ))
141 );
142 }
143}