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    #[must_use]
41    pub fn new() -> Self {
42        Self(IndexMap::new())
43    }
44
45    /// Creates `Params` from an `IndexMap`.
46    #[must_use]
47    pub fn from_index_map(map: IndexMap<String, Value>) -> Self {
48        Self(map)
49    }
50
51    /// Extracts a `u64` value from the params map.
52    ///
53    /// Returns `None` if the key is missing or the value cannot be converted to `u64`.
54    #[must_use]
55    pub fn get_u64(&self, key: &str) -> Option<u64> {
56        self.get(key).and_then(Value::as_u64)
57    }
58
59    /// Extracts an `i64` value from the params map.
60    ///
61    /// Returns `None` if the key is missing or the value cannot be converted to `i64`.
62    #[must_use]
63    pub fn get_i64(&self, key: &str) -> Option<i64> {
64        self.get(key).and_then(Value::as_i64)
65    }
66
67    /// Extracts a `usize` value from the params map.
68    ///
69    /// Returns `None` if the key is missing or the value cannot be converted to `usize`.
70    #[must_use]
71    pub fn get_usize(&self, key: &str) -> Option<usize> {
72        self.get(key)
73            .and_then(Value::as_u64)
74            .and_then(|n| usize::try_from(n).ok())
75    }
76
77    /// Extracts a string value from the params map.
78    ///
79    /// Returns `None` if the key is missing or the value is not a string.
80    #[must_use]
81    pub fn get_str(&self, key: &str) -> Option<&str> {
82        self.get(key).and_then(|v| v.as_str())
83    }
84
85    /// Extracts a boolean value from the params map.
86    ///
87    /// Returns `None` if the key is missing or the value is not a boolean.
88    #[must_use]
89    pub fn get_bool(&self, key: &str) -> Option<bool> {
90        self.get(key).and_then(Value::as_bool)
91    }
92
93    /// Extracts a `f64` value from the params map.
94    ///
95    /// Returns `None` if the key is missing or the value cannot be converted to `f64`.
96    #[must_use]
97    pub fn get_f64(&self, key: &str) -> Option<f64> {
98        self.get(key).and_then(Value::as_f64)
99    }
100
101    #[cfg(feature = "python")]
102    /// Converts `Params` to a Python dict.
103    ///
104    /// # Errors
105    ///
106    /// Returns a `PyErr` if conversion of any value fails.
107    pub fn to_pydict(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::Py<pyo3::types::PyDict>> {
108        crate::python::params::params_to_pydict(py, self)
109    }
110}
111
112impl Deref for Params {
113    type Target = IndexMap<String, Value>;
114
115    fn deref(&self) -> &Self::Target {
116        &self.0
117    }
118}
119
120impl DerefMut for Params {
121    fn deref_mut(&mut self) -> &mut Self::Target {
122        &mut self.0
123    }
124}
125
126impl<'a> IntoIterator for &'a Params {
127    type Item = (&'a String, &'a Value);
128    type IntoIter = indexmap::map::Iter<'a, String, Value>;
129
130    fn into_iter(self) -> Self::IntoIter {
131        self.0.iter()
132    }
133}
134
135#[cfg(feature = "python")]
136/// Converts a Python dict to `Params`.
137///
138/// This is a convenience function that wraps `pydict_to_params`.
139///
140/// # Errors
141///
142/// Returns a `PyErr` if:
143/// - the dict cannot be serialized to JSON
144/// - the JSON is not a valid object
145pub fn from_pydict(
146    py: pyo3::Python<'_>,
147    dict: &pyo3::Py<pyo3::types::PyDict>,
148) -> pyo3::PyResult<Option<Params>> {
149    crate::python::params::pydict_to_params(py, dict)
150}
151
152#[cfg(test)]
153mod tests {
154    use rstest::*;
155    use serde_json::json;
156
157    use super::Params;
158
159    fn create_test_params() -> Params {
160        let mut params = Params::new();
161        params.insert("u64_val".to_string(), json!(42u64));
162        params.insert("i64_val".to_string(), json!(-100i64));
163        params.insert("usize_val".to_string(), json!(5u64));
164        params.insert("str_val".to_string(), json!("hello"));
165        params.insert("bool_val".to_string(), json!(true));
166        params.insert("f64_val".to_string(), json!(2.5));
167        params
168    }
169
170    #[rstest]
171    fn test_params_option_get_u64() {
172        let params = Some(create_test_params());
173        assert_eq!(params.as_ref().and_then(|p| p.get_u64("u64_val")), Some(42));
174        assert_eq!(params.as_ref().and_then(|p| p.get_u64("missing")), None);
175        assert_eq!(params.as_ref().and_then(|p| p.get_u64("str_val")), None);
176    }
177
178    #[rstest]
179    fn test_params_option_get_i64() {
180        let params = Some(create_test_params());
181        assert_eq!(
182            params.as_ref().and_then(|p| p.get_i64("i64_val")),
183            Some(-100)
184        );
185        assert_eq!(params.as_ref().and_then(|p| p.get_i64("missing")), None);
186    }
187
188    #[rstest]
189    fn test_params_option_get_usize() {
190        let params = Some(create_test_params());
191        assert_eq!(
192            params.as_ref().and_then(|p| p.get_usize("usize_val")),
193            Some(5)
194        );
195        assert_eq!(params.as_ref().and_then(|p| p.get_usize("missing")), None);
196    }
197
198    #[rstest]
199    fn test_params_option_get_str() {
200        let params = Some(create_test_params());
201        assert_eq!(
202            params.as_ref().and_then(|p| p.get_str("str_val")),
203            Some("hello")
204        );
205        assert_eq!(params.as_ref().and_then(|p| p.get_str("missing")), None);
206        assert_eq!(params.as_ref().and_then(|p| p.get_str("u64_val")), None);
207    }
208
209    #[rstest]
210    fn test_params_option_get_bool() {
211        let params = Some(create_test_params());
212        assert_eq!(
213            params.as_ref().and_then(|p| p.get_bool("bool_val")),
214            Some(true)
215        );
216        assert_eq!(params.as_ref().and_then(|p| p.get_bool("missing")), None);
217    }
218
219    #[rstest]
220    fn test_params_option_get_f64() {
221        let params = Some(create_test_params());
222        assert_eq!(
223            params.as_ref().and_then(|p| p.get_f64("f64_val")),
224            Some(2.5)
225        );
226        assert_eq!(params.as_ref().and_then(|p| p.get_f64("missing")), None);
227    }
228
229    #[rstest]
230    fn test_params_option_none() {
231        let params: Option<Params> = None;
232        assert_eq!(params.as_ref().and_then(|p| p.get_u64("any")), None);
233        assert_eq!(params.as_ref().and_then(|p| p.get_str("any")), None);
234    }
235
236    #[rstest]
237    fn test_params_ref_get_u64() {
238        let params = create_test_params();
239        assert_eq!(params.get_u64("u64_val"), Some(42));
240        assert_eq!(params.get_u64("missing"), None);
241    }
242
243    #[rstest]
244    fn test_params_ref_get_usize() {
245        let params = create_test_params();
246        assert_eq!(params.get_usize("usize_val"), Some(5));
247        assert_eq!(params.get_usize("missing"), None);
248    }
249
250    #[rstest]
251    fn test_params_ref_get_usize_respects_target_width() {
252        let mut params = Params::new();
253        params.insert("u32_max".to_string(), json!(u32::MAX));
254        params.insert("u32_overflow".to_string(), json!(4_294_967_296_u64));
255
256        assert_eq!(params.get_usize("u32_max"), Some(4_294_967_295_usize));
257        #[cfg(target_pointer_width = "32")]
258        assert_eq!(params.get_usize("u32_overflow"), None);
259        #[cfg(target_pointer_width = "64")]
260        assert_eq!(params.get_usize("u32_overflow"), Some(4_294_967_296));
261    }
262
263    #[rstest]
264    fn test_params_ref_get_str() {
265        let params = create_test_params();
266        assert_eq!(params.get_str("str_val"), Some("hello"));
267        assert_eq!(params.get_str("missing"), None);
268    }
269
270    #[rstest]
271    fn test_submit_tries_pattern() {
272        let mut params = Params::new();
273        params.insert("submit_tries".to_string(), json!(3u64));
274        let cmd_params = Some(params);
275
276        let submit_tries = cmd_params
277            .as_ref()
278            .and_then(|p| p.get_usize("submit_tries"))
279            .filter(|&n| n > 0);
280
281        assert_eq!(submit_tries, Some(3));
282    }
283
284    #[rstest]
285    fn test_submit_tries_pattern_zero_filtered() {
286        let mut params = Params::new();
287        params.insert("submit_tries".to_string(), json!(0u64));
288        let cmd_params = Some(params);
289
290        let submit_tries = cmd_params
291            .as_ref()
292            .and_then(|p| p.get_usize("submit_tries"))
293            .filter(|&n| n > 0);
294
295        assert_eq!(submit_tries, None);
296    }
297
298    #[rstest]
299    fn test_submit_tries_pattern_missing() {
300        let cmd_params: Option<Params> = None;
301
302        let submit_tries = cmd_params
303            .as_ref()
304            .and_then(|p| p.get_usize("submit_tries"))
305            .filter(|&n| n > 0);
306
307        assert_eq!(submit_tries, None);
308    }
309}