nautilus_core/python/
parsing.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! JSON / string parsing helpers for Python inputs.
17
18use pyo3::{
19    exceptions::PyKeyError,
20    prelude::*,
21    types::{PyDict, PyList},
22};
23
24/// Helper function to get a required string value from a Python dictionary.
25///
26/// # Returns
27///
28/// Returns the extracted string value or a `PyErr` if the key is missing or extraction fails.
29///
30/// # Errors
31///
32/// Returns `PyErr` if the key is missing or value extraction fails.
33pub fn get_required_string(dict: &Bound<'_, PyDict>, key: &str) -> PyResult<String> {
34    dict.get_item(key)?
35        .ok_or_else(|| PyKeyError::new_err(format!("Missing required key: {key}")))?
36        .extract()
37}
38
39/// Helper function to get a required value from a Python dictionary and extract it.
40///
41/// # Returns
42///
43/// Returns the extracted value or a `PyErr` if the key is missing or extraction fails.
44///
45/// # Errors
46///
47/// Returns `PyErr` if the key is missing or value extraction fails.
48pub fn get_required<T>(dict: &Bound<'_, PyDict>, key: &str) -> PyResult<T>
49where
50    T: for<'a, 'py> FromPyObject<'a, 'py>,
51    for<'a, 'py> PyErr: From<<T as FromPyObject<'a, 'py>>::Error>,
52{
53    dict.get_item(key)?
54        .ok_or_else(|| PyKeyError::new_err(format!("Missing required key: {key}")))?
55        .extract()
56        .map_err(PyErr::from)
57}
58
59/// Helper function to get an optional value from a Python dictionary.
60///
61/// # Returns
62///
63/// Returns Some(value) if the key exists and extraction succeeds, None if the key is missing
64/// or if the value is Python None, or a `PyErr` if extraction fails.
65///
66/// # Errors
67///
68/// Returns `PyErr` if value extraction fails (but not if the key is missing or value is None).
69pub fn get_optional<T>(dict: &Bound<'_, PyDict>, key: &str) -> PyResult<Option<T>>
70where
71    T: for<'a, 'py> FromPyObject<'a, 'py>,
72    for<'a, 'py> PyErr: From<<T as FromPyObject<'a, 'py>>::Error>,
73{
74    match dict.get_item(key)? {
75        Some(value) => {
76            if value.is_none() {
77                Ok(None)
78            } else {
79                value.extract().map(Some).map_err(PyErr::from)
80            }
81        }
82        None => Ok(None),
83    }
84}
85
86/// Helper function to get a required value, parse it with a closure, and handle parse errors.
87///
88/// # Returns
89///
90/// Returns the parsed value or a `PyErr` if the key is missing, extraction fails, or parsing fails.
91///
92/// # Errors
93///
94/// Returns `PyErr` if the key is missing, value extraction fails, or parsing fails.
95pub fn get_required_parsed<T, F>(dict: &Bound<'_, PyDict>, key: &str, parser: F) -> PyResult<T>
96where
97    F: FnOnce(String) -> Result<T, String>,
98{
99    let value_str = get_required_string(dict, key)?;
100    parser(value_str).map_err(|e| PyKeyError::new_err(format!("Failed to parse {key}: {e}")))
101}
102
103/// Helper function to get an optional value, parse it with a closure, and handle parse errors.
104///
105/// # Returns
106///
107/// Returns `Some(parsed_value)` if the key exists and parsing succeeds, None if the key is missing
108/// or if the value is Python None, or a `PyErr` if extraction or parsing fails.
109///
110/// # Errors
111///
112/// Returns `PyErr` if value extraction or parsing fails (but not if the key is missing or value is None).
113pub fn get_optional_parsed<T, F>(
114    dict: &Bound<'_, PyDict>,
115    key: &str,
116    parser: F,
117) -> PyResult<Option<T>>
118where
119    F: FnOnce(String) -> Result<T, String>,
120{
121    match dict.get_item(key)? {
122        Some(value) => {
123            if value.is_none() {
124                Ok(None)
125            } else {
126                let value_str: String = value.extract()?;
127                parser(value_str)
128                    .map(Some)
129                    .map_err(|e| PyKeyError::new_err(format!("Failed to parse {key}: {e}")))
130            }
131        }
132        None => Ok(None),
133    }
134}
135
136/// Helper function to get a required `PyList` from a Python dictionary.
137///
138/// # Returns
139///
140/// Returns the extracted `PyList` or a `PyErr` if the key is missing or extraction fails.
141///
142/// # Errors
143///
144/// Returns `PyErr` if the key is missing or value extraction fails.
145pub fn get_required_list<'py>(
146    dict: &Bound<'py, PyDict>,
147    key: &str,
148) -> PyResult<Bound<'py, PyList>> {
149    dict.get_item(key)?
150        .ok_or_else(|| PyKeyError::new_err(format!("Missing required key: {key}")))?
151        .downcast_into()
152        .map_err(Into::into)
153}