1use color_eyre::eyre::Context;
4use color_eyre::eyre::eyre;
5use nvim_oxi::Dictionary;
6use nvim_oxi::ObjectKind;
7
8use crate::extract::OxiExtract;
9
10pub trait DictionaryExt {
12 fn get_t<T: OxiExtract>(&self, key: &str) -> color_eyre::Result<T::Out>;
21
22 fn get_opt_t<T: OxiExtract>(&self, key: &str) -> color_eyre::Result<Option<T::Out>>;
30
31 fn get_dict(&self, keys: &[&str]) -> color_eyre::Result<Option<Dictionary>>;
39
40 fn get_required_dict(&self, keys: &[&str]) -> color_eyre::Result<Dictionary>;
49}
50
51impl DictionaryExt for Dictionary {
53 fn get_t<T: OxiExtract>(&self, key: &str) -> color_eyre::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) -> color_eyre::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]) -> color_eyre::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()).with_context(|| {
70 crate::extract::unexpected_kind_error_msg(obj, key, ¤t, ObjectKind::Dictionary)
71 })?;
72 }
73
74 Ok(Some(current))
75 }
76
77 fn get_required_dict(&self, keys: &[&str]) -> color_eyre::Result<Dictionary> {
78 self.get_dict(keys)?.ok_or_else(|| no_value_matching(keys, self))
79 }
80}
81
82fn no_value_matching(query: &[&str], dict: &Dictionary) -> color_eyre::eyre::Error {
84 eyre!("missing dict value | query={query:#?} dict={dict:#?}")
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use crate::dict;
91
92 #[test]
93 fn get_t_missing_key_errors() {
94 let d = dict! { other: 1 };
95 assert2::let_assert!(Err(err) = d.get_t::<nvim_oxi::String>("name"));
96 let msg = err.to_string();
97 assert!(msg.starts_with("missing dict value |"), "actual: {msg}");
98 assert!(msg.contains("query=[\n \"name\",\n]"), "actual: {msg}");
99 }
100
101 #[test]
102 fn get_opt_t_missing_key_ok_none() {
103 let d = dict! { other: 1 };
104 assert2::let_assert!(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::let_assert!(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::let_assert!(Err(err) = d.get_dict(&["root", "leaf", "value"]));
119 let msg = err.to_string();
120 assert!(msg.contains(" is Integer but Dictionary was expected"), "actual: {msg}");
121 assert!(msg.contains("key \"leaf\""), "actual: {msg}");
122 }
123
124 #[test]
125 fn get_required_dict_missing_errors() {
126 let d = dict! { root: dict! { leaf: 1 } };
127 assert2::let_assert!(Err(err) = d.get_required_dict(&["root", "branch"]));
128 let msg = err.to_string();
129 assert!(msg.starts_with("missing dict value |"), "actual: {msg}");
130 assert!(
131 msg.contains("query=[\n \"root\",\n \"branch\",\n]"),
132 "actual: {msg}"
133 );
134 }
135}