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/// `from_pydict` rejects integer values outside the signed or unsigned 64-bit range with
33/// `ValueError`, including values in nested containers.
34///
35/// `Params` uses `IndexMap` to preserve insertion order, which is important for
36/// consistent serialization and debugging.
37#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
38#[serde(transparent)]
39pub struct Params(IndexMap<String, Value>);
40
41impl Params {
42    /// Creates an empty `Params` map.
43    #[must_use]
44    pub fn new() -> Self {
45        Self(IndexMap::new())
46    }
47
48    /// Creates `Params` from an `IndexMap`.
49    #[must_use]
50    pub fn from_index_map(map: IndexMap<String, Value>) -> Self {
51        Self(map)
52    }
53
54    /// Extracts a `u64` value from the params map.
55    ///
56    /// Returns `None` if the key is missing or the value cannot be converted to `u64`.
57    #[must_use]
58    pub fn get_u64(&self, key: &str) -> Option<u64> {
59        self.get(key).and_then(Value::as_u64)
60    }
61
62    /// Extracts an `i64` value from the params map.
63    ///
64    /// Returns `None` if the key is missing or the value cannot be converted to `i64`.
65    #[must_use]
66    pub fn get_i64(&self, key: &str) -> Option<i64> {
67        self.get(key).and_then(Value::as_i64)
68    }
69
70    /// Extracts a `usize` value from the params map.
71    ///
72    /// Returns `None` if the key is missing or the value cannot be converted to `usize`.
73    #[must_use]
74    pub fn get_usize(&self, key: &str) -> Option<usize> {
75        self.get(key)
76            .and_then(Value::as_u64)
77            .and_then(|n| usize::try_from(n).ok())
78    }
79
80    /// Extracts a string value from the params map.
81    ///
82    /// Returns `None` if the key is missing or the value is not a string.
83    #[must_use]
84    pub fn get_str(&self, key: &str) -> Option<&str> {
85        self.get(key).and_then(|v| v.as_str())
86    }
87
88    /// Extracts a boolean value from the params map.
89    ///
90    /// Returns `None` if the key is missing or the value is not a boolean.
91    #[must_use]
92    pub fn get_bool(&self, key: &str) -> Option<bool> {
93        self.get(key).and_then(Value::as_bool)
94    }
95
96    /// Extracts a `f64` value from the params map.
97    ///
98    /// Returns `None` if the key is missing or the value cannot be converted to `f64`.
99    #[must_use]
100    pub fn get_f64(&self, key: &str) -> Option<f64> {
101        self.get(key).and_then(Value::as_f64)
102    }
103
104    #[cfg(feature = "python")]
105    /// Converts `Params` to a Python dict.
106    ///
107    /// # Errors
108    ///
109    /// Returns an error if conversion of any value fails.
110    pub fn to_pydict(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::Py<pyo3::types::PyDict>> {
111        crate::python::params::params_to_pydict(py, self)
112    }
113}
114
115impl Deref for Params {
116    type Target = IndexMap<String, Value>;
117
118    fn deref(&self) -> &Self::Target {
119        &self.0
120    }
121}
122
123impl DerefMut for Params {
124    fn deref_mut(&mut self) -> &mut Self::Target {
125        &mut self.0
126    }
127}
128
129impl<'a> IntoIterator for &'a Params {
130    type Item = (&'a String, &'a Value);
131    type IntoIter = indexmap::map::Iter<'a, String, Value>;
132
133    fn into_iter(self) -> Self::IntoIter {
134        self.0.iter()
135    }
136}
137
138#[cfg(feature = "python")]
139/// Converts a Python dict to `Params`.
140///
141/// This is a convenience function that wraps `pydict_to_params`.
142///
143/// # Errors
144///
145/// Returns an error if:
146/// - The dict cannot be serialized to JSON.
147/// - The JSON is not a valid object.
148/// - An integer value is outside the signed or unsigned 64-bit range.
149pub fn from_pydict(
150    py: pyo3::Python<'_>,
151    dict: &pyo3::Py<pyo3::types::PyDict>,
152) -> pyo3::PyResult<Option<Params>> {
153    crate::python::params::pydict_to_params(py, dict)
154}
155
156#[cfg(test)]
157mod tests {
158    use rstest::*;
159    use serde_json::json;
160
161    use super::Params;
162
163    fn create_test_params() -> Params {
164        let mut params = Params::new();
165        params.insert("u64_val".to_string(), json!(42u64));
166        params.insert("i64_val".to_string(), json!(-100i64));
167        params.insert("usize_val".to_string(), json!(5u64));
168        params.insert("str_val".to_string(), json!("hello"));
169        params.insert("bool_val".to_string(), json!(true));
170        params.insert("f64_val".to_string(), json!(2.5));
171        params
172    }
173
174    #[rstest]
175    fn test_params_getters() {
176        let params = create_test_params();
177
178        assert_eq!(params.get_u64("u64_val"), Some(42));
179        assert_eq!(params.get_i64("i64_val"), Some(-100));
180        assert_eq!(params.get_usize("usize_val"), Some(5));
181        assert_eq!(params.get_str("str_val"), Some("hello"));
182        assert_eq!(params.get_bool("bool_val"), Some(true));
183        assert_eq!(params.get_f64("f64_val"), Some(2.5));
184        assert_eq!(params.get_u64("missing"), None);
185        assert_eq!(params.get_i64("missing"), None);
186        assert_eq!(params.get_usize("missing"), None);
187        assert_eq!(params.get_str("missing"), None);
188        assert_eq!(params.get_bool("missing"), None);
189        assert_eq!(params.get_f64("missing"), None);
190        assert_eq!(params.get_u64("str_val"), None);
191        assert_eq!(params.get_str("u64_val"), None);
192    }
193
194    #[rstest]
195    fn test_params_ref_get_usize_respects_target_width() {
196        let mut params = Params::new();
197        params.insert("u32_max".to_string(), json!(u32::MAX));
198        params.insert("u32_overflow".to_string(), json!(4_294_967_296_u64));
199
200        assert_eq!(params.get_usize("u32_max"), Some(4_294_967_295_usize));
201        #[cfg(target_pointer_width = "32")]
202        assert_eq!(params.get_usize("u32_overflow"), None);
203        #[cfg(target_pointer_width = "64")]
204        assert_eq!(params.get_usize("u32_overflow"), Some(4_294_967_296));
205    }
206}