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 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 extractors.insert(type_name, extractor);
96 Ok(())
97 }
98
99 /// Extracts a `Py<PyAny>` factory to a boxed `DataClientFactory` trait object.
100 ///
101 /// # Errors
102 ///
103 /// Returns an error if no extractor is registered for the factory type or extraction fails.
104 ///
105 /// # Panics
106 ///
107 /// Panics if the internal mutex is poisoned.
108 pub fn extract_factory(
109 &self,
110 py: Python<'_>,
111 factory: Py<PyAny>,
112 ) -> PyResult<Box<dyn DataClientFactory>> {
113 // Get the factory name to find the appropriate extractor
114 let factory_name = factory
115 .getattr(py, "name")?
116 .call0(py)?
117 .extract::<String>(py)?;
118
119 let extractors = self.factory_extractors.lock().expect(MUTEX_POISONED);
120 if let Some(extractor) = extractors.get(&factory_name) {
121 extractor(py, factory)
122 } else {
123 Err(PyErr::new::<pyo3::exceptions::PyNotImplementedError, _>(
124 format!("No factory extractor registered for '{factory_name}'"),
125 ))
126 }
127 }
128
129 /// Extracts a `Py<PyAny>` config to a boxed `ClientConfig` trait object.
130 ///
131 /// # Errors
132 ///
133 /// Returns an error if no extractor is registered for the config type or extraction fails.
134 ///
135 /// # Panics
136 ///
137 /// Panics if the internal mutex is poisoned.
138 pub fn extract_config(
139 &self,
140 py: Python<'_>,
141 config: Py<PyAny>,
142 ) -> PyResult<Box<dyn ClientConfig>> {
143 // Get the config class name to find the appropriate extractor
144 let config_type_name = config
145 .getattr(py, "__class__")?
146 .getattr(py, "__name__")?
147 .extract::<String>(py)?;
148
149 let extractors = self.config_extractors.lock().expect(MUTEX_POISONED);
150 if let Some(extractor) = extractors.get(&config_type_name) {
151 extractor(py, config)
152 } else {
153 Err(PyErr::new::<pyo3::exceptions::PyNotImplementedError, _>(
154 format!("No config extractor registered for '{config_type_name}'"),
155 ))
156 }
157 }
158}
159
160impl Default for FactoryRegistry {
161 fn default() -> Self {
162 Self::new()
163 }
164}
165
166/// Global PyO3 registry instance.
167static GLOBAL_PYO3_REGISTRY: std::sync::LazyLock<FactoryRegistry> =
168 std::sync::LazyLock::new(FactoryRegistry::new);
169
170/// Gets a reference to the global PyO3 registry.
171#[must_use]
172pub fn get_global_pyo3_registry() -> &'static FactoryRegistry {
173 &GLOBAL_PYO3_REGISTRY
174}