Skip to main content

nautilus_core/
params.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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//! Generic parameter storage using `IndexMap<String, Value>`.
17//!
18//! This module provides a centralized definition of [`Params`] as a generic storage
19//! solution for `serde_json::Value` data, along with Python bindings.
20
21use std::ops::{Deref, DerefMut};
22
23use indexmap::IndexMap;
24use serde::{Deserialize, Serialize};
25use serde_json::Value;
26
27/// Newtype wrapper for generic parameter storage.
28///
29/// This represents a map of string keys to JSON values, used for passing
30/// adapter-specific configuration, metadata, and any generic key-value data.
31///
32/// `Params` uses `IndexMap` to preserve insertion order, which is important for
33/// consistent serialization and debugging.
34#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
35#[serde(transparent)]
36pub struct Params(IndexMap<String, Value>);
37
38impl Params {
39    /// Creates an empty `Params` map.
40    pub fn new() -> Self {
41        Self(IndexMap::new())
42    }
43
44    /// Creates `Params` from an `IndexMap`.
45    pub fn from_index_map(map: IndexMap<String, Value>) -> Self {
46        Self(map)
47    }
48
49    /// Extracts a `u64` value from the params map.
50    ///
51    /// Returns `None` if the key is missing or the value cannot be converted to `u64`.
52    pub fn get_u64(&self, key: &str) -> Option<u64> {
53        self.get(key).and_then(|v| v.as_u64())
54    }
55
56    /// Extracts an `i64` value from the params map.
57    ///
58    /// Returns `None` if the key is missing or the value cannot be converted to `i64`.
59    pub fn get_i64(&self, key: &str) -> Option<i64> {
60        self.get(key).and_then(|v| v.as_i64())
61    }
62
63    /// Extracts a `usize` value from the params map.
64    ///
65    /// Returns `None` if the key is missing or the value cannot be converted to `usize`.
66    pub fn get_usize(&self, key: &str) -> Option<usize> {
67        self.get(key).and_then(|v| v.as_u64()).map(|n| n as usize)
68    }
69
70    /// Extracts a string value from the params map.
71    ///
72    /// Returns `None` if the key is missing or the value is not a string.
73    pub fn get_str(&self, key: &str) -> Option<&str> {
74        self.get(key).and_then(|v| v.as_str())
75    }
76
77    /// Extracts a boolean value from the params map.
78    ///
79    /// Returns `None` if the key is missing or the value is not a boolean.
80    pub fn get_bool(&self, key: &str) -> Option<bool> {
81        self.get(key).and_then(|v| v.as_bool())
82    }
83
84    /// Extracts a `f64` value from the params map.
85    ///
86    /// Returns `None` if the key is missing or the value cannot be converted to `f64`.
87    pub fn get_f64(&self, key: &str) -> Option<f64> {
88        self.get(key).and_then(|v| v.as_f64())
89    }
90
91    #[cfg(feature = "python")]
92    /// Converts `Params` to a Python dict.
93    ///
94    /// # Errors
95    ///
96    /// Returns a `PyErr` if conversion of any value fails.
97    pub fn to_pydict(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::Py<pyo3::types::PyDict>> {
98        crate::python::params::params_to_pydict(py, self)
99    }
100}
101
102impl Deref for Params {
103    type Target = IndexMap<String, Value>;
104
105    fn deref(&self) -> &Self::Target {
106        &self.0
107    }
108}
109
110impl DerefMut for Params {
111    fn deref_mut(&mut self) -> &mut Self::Target {
112        &mut self.0
113    }
114}
115
116impl<'a> IntoIterator for &'a Params {
117    type Item = (&'a String, &'a Value);
118    type IntoIter = indexmap::map::Iter<'a, String, Value>;
119
120    fn into_iter(self) -> Self::IntoIter {
121        self.0.iter()
122    }
123}
124
125#[cfg(feature = "python")]
126/// Converts a Python dict to `Params`.
127///
128/// This is a convenience function that wraps `pydict_to_params`.
129///
130/// # Errors
131///
132/// Returns a `PyErr` if:
133/// - the dict cannot be serialized to JSON
134/// - the JSON is not a valid object
135pub fn from_pydict(
136    py: pyo3::Python<'_>,
137    dict: pyo3::Py<pyo3::types::PyDict>,
138) -> pyo3::PyResult<Option<Params>> {
139    crate::python::params::pydict_to_params(py, dict)
140}
141
142#[cfg(test)]
143mod tests {
144    use rstest::*;
145    use serde_json::json;
146
147    use super::Params;
148
149    fn create_test_params() -> Params {
150        let mut params = Params::new();
151        params.insert("u64_val".to_string(), json!(42u64));
152        params.insert("i64_val".to_string(), json!(-100i64));
153        params.insert("usize_val".to_string(), json!(5u64));
154        params.insert("str_val".to_string(), json!("hello"));
155        params.insert("bool_val".to_string(), json!(true));
156        params.insert("f64_val".to_string(), json!(2.5));
157        params
158    }
159
160    #[rstest]
161    fn test_params_option_get_u64() {
162        let params = Some(create_test_params());
163        assert_eq!(params.as_ref().and_then(|p| p.get_u64("u64_val")), Some(42));
164        assert_eq!(params.as_ref().and_then(|p| p.get_u64("missing")), None);
165        assert_eq!(params.as_ref().and_then(|p| p.get_u64("str_val")), None);
166    }
167
168    #[rstest]
169    fn test_params_option_get_i64() {
170        let params = Some(create_test_params());
171        assert_eq!(
172            params.as_ref().and_then(|p| p.get_i64("i64_val")),
173            Some(-100)
174        );
175        assert_eq!(params.as_ref().and_then(|p| p.get_i64("missing")), None);
176    }
177
178    #[rstest]
179    fn test_params_option_get_usize() {
180        let params = Some(create_test_params());
181        assert_eq!(
182            params.as_ref().and_then(|p| p.get_usize("usize_val")),
183            Some(5)
184        );
185        assert_eq!(params.as_ref().and_then(|p| p.get_usize("missing")), None);
186    }
187
188    #[rstest]
189    fn test_params_option_get_str() {
190        let params = Some(create_test_params());
191        assert_eq!(
192            params.as_ref().and_then(|p| p.get_str("str_val")),
193            Some("hello")
194        );
195        assert_eq!(params.as_ref().and_then(|p| p.get_str("missing")), None);
196        assert_eq!(params.as_ref().and_then(|p| p.get_str("u64_val")), None);
197    }
198
199    #[rstest]
200    fn test_params_option_get_bool() {
201        let params = Some(create_test_params());
202        assert_eq!(
203            params.as_ref().and_then(|p| p.get_bool("bool_val")),
204            Some(true)
205        );
206        assert_eq!(params.as_ref().and_then(|p| p.get_bool("missing")), None);
207    }
208
209    #[rstest]
210    fn test_params_option_get_f64() {
211        let params = Some(create_test_params());
212        assert_eq!(
213            params.as_ref().and_then(|p| p.get_f64("f64_val")),
214            Some(2.5)
215        );
216        assert_eq!(params.as_ref().and_then(|p| p.get_f64("missing")), None);
217    }
218
219    #[rstest]
220    fn test_params_option_none() {
221        let params: Option<Params> = None;
222        assert_eq!(params.as_ref().and_then(|p| p.get_u64("any")), None);
223        assert_eq!(params.as_ref().and_then(|p| p.get_str("any")), None);
224    }
225
226    #[rstest]
227    fn test_params_ref_get_u64() {
228        let params = create_test_params();
229        assert_eq!(params.get_u64("u64_val"), Some(42));
230        assert_eq!(params.get_u64("missing"), None);
231    }
232
233    #[rstest]
234    fn test_params_ref_get_usize() {
235        let params = create_test_params();
236        assert_eq!(params.get_usize("usize_val"), Some(5));
237        assert_eq!(params.get_usize("missing"), None);
238    }
239
240    #[rstest]
241    fn test_params_ref_get_str() {
242        let params = create_test_params();
243        assert_eq!(params.get_str("str_val"), Some("hello"));
244        assert_eq!(params.get_str("missing"), None);
245    }
246
247    #[rstest]
248    fn test_submit_tries_pattern() {
249        let mut params = Params::new();
250        params.insert("submit_tries".to_string(), json!(3u64));
251        let cmd_params = Some(params);
252
253        let submit_tries = cmd_params
254            .as_ref()
255            .and_then(|p| p.get_usize("submit_tries"))
256            .filter(|&n| n > 0);
257
258        assert_eq!(submit_tries, Some(3));
259    }
260
261    #[rstest]
262    fn test_submit_tries_pattern_zero_filtered() {
263        let mut params = Params::new();
264        params.insert("submit_tries".to_string(), json!(0u64));
265        let cmd_params = Some(params);
266
267        let submit_tries = cmd_params
268            .as_ref()
269            .and_then(|p| p.get_usize("submit_tries"))
270            .filter(|&n| n > 0);
271
272        assert_eq!(submit_tries, None);
273    }
274
275    #[rstest]
276    fn test_submit_tries_pattern_missing() {
277        let cmd_params: Option<Params> = None;
278
279        let submit_tries = cmd_params
280            .as_ref()
281            .and_then(|p| p.get_usize("submit_tries"))
282            .filter(|&n| n > 0);
283
284        assert_eq!(submit_tries, None);
285    }
286}