nautilus_coinapi/
factories.rs1use 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#[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 #[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}