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>(
116 dict: &Bound<'_, PyDict>,
117 key: &str,
118 parser: F,
119) -> PyResult<Option<T>>
120where
121 F: FnOnce(String) -> Result<T, String>,
122{
123 get_optional::<String>(dict, key)?
124 .map(parser)
125 .transpose()
126 .map_err(|e| to_pyvalue_err(format!("Failed to parse '{key}': {e}")))
127}
128
129pub fn get_required_list<'py>(
139 dict: &Bound<'py, PyDict>,
140 key: &str,
141) -> PyResult<Bound<'py, PyList>> {
142 dict.get_item(key)?
143 .ok_or_else(|| to_pykey_err(format!("Missing required key: {key}")))?
144 .cast_into()
145 .map_err(Into::into)
146}
147
148#[cfg(test)]
149mod tests {
150 use std::sync::Once;
151
152 use pyo3::exceptions::{PyKeyError, PyTypeError, PyValueError};
153 use rstest::rstest;
154
155 use super::*;
156
157 fn ensure_python_initialized() {
158 static INIT: Once = Once::new();
159 INIT.call_once(Python::initialize);
160 }
161
162 #[rstest]
163 fn test_get_required_string() {
164 ensure_python_initialized();
165
166 Python::attach(|py| {
167 let dict = PyDict::new(py);
168 dict.set_item("name", "nautilus").unwrap();
169
170 let value = get_required_string(&dict, "name").unwrap();
171 let error = get_required_string(&dict, "missing").unwrap_err();
172
173 assert_eq!(value, "nautilus");
174 assert!(error.is_instance_of::<PyKeyError>(py));
175 assert_eq!(
176 error.value(py).to_string(),
177 "'Missing required key: missing'"
178 );
179 });
180 }
181
182 #[rstest]
183 fn test_get_required() {
184 ensure_python_initialized();
185
186 Python::attach(|py| {
187 let dict = PyDict::new(py);
188 dict.set_item("quantity", 125_u64).unwrap();
189
190 let value = get_required::<u64>(&dict, "quantity").unwrap();
191 let missing = get_required::<u64>(&dict, "missing").unwrap_err();
192 dict.set_item("quantity", "invalid").unwrap();
193 let invalid = get_required::<u64>(&dict, "quantity").unwrap_err();
194
195 assert_eq!(value, 125);
196 assert!(missing.is_instance_of::<PyKeyError>(py));
197 assert_eq!(
198 missing.value(py).to_string(),
199 "'Missing required key: missing'"
200 );
201 assert!(invalid.is_instance_of::<PyTypeError>(py));
202 });
203 }
204
205 #[rstest]
206 fn test_get_required_parsed_and_list() {
207 ensure_python_initialized();
208
209 Python::attach(|py| {
210 let dict = PyDict::new(py);
211 dict.set_item("limit", "250").unwrap();
212 dict.set_item("levels", PyList::new(py, [2_u64, 5, 8]).unwrap())
213 .unwrap();
214
215 let parsed = get_required_parsed(&dict, "limit", |value| {
216 value.parse::<u64>().map_err(|e| e.to_string())
217 })
218 .unwrap();
219 let levels = get_required_list(&dict, "levels")
220 .unwrap()
221 .extract::<Vec<u64>>()
222 .unwrap();
223 dict.set_item("limit", "invalid").unwrap();
224 let invalid =
225 get_required_parsed::<u64, _>(&dict, "limit", |_| Err("not a number".to_string()))
226 .unwrap_err();
227 let missing = get_required_list(&dict, "missing").unwrap_err();
228
229 assert_eq!(parsed, 250);
230 assert_eq!(levels, [2, 5, 8]);
231 assert!(invalid.is_instance_of::<PyValueError>(py));
232 assert_eq!(
233 invalid.value(py).to_string(),
234 "Failed to parse 'limit': not a number"
235 );
236 assert!(missing.is_instance_of::<PyKeyError>(py));
237 assert_eq!(
238 missing.value(py).to_string(),
239 "'Missing required key: missing'"
240 );
241 });
242 }
243
244 #[rstest]
245 fn test_get_optional_parsed() {
246 ensure_python_initialized();
247
248 Python::attach(|py| {
249 let dict = PyDict::new(py);
250 dict.set_item("value", "42").unwrap();
251 let parsed = get_optional_parsed(&dict, "value", |value| {
252 value.parse::<u64>().map_err(|e| e.to_string())
253 })
254 .unwrap();
255 let missing = get_optional_parsed(&dict, "missing", |value| {
256 value.parse::<u64>().map_err(|e| e.to_string())
257 })
258 .unwrap();
259
260 dict.set_item("value", py.None()).unwrap();
261 let none = get_optional_parsed(&dict, "value", |value| {
262 value.parse::<u64>().map_err(|e| e.to_string())
263 })
264 .unwrap();
265
266 dict.set_item("value", "invalid").unwrap();
267 let error =
268 get_optional_parsed::<u64, _>(&dict, "value", |_| Err("not a number".to_string()))
269 .unwrap_err();
270
271 assert_eq!(parsed, Some(42));
272 assert_eq!(missing, None);
273 assert_eq!(none, None);
274 assert!(error.is_instance_of::<PyValueError>(py));
275 assert_eq!(
276 error.value(py).to_string(),
277 "Failed to parse 'value': not a number"
278 );
279 });
280 }
281}