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//! Immutable JSON parameter storage with optional shared values.
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::{
22    fmt::Debug,
23    hash::Hash,
24    ops::Deref,
25    sync::{Arc, OnceLock},
26};
27
28use indexmap::{Equivalent, IndexMap};
29use serde::{Deserialize, Deserializer, Serialize, Serializer, ser::SerializeMap};
30use serde_json::Value;
31
32/// Immutable generic parameter storage.
33///
34/// This represents a map of string keys to JSON values, used for passing
35/// adapter-specific configuration, metadata, and any generic key-value data.
36///
37/// `from_pydict` rejects integer values outside the signed or unsigned 64-bit range with
38/// `ValueError`, including values in nested containers.
39///
40/// `Params` uses `IndexMap` to preserve insertion order, which is important for
41/// consistent serialization and debugging.
42///
43/// Maps built with [`Self::from_shared`] share storage across clones and can share values
44/// across different maps. [`Self::get`], [`Self::borrowed_entries`], and the typed getters borrow
45/// these values directly. `len`, `is_empty`, `capacity`, and `contains_key` also avoid copying.
46/// Indexing, `&Params` iteration, and access through the read-only `IndexMap` interface
47/// materialize and cache owned values. Use `borrowed_entries` to iterate without copying.
48/// Serialization preserves contents and insertion order, but not allocation sharing.
49///
50/// Build an owned map before calling [`Self::new`]. To derive different
51/// parameters, use `IndexMap::clone(&params)`, edit the map, and construct a new `Params`.
52/// To retain shared values, edit the map from [`Self::to_shared_map`] and call
53/// [`Self::from_shared`].
54///
55/// The compiler rejects mutation, including nested JSON values:
56///
57/// ```compile_fail
58/// use nautilus_core::Params;
59///
60/// let mut params = Params::empty();
61/// params.insert("key".to_string(), serde_json::json!(1));
62/// ```
63///
64/// ```compile_fail
65/// use nautilus_core::Params;
66///
67/// let mut params = Params::new(
68///     [("nested".to_string(), serde_json::json!({"value": 1}))].into(),
69/// );
70/// params["nested"]["value"] = serde_json::json!(2);
71/// ```
72///
73/// ```compile_fail
74/// use nautilus_core::Params;
75/// use std::sync::Arc;
76///
77/// let mut params = Params::from_shared(
78///     [("nested".to_string(), Arc::new(serde_json::json!({"value": 1})))].into(),
79/// );
80/// params["nested"]["value"] = serde_json::json!(2);
81/// ```
82#[derive(Clone)]
83pub struct Params(Storage);
84
85impl Params {
86    /// Creates an empty `Params` map.
87    #[must_use]
88    pub fn empty() -> Self {
89        Self::new(IndexMap::new())
90    }
91
92    /// Creates `Params` from an `IndexMap`.
93    #[must_use]
94    pub fn new(map: IndexMap<String, Value>) -> Self {
95        Self(Storage::Owned(map))
96    }
97
98    /// Creates immutable parameters from shared JSON values without copying their contents.
99    #[must_use]
100    pub fn from_shared(map: IndexMap<String, Arc<Value>>) -> Self {
101        Self(Storage::Shared(Arc::new(SharedStorage {
102            values: map,
103            owned: OnceLock::new(),
104        })))
105    }
106
107    /// Copies the map for deriving new parameters, retaining existing shared value allocations.
108    ///
109    /// Owned values are cloned.
110    #[must_use]
111    pub fn to_shared_map(&self) -> IndexMap<String, Arc<Value>> {
112        match &self.0 {
113            Storage::Owned(values) => values
114                .iter()
115                .map(|(key, value)| (key.clone(), Arc::new(value.clone())))
116                .collect(),
117            Storage::Shared(shared) => shared.values.clone(),
118        }
119    }
120
121    /// Borrows entries in insertion order without materializing shared values.
122    pub fn borrowed_entries(&self) -> impl DoubleEndedIterator<Item = (&String, &Value)> {
123        let (owned, shared) = match &self.0 {
124            Storage::Owned(values) => (Some(values), None),
125            Storage::Shared(shared) => (None, Some(&shared.values)),
126        };
127
128        owned.into_iter().flat_map(IndexMap::iter).chain(
129            shared
130                .into_iter()
131                .flat_map(IndexMap::iter)
132                .map(|(key, value)| (key, value.as_ref())),
133        )
134    }
135
136    /// Returns the number of parameters.
137    #[must_use]
138    pub fn len(&self) -> usize {
139        match &self.0 {
140            Storage::Owned(values) => values.len(),
141            Storage::Shared(shared) => shared.values.len(),
142        }
143    }
144
145    /// Returns whether the map is empty.
146    #[must_use]
147    pub fn is_empty(&self) -> bool {
148        self.len() == 0
149    }
150
151    /// Returns the map capacity without materializing shared values.
152    #[must_use]
153    pub fn capacity(&self) -> usize {
154        match &self.0 {
155            Storage::Owned(values) => values.capacity(),
156            Storage::Shared(shared) => shared.values.capacity(),
157        }
158    }
159
160    /// Returns a value by key without materializing shared values.
161    #[must_use]
162    pub fn get<Q>(&self, key: &Q) -> Option<&Value>
163    where
164        Q: ?Sized + Hash + Equivalent<String>,
165    {
166        match &self.0 {
167            Storage::Owned(values) => values.get(key),
168            Storage::Shared(shared) => shared.values.get(key).map(AsRef::as_ref),
169        }
170    }
171
172    /// Returns whether the map contains a key.
173    #[must_use]
174    pub fn contains_key<Q>(&self, key: &Q) -> bool
175    where
176        Q: ?Sized + Hash + Equivalent<String>,
177    {
178        self.get(key).is_some()
179    }
180
181    /// Estimates retained key, value, and element storage without materializing shared values.
182    ///
183    /// Charges shared values in full for each entry and includes any cached owned map.
184    /// Excludes the outer storage allocation, allocator overhead, and hash-table overhead.
185    #[must_use]
186    pub fn estimated_heap_size(&self) -> usize {
187        match &self.0 {
188            Storage::Owned(values) => map_heap_size(values),
189            Storage::Shared(shared) => shared.values.iter().fold(
190                shared.owned.get().map_or(0, map_heap_size).saturating_add(
191                    shared
192                        .values
193                        .capacity()
194                        .saturating_mul(size_of::<(usize, String, Arc<Value>)>()),
195                ),
196                |bytes, (key, value)| {
197                    bytes
198                        .saturating_add(key.capacity())
199                        .saturating_add(size_of::<(usize, usize, Value)>())
200                        .saturating_add(json_heap_size(value))
201                },
202            ),
203        }
204    }
205
206    /// Extracts a `u64` value from the params map.
207    ///
208    /// Returns `None` if the key is missing or the value cannot be converted to `u64`.
209    #[must_use]
210    pub fn get_u64(&self, key: &str) -> Option<u64> {
211        self.get(key).and_then(Value::as_u64)
212    }
213
214    /// Extracts an `i64` value from the params map.
215    ///
216    /// Returns `None` if the key is missing or the value cannot be converted to `i64`.
217    #[must_use]
218    pub fn get_i64(&self, key: &str) -> Option<i64> {
219        self.get(key).and_then(Value::as_i64)
220    }
221
222    /// Extracts a `usize` value from the params map.
223    ///
224    /// Returns `None` if the key is missing or the value cannot be converted to `usize`.
225    #[must_use]
226    pub fn get_usize(&self, key: &str) -> Option<usize> {
227        self.get(key)
228            .and_then(Value::as_u64)
229            .and_then(|n| usize::try_from(n).ok())
230    }
231
232    /// Extracts a string value from the params map.
233    ///
234    /// Returns `None` if the key is missing or the value is not a string.
235    #[must_use]
236    pub fn get_str(&self, key: &str) -> Option<&str> {
237        self.get(key).and_then(|v| v.as_str())
238    }
239
240    /// Extracts a boolean value from the params map.
241    ///
242    /// Returns `None` if the key is missing or the value is not a boolean.
243    #[must_use]
244    pub fn get_bool(&self, key: &str) -> Option<bool> {
245        self.get(key).and_then(Value::as_bool)
246    }
247
248    /// Extracts a `f64` value from the params map.
249    ///
250    /// Returns `None` if the key is missing or the value cannot be converted to `f64`.
251    #[must_use]
252    pub fn get_f64(&self, key: &str) -> Option<f64> {
253        self.get(key).and_then(Value::as_f64)
254    }
255
256    #[cfg(feature = "python")]
257    /// Converts `Params` to a Python dict.
258    ///
259    /// # Errors
260    ///
261    /// Returns an error if conversion of any value fails.
262    pub fn to_pydict(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::Py<pyo3::types::PyDict>> {
263        crate::python::params::params_to_pydict(py, self)
264    }
265}
266
267impl Deref for Params {
268    type Target = IndexMap<String, Value>;
269
270    fn deref(&self) -> &Self::Target {
271        match &self.0 {
272            Storage::Owned(values) => values,
273            Storage::Shared(shared) => shared.owned.get_or_init(|| {
274                shared
275                    .values
276                    .iter()
277                    .map(|(key, value)| (key.clone(), value.as_ref().clone()))
278                    .collect()
279            }),
280        }
281    }
282}
283
284impl<'a> IntoIterator for &'a Params {
285    type Item = (&'a String, &'a Value);
286    type IntoIter = indexmap::map::Iter<'a, String, Value>;
287
288    fn into_iter(self) -> Self::IntoIter {
289        self.deref().iter()
290    }
291}
292
293impl Default for Params {
294    fn default() -> Self {
295        Self::empty()
296    }
297}
298
299impl Debug for Params {
300    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301        match &self.0 {
302            Storage::Owned(values) => f.debug_tuple("Params").field(values).finish(),
303            Storage::Shared(shared) => f.debug_tuple("Params").field(&shared.values).finish(),
304        }
305    }
306}
307
308impl PartialEq for Params {
309    fn eq(&self, other: &Self) -> bool {
310        self.len() == other.len()
311            && self
312                .borrowed_entries()
313                .all(|(key, value)| other.get(key) == Some(value))
314    }
315}
316
317impl Eq for Params {}
318
319impl Serialize for Params {
320    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
321        let mut map = serializer.serialize_map(Some(self.len()))?;
322        for (key, value) in self.borrowed_entries() {
323            map.serialize_entry(key, value)?;
324        }
325
326        map.end()
327    }
328}
329
330impl<'de> Deserialize<'de> for Params {
331    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
332        IndexMap::deserialize(deserializer).map(Self::new)
333    }
334}
335
336#[derive(Clone)]
337enum Storage {
338    Owned(IndexMap<String, Value>),
339    Shared(Arc<SharedStorage>),
340}
341
342struct SharedStorage {
343    values: IndexMap<String, Arc<Value>>,
344    owned: OnceLock<IndexMap<String, Value>>,
345}
346
347fn map_heap_size(values: &IndexMap<String, Value>) -> usize {
348    values.iter().fold(
349        values
350            .capacity()
351            .saturating_mul(size_of::<(usize, String, Value)>()),
352        |bytes, (key, value)| {
353            bytes
354                .saturating_add(key.capacity())
355                .saturating_add(json_heap_size(value))
356        },
357    )
358}
359
360fn json_heap_size(value: &Value) -> usize {
361    match value {
362        Value::String(value) => value.capacity(),
363        Value::Array(values) => values.iter().fold(
364            values.capacity().saturating_mul(size_of::<Value>()),
365            |bytes, value| bytes.saturating_add(json_heap_size(value)),
366        ),
367        Value::Object(values) => values.iter().fold(0usize, |bytes, (key, value)| {
368            bytes
369                .saturating_add(size_of::<(String, Value)>())
370                .saturating_add(key.capacity())
371                .saturating_add(json_heap_size(value))
372        }),
373        _ => 0,
374    }
375}
376
377#[cfg(feature = "python")]
378/// Converts a Python dict to `Params`.
379///
380/// This is a convenience function that wraps `pydict_to_params`.
381///
382/// # Errors
383///
384/// Returns an error if:
385/// - The dict cannot be serialized to JSON.
386/// - The JSON is not a valid object.
387/// - An integer value is outside the signed or unsigned 64-bit range.
388pub fn from_pydict(
389    py: pyo3::Python<'_>,
390    dict: &pyo3::Py<pyo3::types::PyDict>,
391) -> pyo3::PyResult<Option<Params>> {
392    crate::python::params::pydict_to_params(py, dict)
393}
394
395#[cfg(test)]
396mod tests {
397    use rstest::*;
398    use serde_json::json;
399
400    use super::*;
401
402    fn create_test_params() -> Params {
403        Params::new(
404            [
405                ("u64_val".to_string(), json!(42u64)),
406                ("i64_val".to_string(), json!(-100i64)),
407                ("usize_val".to_string(), json!(5u64)),
408                ("str_val".to_string(), json!("hello")),
409                ("bool_val".to_string(), json!(true)),
410                ("f64_val".to_string(), json!(2.5)),
411            ]
412            .into(),
413        )
414    }
415
416    #[rstest]
417    fn test_params_getters() {
418        let params = create_test_params();
419
420        assert_eq!(params.get_u64("u64_val"), Some(42));
421        assert_eq!(params.get_i64("i64_val"), Some(-100));
422        assert_eq!(params.get_usize("usize_val"), Some(5));
423        assert_eq!(params.get_str("str_val"), Some("hello"));
424        assert_eq!(params.get_bool("bool_val"), Some(true));
425        assert_eq!(params.get_f64("f64_val"), Some(2.5));
426        assert_eq!(params.get_u64("missing"), None);
427        assert_eq!(params.get_i64("missing"), None);
428        assert_eq!(params.get_usize("missing"), None);
429        assert_eq!(params.get_str("missing"), None);
430        assert_eq!(params.get_bool("missing"), None);
431        assert_eq!(params.get_f64("missing"), None);
432        assert_eq!(params.get_u64("str_val"), None);
433        assert_eq!(params.get_str("u64_val"), None);
434    }
435
436    #[rstest]
437    #[case(false)]
438    #[case(true)]
439    fn test_params_shared_reads_and_serialization(#[case] materialize: bool) {
440        let original = create_test_params();
441        let shared = Params::from_shared(original.to_shared_map());
442        let cloned = shared.clone();
443        let retained = shared.estimated_heap_size();
444        let json = serde_json::to_string(&original).unwrap();
445        let msgpack = rmp_serde::to_vec_named(&original).unwrap();
446
447        assert!(std::ptr::eq(
448            shared.get("str_val").unwrap(),
449            cloned.get("str_val").unwrap()
450        ));
451        assert_eq!(shared, original);
452        assert_eq!(format!("{shared:?}"), format!("{original:?}"));
453        assert_eq!(shared.len(), 6);
454        assert!(!shared.is_empty());
455        assert!(shared.contains_key("str_val"));
456        assert!(!shared.contains_key("missing"));
457        assert_eq!(shared.get_str("str_val"), Some("hello"));
458        assert_eq!(shared.get_u64("u64_val"), Some(42));
459        assert_eq!(shared.get_i64("i64_val"), Some(-100));
460        assert_eq!(shared.get_usize("usize_val"), Some(5));
461        assert_eq!(shared.get_bool("bool_val"), Some(true));
462        assert_eq!(shared.get_f64("f64_val"), Some(2.5));
463        assert_eq!(serde_json::to_string(&shared).unwrap(), json);
464        assert_eq!(rmp_serde::to_vec_named(&shared).unwrap(), msgpack);
465        assert_eq!(shared.estimated_heap_size(), retained);
466
467        if materialize {
468            let map: &IndexMap<String, Value> = &shared;
469            let cloned_map: &IndexMap<String, Value> = &cloned;
470            assert!(std::ptr::eq(map, cloned_map));
471            assert_eq!(map, &*original);
472            assert_eq!(shared["str_val"], json!("hello"));
473            assert_eq!(
474                (&shared).into_iter().collect::<Vec<_>>(),
475                original.iter().collect::<Vec<_>>()
476            );
477        }
478
479        assert_eq!(
480            shared.borrowed_entries().collect::<Vec<_>>(),
481            original.iter().collect::<Vec<_>>()
482        );
483        assert_eq!(
484            shared.borrowed_entries().rev().collect::<Vec<_>>(),
485            original.iter().rev().collect::<Vec<_>>()
486        );
487        assert_eq!(serde_json::from_str::<Params>(&json).unwrap(), shared);
488        assert_eq!(rmp_serde::from_slice::<Params>(&msgpack).unwrap(), shared);
489    }
490
491    #[rstest]
492    fn test_params_derivation_preserves_original_and_shared_values() {
493        let event = Arc::new(json!({"markets": ["first", "second"], "unknown": {"count": 7}}));
494        let original = Params::from_shared(
495            [
496                ("event".to_string(), Arc::clone(&event)),
497                ("closed".to_string(), Arc::new(json!(false))),
498            ]
499            .into(),
500        );
501        let mut map = original.to_shared_map();
502        map.insert("closed".to_string(), Arc::new(json!(true)));
503        let derived = Params::from_shared(map);
504        let mut owned = IndexMap::clone(&original);
505        owned["event"]["markets"][0] = json!("changed");
506        let modified = Params::new(owned);
507
508        assert_eq!(original.get_bool("closed"), Some(false));
509        assert_eq!(derived.get_bool("closed"), Some(true));
510        assert_eq!(original.get("event"), Some(event.as_ref()));
511        assert_eq!(derived.get("event"), Some(event.as_ref()));
512        assert!(std::ptr::eq(derived.get("event").unwrap(), event.as_ref()));
513        assert_eq!(modified["event"]["markets"][0], json!("changed"));
514        assert_eq!(original["event"]["markets"][0], json!("first"));
515        assert_eq!(derived["event"]["markets"][0], json!("first"));
516    }
517
518    #[rstest]
519    fn test_params_shared_storage_accounts_for_materialization() {
520        let mut key = String::with_capacity(31);
521        key.push_str("event");
522        let mut text = String::with_capacity(137);
523        text.push_str("snapshot");
524        let contents = key.capacity() + text.capacity();
525        let params = Params::from_shared([(key, Arc::new(Value::String(text)))].into());
526        let shared_bytes = params.capacity() * size_of::<(usize, String, Arc<Value>)>()
527            + size_of::<(usize, usize, Value)>()
528            + contents;
529        assert_eq!(params.estimated_heap_size(), shared_bytes);
530
531        let map: &IndexMap<String, Value> = &params;
532
533        let (key, Value::String(value)) = map.get_index(0).unwrap() else {
534            unreachable!()
535        };
536
537        let owned_bytes = map.capacity() * size_of::<(usize, String, Value)>()
538            + key.capacity()
539            + value.capacity();
540        assert_eq!(params.estimated_heap_size(), shared_bytes + owned_bytes);
541    }
542
543    #[rstest]
544    fn test_params_empty_and_order_independent_equality() {
545        let first = Params::from_shared(
546            [
547                ("z".to_string(), Arc::new(json!(null))),
548                ("a".to_string(), Arc::new(json!([1, {"nested": true}]))),
549            ]
550            .into(),
551        );
552
553        let reverse = Params::new(
554            first
555                .borrowed_entries()
556                .rev()
557                .map(|(key, value)| (key.clone(), value.clone()))
558                .collect(),
559        );
560        let empty = Params::from_shared(IndexMap::new());
561
562        assert_eq!(first, reverse);
563        assert_eq!(
564            serde_json::to_string(&first).unwrap(),
565            r#"{"z":null,"a":[1,{"nested":true}]}"#
566        );
567        assert_eq!(empty, Params::empty());
568        assert!(empty.is_empty());
569        assert_eq!(empty.len(), 0);
570        assert_eq!(empty.get("missing"), None);
571        assert_eq!(empty.borrowed_entries().count(), 0);
572        assert_eq!(serde_json::to_string(&empty).unwrap(), "{}");
573    }
574
575    #[rstest]
576    fn test_params_ref_get_usize_respects_target_width() {
577        let params = Params::new(
578            [
579                ("u32_max".to_string(), json!(u32::MAX)),
580                ("u32_overflow".to_string(), json!(4_294_967_296_u64)),
581            ]
582            .into(),
583        );
584
585        assert_eq!(params.get_usize("u32_max"), Some(4_294_967_295_usize));
586        #[cfg(target_pointer_width = "32")]
587        assert_eq!(params.get_usize("u32_overflow"), None);
588        #[cfg(target_pointer_width = "64")]
589        assert_eq!(params.get_usize("u32_overflow"), Some(4_294_967_296));
590    }
591}