nautilus_system/python/
registry.rs

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