Skip to main content

nautilus_coinapi/
factories.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//! Factory for creating CoinAPI data clients.
17
18use std::{any::Any, cell::RefCell, rc::Rc};
19
20use nautilus_common::{
21    cache::CacheView,
22    clients::DataClient,
23    clock::Clock,
24    factories::{ClientConfig, DataClientFactory},
25};
26use nautilus_model::identifiers::ClientId;
27
28use crate::{common::consts::COINAPI, config::CoinApiDataClientConfig, data::CoinApiDataClient};
29
30impl ClientConfig for CoinApiDataClientConfig {
31    fn as_any(&self) -> &dyn Any {
32        self
33    }
34}
35
36/// Factory for creating CoinAPI data clients.
37#[derive(Debug, Clone)]
38#[cfg_attr(
39    feature = "python",
40    pyo3::pyclass(module = "nautilus_trader.adapters.coinapi", from_py_object)
41)]
42#[cfg_attr(
43    feature = "python",
44    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.coinapi")
45)]
46pub struct CoinApiDataClientFactory;
47
48impl CoinApiDataClientFactory {
49    /// Creates a new [`CoinApiDataClientFactory`] instance.
50    #[must_use]
51    pub const fn new() -> Self {
52        Self
53    }
54}
55
56impl Default for CoinApiDataClientFactory {
57    fn default() -> Self {
58        Self::new()
59    }
60}
61
62impl DataClientFactory for CoinApiDataClientFactory {
63    fn create(
64        &self,
65        name: &str,
66        config: &dyn ClientConfig,
67        _cache: CacheView,
68        _clock: Rc<RefCell<dyn Clock>>,
69    ) -> anyhow::Result<Box<dyn DataClient>> {
70        let coinapi_config = config
71            .as_any()
72            .downcast_ref::<CoinApiDataClientConfig>()
73            .ok_or_else(|| {
74                anyhow::anyhow!(
75                    "Invalid config type for CoinApiDataClientFactory. Expected CoinApiDataClientConfig, was {config:?}",
76                )
77            })?
78            .clone();
79
80        let client_id = ClientId::from(name);
81        let client = CoinApiDataClient::new(client_id, coinapi_config)?;
82        Ok(Box::new(client))
83    }
84
85    fn name(&self) -> &'static str {
86        COINAPI
87    }
88
89    fn config_type(&self) -> &'static str {
90        "CoinApiDataClientConfig"
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use std::{cell::RefCell, rc::Rc};
97
98    use nautilus_common::{
99        cache::Cache, clock::VirtualClock, live::runner::set_data_event_sender, messages::DataEvent,
100    };
101    use nautilus_core::string::secret::SecretString;
102    use rstest::rstest;
103
104    use super::*;
105
106    fn setup_test_env() {
107        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
108        set_data_event_sender(sender);
109    }
110
111    fn create(name: &str, config: &dyn ClientConfig) -> anyhow::Result<Box<dyn DataClient>> {
112        let cache = Rc::new(RefCell::new(Cache::default()));
113        let clock = Rc::new(RefCell::new(VirtualClock::new()));
114        CoinApiDataClientFactory::new().create(name, config, cache.into(), clock)
115    }
116
117    #[rstest]
118    fn factory_reports_name_and_config_type() {
119        let factory = CoinApiDataClientFactory::new();
120
121        assert_eq!(factory.name(), "COINAPI");
122        assert_eq!(factory.config_type(), "CoinApiDataClientConfig");
123    }
124
125    #[rstest]
126    fn config_downcasts_from_client_config() {
127        let config = CoinApiDataClientConfig::builder().credit_budget(7).build();
128        let boxed_config: Box<dyn ClientConfig> = Box::new(config);
129
130        let downcasted = boxed_config
131            .as_any()
132            .downcast_ref::<CoinApiDataClientConfig>()
133            .unwrap();
134
135        assert_eq!(downcasted.credit_budget, 7);
136    }
137
138    #[rstest]
139    fn factory_creates_client_with_name_as_client_id() {
140        setup_test_env();
141        let config = CoinApiDataClientConfig::builder()
142            .api_key(SecretString::from("test-key"))
143            .build();
144
145        let client = create("COINAPI-TEST", &config).unwrap();
146
147        assert_eq!(client.client_id(), ClientId::from("COINAPI-TEST"));
148        assert_eq!(client.venue(), None);
149    }
150
151    #[rstest]
152    fn factory_propagates_client_construction_error() {
153        setup_test_env();
154        let config = CoinApiDataClientConfig::builder()
155            .api_key(SecretString::from("has space"))
156            .build();
157
158        let err = create("COINAPI-TEST", &config).err().unwrap();
159
160        assert_eq!(
161            err.to_string(),
162            "CoinAPI API key must contain visible ASCII characters"
163        );
164    }
165
166    #[rstest]
167    fn factory_rejects_wrong_config_type() {
168        #[derive(Debug)]
169        struct WrongConfig;
170
171        impl ClientConfig for WrongConfig {
172            fn as_any(&self) -> &dyn Any {
173                self
174            }
175        }
176
177        let err = create("COINAPI-TEST", &WrongConfig).err().unwrap();
178
179        assert_eq!(
180            err.to_string(),
181            "Invalid config type for CoinApiDataClientFactory. Expected CoinApiDataClientConfig, was WrongConfig"
182        );
183    }
184}