1use pyo3::{
19 prelude::*,
20 types::{PyDict, PyList},
21};
22
23use super::{to_pykey_err, to_pyvalue_err};
24
25pub fn get_required_string(dict: &Bound<'_, PyDict>, key: &str) -> PyResult<String> {
35 dict.get_item(key)?
36 .ok_or_else(|| to_pykey_err(format!("Missing required key: {key}")))?
37 .extract()
38}
39
40pub fn get_required<T>(dict: &Bound<'_, PyDict>, key: &str) -> PyResult<T>
50where
51 T: for<'a, 'py> FromPyObject<'a, 'py>,
52 for<'a, 'py> PyErr: From<<T as FromPyObject<'a, 'py>>::Error>,
53{
54 dict.get_item(key)?
55 .ok_or_else(|| to_pykey_err(format!("Missing required key: {key}")))?
56 .extract()
57 .map_err(PyErr::from)
58}
59
60#[inline]
71pub fn get_optional<T>(dict: &Bound<'_, PyDict>, key: &str) -> PyResult<Option<T>>
72where
73 T: for<'a, 'py> FromPyObject<'a, 'py>,
74 for<'a, 'py> PyErr: From<<T as FromPyObject<'a, 'py>>::Error>,
75{
76 match dict.get_item(key)? {
77 Some(value) => {
78 if value.is_none() {
79 Ok(None)
80 } else {
81 value.extract().map(Some).map_err(PyErr::from)
82 }
83 }
84 None => Ok(None),
85 }
86}
87
88pub fn get_required_parsed<T, F>(dict: &Bound<'_, PyDict>, key: &str, parser: F) -> PyResult<T>
98where
99 F: FnOnce(String) -> Result<T, String>,
100{
101 let value_str = get_required_string(dict, key)?;
102 parser(value_str).map_err(|e| to_pyvalue_err(format!("Failed to parse '{key}': {e}")))
103}
104
105pub fn get_optional_parsed<T, F>(
117 dict: &Bound<'_, PyDict>,
118 key: &str,
119 parser: F,
120) -> PyResult<Option<T>>
121where
122 F: FnOnce(String) -> Result<T, String>,
123{
124 get_optional::<String>(dict, key)?
125 .map(parser)
126 .transpose()
127 .map_err(|e| to_pyvalue_err(format!("Failed to parse '{key}': {e}")))
128}
129
130pub fn get_required_list<'py>(
140 dict: &Bound<'py, PyDict>,
141 key: &str,
142) -> PyResult<Bound<'py, PyList>> {
143 dict.get_item(key)?
144 .ok_or_else(|| to_pykey_err(format!("Missing required key: {key}")))?
145 .cast_into()
146 .map_err(Into::into)
147}
148
149#[cfg(test)]
150mod tests {
151 use std::sync::Once;
152
153 use pyo3::exceptions::{PyKeyError, PyTypeError, PyValueError};
154 use rstest::rstest;
155
156 use super::*;
157
158 fn ensure_python_initialized() {
159 static INIT: Once = Once::new();
160 INIT.call_once(Python::initialize);
161 }
162
163 #[rstest]
164 fn test_get_required_string() {
165 ensure_python_initialized();
166
167 Python::attach(|py| {
168 let dict = PyDict::new(py);
169 dict.set_item("name", "nautilus").unwrap();
170
171 let value = get_required_string(&dict, "name").unwrap();
172 let error = get_required_string(&dict, "missing").unwrap_err();
173
174 assert_eq!(value, "nautilus");
175 assert!(error.is_instance_of::<PyKeyError>(py));
176 assert_eq!(
177 error.value(py).to_string(),
178 "'Missing required key: missing'"
179 );
180 });
181 }
182
183 #[rstest]
184 fn test_get_required() {
185 ensure_python_initialized();
186
187 Python::attach(|py| {
188 let dict = PyDict::new(py);
189 dict.set_item("quantity", 125_u64).unwrap();
190
191 let value = get_required::<u64>(&dict, "quantity").unwrap();
192 let missing = get_required::<u64>(&dict, "missing").unwrap_err();
193 dict.set_item("quantity", "invalid").unwrap();
194 let invalid = get_required::<u64>(&dict, "quantity").unwrap_err();
195
196 assert_eq!(value, 125);
197 assert!(missing.is_instance_of::<PyKeyError>(py));
198 assert_eq!(
199 missing.value(py).to_string(),
200 "'Missing required key: missing'"
201 );
202 assert!(invalid.is_instance_of::<PyTypeError>(py));
203 });
204 }
205
206 #[rstest]
207 fn test_get_required_parsed_and_list() {
208 ensure_python_initialized();
209
210 Python::attach(|py| {
211 let dict = PyDict::new(py);
212 dict.set_item("limit", "250").unwrap();
213 dict.set_item("levels", PyList::new(py, [2_u64, 5, 8]).unwrap())
214 .unwrap();
215
216 let parsed = get_required_parsed(&dict, "limit", |value| {
217 value.parse::<u64>().map_err(|e| e.to_string())
218 })
219 .unwrap();
220 let levels = get_required_list(&dict, "levels")
221 .unwrap()
222 .extract::<Vec<u64>>()
223 .unwrap();
224 dict.set_item("limit", "invalid").unwrap();
225 let invalid =
226 get_required_parsed::<u64, _>(&dict, "limit", |_| Err("not a number".to_string()))
227 .unwrap_err();
228 let missing = get_required_list(&dict, "missing").unwrap_err();
229
230 assert_eq!(parsed, 250);
231 assert_eq!(levels, [2, 5, 8]);
232 assert!(invalid.is_instance_of::<PyValueError>(py));
233 assert_eq!(
234 invalid.value(py).to_string(),
235 "Failed to parse 'limit': not a number"
236 );
237 assert!(missing.is_instance_of::<PyKeyError>(py));
238 assert_eq!(
239 missing.value(py).to_string(),
240 "'Missing required key: missing'"
241 );
242 });
243 }
244
245 #[rstest]
246 fn test_get_optional_parsed() {
247 ensure_python_initialized();
248
249 Python::attach(|py| {
250 let dict = PyDict::new(py);
251 dict.set_item("value", "42").unwrap();
252 let parsed = get_optional_parsed(&dict, "value", |value| {
253 value.parse::<u64>().map_err(|e| e.to_string())
254 })
255 .unwrap();
256 let missing = get_optional_parsed(&dict, "missing", |value| {
257 value.parse::<u64>().map_err(|e| e.to_string())
258 })
259 .unwrap();
260
261 dict.set_item("value", py.None()).unwrap();
262 let none = get_optional_parsed(&dict, "value", |value| {
263 value.parse::<u64>().map_err(|e| e.to_string())
264 })
265 .unwrap();
266
267 dict.set_item("value", "invalid").unwrap();
268 let error =
269 get_optional_parsed::<u64, _>(&dict, "value", |_| Err("not a number".to_string()))
270 .unwrap_err();
271
272 assert_eq!(parsed, Some(42));
273 assert_eq!(missing, None);
274 assert_eq!(none, None);
275 assert!(error.is_instance_of::<PyValueError>(py));
276 assert_eq!(
277 error.value(py).to_string(),
278 "Failed to parse 'value': not a number"
279 );
280 });
281 }
282}