nautilus_core/python/
parsing.rs1use 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, 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_optional_parsed() {
184 ensure_python_initialized();
185
186 Python::attach(|py| {
187 let dict = PyDict::new(py);
188 dict.set_item("value", "42").unwrap();
189 let parsed = get_optional_parsed(&dict, "value", |value| {
190 value.parse::<u64>().map_err(|e| e.to_string())
191 })
192 .unwrap();
193 let missing = get_optional_parsed(&dict, "missing", |value| {
194 value.parse::<u64>().map_err(|e| e.to_string())
195 })
196 .unwrap();
197
198 dict.set_item("value", py.None()).unwrap();
199 let none = get_optional_parsed(&dict, "value", |value| {
200 value.parse::<u64>().map_err(|e| e.to_string())
201 })
202 .unwrap();
203
204 dict.set_item("value", "invalid").unwrap();
205 let error =
206 get_optional_parsed::<u64, _>(&dict, "value", |_| Err("not a number".to_string()))
207 .unwrap_err();
208
209 assert_eq!(parsed, Some(42));
210 assert_eq!(missing, None);
211 assert_eq!(none, None);
212 assert!(error.is_instance_of::<PyValueError>(py));
213 assert_eq!(
214 error.value(py).to_string(),
215 "Failed to parse 'value': not a number"
216 );
217 });
218 }
219}