nautilus_system/python/
registry.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//! PyO3 registry system for generic trait object extraction.
17
18use std::{collections::HashMap, sync::Mutex};
19
20use nautilus_core::MUTEX_POISONED;
21use pyo3::prelude::*;
22
23use crate::factories::{ClientConfig, DataClientFactory};
24
25/// Function type for extracting a `Py<PyAny>` factory to a boxed `DataClientFactory` trait object.
26pub type FactoryExtractor =
27    fn(py: Python<'_>, factory: Py<PyAny>) -> PyResult<Box<dyn DataClientFactory>>;
28
29/// Function type for extracting a `Py<PyAny>` config to a boxed `ClientConfig` trait object.
30pub type ConfigExtractor = fn(py: Python<'_>, config: Py<PyAny>) -> PyResult<Box<dyn ClientConfig>>;
31
32/// Registry for PyO3 factory and config extractors.
33///
34/// This allows each adapter to register its own extraction logic for converting
35/// `Py<PyAny>s` to boxed trait objects without requiring the live crate to know
36/// about specific implementations.
37#[derive(Debug)]
38pub struct FactoryRegistry {
39    factory_extractors: Mutex<HashMap<String, FactoryExtractor>>,
40    config_extractors: Mutex<HashMap<String, ConfigExtractor>>,
41}
42
43impl FactoryRegistry {
44    /// Creates a new empty registry.
45    #[must_use]
46    pub fn new() -> Self {
47        Self {
48            factory_extractors: Mutex::new(HashMap::new()),
49            config_extractors: Mutex::new(HashMap::new()),
50        }
51    }
52
53    /// Registers a factory extractor for a specific factory name.
54    ///
55    /// # Errors
56    ///
57    /// Returns an error if a factory with the same name is already registered.
58    ///
59    /// # Panics
60    ///
61    /// Panics if the internal mutex is poisoned.
62    pub fn register_factory_extractor(
63        &self,
64        name: String,
65        extractor: FactoryExtractor,
66    ) -> anyhow::Result<()> {
67        let mut extractors = self.factory_extractors.lock().expect(MUTEX_POISONED);
68
69        if extractors.contains_key(&name) {
70            anyhow::bail!("Factory extractor '{name}' is already registered");
71        }
72        extractors.insert(name, extractor);
73        Ok(())
74    }
75
76    /// Registers a config extractor for a specific config type name.
77    ///
78    /// # Errors
79    ///
80    /// Returns an error if a config with the same type name is already registered.
81    ///
82    /// # Panics
83    ///
84    /// Panics if the internal mutex is poisoned.
85    pub fn register_config_extractor(
86        &self,
87        type_name: String,
88        extractor: ConfigExtractor,
89    ) -> anyhow::Result<()> {
90        let mut extractors = self.config_extractors.lock().expect(MUTEX_POISONED);
91
92        if extractors.contains_key(&type_name) {
93            anyhow::bail!("Config extractor '{type_name}' is already registered");
94        }
95
96        extractors.insert(type_name, extractor);
97        Ok(())
98    }
99
100    /// Extracts a `Py<PyAny>` factory to a boxed `DataClientFactory` trait object.
101    ///
102    /// # Errors
103    ///
104    /// Returns an error if no extractor is registered for the factory type or extraction fails.
105    ///
106    /// # Panics
107    ///
108    /// Panics if the internal mutex is poisoned.
109    pub fn extract_factory(
110        &self,
111        py: Python<'_>,
112        factory: Py<PyAny>,
113    ) -> PyResult<Box<dyn DataClientFactory>> {
114        // Get the factory name to find the appropriate extractor
115        let factory_name = factory
116            .getattr(py, "name")?
117            .call0(py)?
118            .extract::<String>(py)?;
119
120        let extractors = self.factory_extractors.lock().expect(MUTEX_POISONED);
121        if let Some(extractor) = extractors.get(&factory_name) {
122            extractor(py, factory)
123        } else {
124            Err(PyErr::new::<pyo3::exceptions::PyNotImplementedError, _>(
125                format!("No factory extractor registered for '{factory_name}'"),
126            ))
127        }
128    }
129
130    /// Extracts a `Py<PyAny>` config to a boxed `ClientConfig` trait object.
131    ///
132    /// # Errors
133    ///
134    /// Returns an error if no extractor is registered for the config type or extraction fails.
135    ///
136    /// # Panics
137    ///
138    /// Panics if the internal mutex is poisoned.
139    pub fn extract_config(
140        &self,
141        py: Python<'_>,
142        config: Py<PyAny>,
143    ) -> PyResult<Box<dyn ClientConfig>> {
144        // Get the config class name to find the appropriate extractor
145        let config_type_name = config
146            .getattr(py, "__class__")?
147            .getattr(py, "__name__")?
148            .extract::<String>(py)?;
149
150        let extractors = self.config_extractors.lock().expect(MUTEX_POISONED);
151        if let Some(extractor) = extractors.get(&config_type_name) {
152            extractor(py, config)
153        } else {
154            Err(PyErr::new::<pyo3::exceptions::PyNotImplementedError, _>(
155                format!("No config extractor registered for '{config_type_name}'"),
156            ))
157        }
158    }
159}
160
161impl Default for FactoryRegistry {
162    fn default() -> Self {
163        Self::new()
164    }
165}
166
167/// Global PyO3 registry instance.
168static GLOBAL_PYO3_REGISTRY: std::sync::LazyLock<FactoryRegistry> =
169    std::sync::LazyLock::new(FactoryRegistry::new);
170
171/// Gets a reference to the global PyO3 registry.
172#[must_use]
173pub fn get_global_pyo3_registry() -> &'static FactoryRegistry {
174    &GLOBAL_PYO3_REGISTRY
175}