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