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_getters() {
172        let params = create_test_params();
173
174        assert_eq!(params.get_u64("u64_val"), Some(42));
175        assert_eq!(params.get_i64("i64_val"), Some(-100));
176        assert_eq!(params.get_usize("usize_val"), Some(5));
177        assert_eq!(params.get_str("str_val"), Some("hello"));
178        assert_eq!(params.get_bool("bool_val"), Some(true));
179        assert_eq!(params.get_f64("f64_val"), Some(2.5));
180        assert_eq!(params.get_u64("missing"), None);
181        assert_eq!(params.get_i64("missing"), None);
182        assert_eq!(params.get_usize("missing"), None);
183        assert_eq!(params.get_str("missing"), None);
184        assert_eq!(params.get_bool("missing"), None);
185        assert_eq!(params.get_f64("missing"), None);
186        assert_eq!(params.get_u64("str_val"), None);
187        assert_eq!(params.get_str("u64_val"), None);
188    }
189
190    #[rstest]
191    fn test_params_ref_get_usize_respects_target_width() {
192        let mut params = Params::new();
193        params.insert("u32_max".to_string(), json!(u32::MAX));
194        params.insert("u32_overflow".to_string(), json!(4_294_967_296_u64));
195
196        assert_eq!(params.get_usize("u32_max"), Some(4_294_967_295_usize));
197        #[cfg(target_pointer_width = "32")]
198        assert_eq!(params.get_usize("u32_overflow"), None);
199        #[cfg(target_pointer_width = "64")]
200        assert_eq!(params.get_usize("u32_overflow"), Some(4_294_967_296));
201    }
202}