borsa_mock/
lib.rs

1use async_trait::async_trait;
2use borsa_core::connector::{
3    AnalystPriceTargetProvider, BalanceSheetProvider, BorsaConnector, CalendarProvider,
4    CashflowProvider, EarningsProvider, EsgProvider, HistoryProvider, IncomeStatementProvider,
5    NewsProvider, OptionChainProvider, OptionsExpirationsProvider, ProfileProvider, QuoteProvider,
6    RecommendationsProvider, RecommendationsSummaryProvider, SearchProvider,
7    UpgradesDowngradesProvider,
8};
9use borsa_core::{
10    AssetKind, BalanceSheetRow, BorsaError, Calendar, CashflowRow, Earnings, EsgScores,
11    HistoryRequest, HistoryResponse, IncomeStatementRow, Instrument, Interval, NewsRequest,
12    OptionChain, Profile, Quote, RecommendationRow, RecommendationSummary, SearchRequest,
13    SearchResponse, UpgradeDowngradeRow, types,
14};
15
16mod fixtures;
17
18/// Mock connector for CI-safe examples. Provides deterministic data from static fixtures.
19pub struct MockConnector;
20
21impl Default for MockConnector {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27impl MockConnector {
28    #[must_use]
29    pub const fn new() -> Self {
30        Self
31    }
32
33    fn not_found(what: &str) -> BorsaError {
34        BorsaError::not_found(what.to_string())
35    }
36
37    async fn maybe_fail_or_timeout(
38        symbol: &str,
39        capability: &'static str,
40    ) -> Result<(), BorsaError> {
41        match symbol {
42            "FAIL" => Err(BorsaError::connector(
43                "borsa-mock",
44                format!("forced failure: {capability}"),
45            )),
46            "TIMEOUT" => {
47                // Simulate brief latency; orchestrator may time out depending on config
48                // Keep short to avoid slowing tests excessively
49                tokio::time::sleep(std::time::Duration::from_millis(200)).await;
50                Ok(())
51            }
52            _ => Ok(()),
53        }
54    }
55}
56
57#[async_trait]
58impl BorsaConnector for MockConnector {
59    fn name(&self) -> &'static str {
60        "borsa-mock"
61    }
62    fn vendor(&self) -> &'static str {
63        "Mock"
64    }
65
66    fn supports_kind(&self, _kind: AssetKind) -> bool {
67        true
68    }
69
70    fn as_quote_provider(&self) -> Option<&dyn QuoteProvider> {
71        Some(self as &dyn QuoteProvider)
72    }
73    fn as_history_provider(&self) -> Option<&dyn HistoryProvider> {
74        Some(self as &dyn HistoryProvider)
75    }
76    fn as_search_provider(&self) -> Option<&dyn SearchProvider> {
77        Some(self as &dyn SearchProvider)
78    }
79    fn as_profile_provider(&self) -> Option<&dyn ProfileProvider> {
80        Some(self as &dyn ProfileProvider)
81    }
82    fn as_earnings_provider(&self) -> Option<&dyn EarningsProvider> {
83        Some(self as &dyn EarningsProvider)
84    }
85    fn as_income_statement_provider(&self) -> Option<&dyn IncomeStatementProvider> {
86        Some(self as &dyn IncomeStatementProvider)
87    }
88    fn as_balance_sheet_provider(&self) -> Option<&dyn BalanceSheetProvider> {
89        Some(self as &dyn BalanceSheetProvider)
90    }
91    fn as_cashflow_provider(&self) -> Option<&dyn CashflowProvider> {
92        Some(self as &dyn CashflowProvider)
93    }
94    fn as_calendar_provider(&self) -> Option<&dyn CalendarProvider> {
95        Some(self as &dyn CalendarProvider)
96    }
97    fn as_options_expirations_provider(&self) -> Option<&dyn OptionsExpirationsProvider> {
98        Some(self as &dyn OptionsExpirationsProvider)
99    }
100    fn as_option_chain_provider(&self) -> Option<&dyn OptionChainProvider> {
101        Some(self as &dyn OptionChainProvider)
102    }
103    fn as_recommendations_provider(&self) -> Option<&dyn RecommendationsProvider> {
104        Some(self as &dyn RecommendationsProvider)
105    }
106    fn as_recommendations_summary_provider(&self) -> Option<&dyn RecommendationsSummaryProvider> {
107        Some(self as &dyn RecommendationsSummaryProvider)
108    }
109    fn as_upgrades_downgrades_provider(&self) -> Option<&dyn UpgradesDowngradesProvider> {
110        Some(self as &dyn UpgradesDowngradesProvider)
111    }
112    fn as_analyst_price_target_provider(&self) -> Option<&dyn AnalystPriceTargetProvider> {
113        Some(self as &dyn AnalystPriceTargetProvider)
114    }
115    fn as_esg_provider(&self) -> Option<&dyn EsgProvider> {
116        Some(self as &dyn EsgProvider)
117    }
118    fn as_news_provider(&self) -> Option<&dyn NewsProvider> {
119        Some(self as &dyn NewsProvider)
120    }
121    // Stream intentionally unsupported for examples
122}
123
124#[async_trait]
125impl QuoteProvider for MockConnector {
126    async fn quote(&self, instrument: &Instrument) -> Result<Quote, BorsaError> {
127        let s = instrument.symbol_str();
128        Self::maybe_fail_or_timeout(s, "quote").await?;
129        fixtures::quotes::by_symbol(s).ok_or_else(|| Self::not_found(&format!("quote for {s}")))
130    }
131}
132
133#[async_trait]
134impl HistoryProvider for MockConnector {
135    async fn history(
136        &self,
137        instrument: &Instrument,
138        _req: HistoryRequest,
139    ) -> Result<HistoryResponse, BorsaError> {
140        let s = instrument.symbol_str();
141        Self::maybe_fail_or_timeout(s, "history").await?;
142        fixtures::history::by_symbol(s).ok_or_else(|| Self::not_found(&format!("history for {s}")))
143    }
144
145    fn supported_history_intervals(&self, _kind: AssetKind) -> &'static [Interval] {
146        const ONLY_D1: &[Interval] = &[Interval::D1];
147        ONLY_D1
148    }
149}
150
151#[async_trait]
152impl SearchProvider for MockConnector {
153    async fn search(&self, req: SearchRequest) -> Result<SearchResponse, BorsaError> {
154        Ok(fixtures::search::search(&req))
155    }
156}
157
158#[async_trait]
159impl ProfileProvider for MockConnector {
160    async fn profile(&self, instrument: &Instrument) -> Result<Profile, BorsaError> {
161        let s = instrument.symbol_str();
162        Ok(fixtures::profile::by_symbol(s))
163    }
164}
165
166#[async_trait]
167impl EarningsProvider for MockConnector {
168    async fn earnings(&self, instrument: &Instrument) -> Result<Earnings, BorsaError> {
169        let s = instrument.symbol_str();
170        Ok(fixtures::fundamentals::earnings_by_symbol(s))
171    }
172}
173
174#[async_trait]
175impl IncomeStatementProvider for MockConnector {
176    async fn income_statement(
177        &self,
178        instrument: &Instrument,
179        _q: bool,
180    ) -> Result<Vec<IncomeStatementRow>, BorsaError> {
181        let s = instrument.symbol_str();
182        Ok(fixtures::fundamentals::income_stmt_by_symbol(s))
183    }
184}
185
186#[async_trait]
187impl BalanceSheetProvider for MockConnector {
188    async fn balance_sheet(
189        &self,
190        instrument: &Instrument,
191        _q: bool,
192    ) -> Result<Vec<BalanceSheetRow>, BorsaError> {
193        let s = instrument.symbol_str();
194        Ok(fixtures::fundamentals::balance_sheet_by_symbol(s))
195    }
196}
197
198#[async_trait]
199impl CashflowProvider for MockConnector {
200    async fn cashflow(
201        &self,
202        instrument: &Instrument,
203        _q: bool,
204    ) -> Result<Vec<CashflowRow>, BorsaError> {
205        let s = instrument.symbol_str();
206        Ok(fixtures::fundamentals::cashflow_by_symbol(s))
207    }
208}
209
210#[async_trait]
211impl CalendarProvider for MockConnector {
212    async fn calendar(&self, instrument: &Instrument) -> Result<Calendar, BorsaError> {
213        let s = instrument.symbol_str();
214        Ok(fixtures::calendar::by_symbol(s))
215    }
216}
217
218#[async_trait]
219impl OptionsExpirationsProvider for MockConnector {
220    async fn options_expirations(&self, instrument: &Instrument) -> Result<Vec<i64>, BorsaError> {
221        let s = instrument.symbol_str();
222        Ok(fixtures::options::expirations_by_symbol(s))
223    }
224}
225
226#[async_trait]
227impl OptionChainProvider for MockConnector {
228    async fn option_chain(
229        &self,
230        instrument: &Instrument,
231        date: Option<i64>,
232    ) -> Result<OptionChain, BorsaError> {
233        let s = instrument.symbol_str();
234        Ok(fixtures::options::chain_by_symbol_and_date(s, date))
235    }
236}
237
238#[async_trait]
239impl RecommendationsProvider for MockConnector {
240    async fn recommendations(
241        &self,
242        instrument: &Instrument,
243    ) -> Result<Vec<RecommendationRow>, BorsaError> {
244        let s = instrument.symbol_str();
245        Ok(fixtures::analysis::recommendations_by_symbol(s))
246    }
247}
248
249#[async_trait]
250impl RecommendationsSummaryProvider for MockConnector {
251    async fn recommendations_summary(
252        &self,
253        instrument: &Instrument,
254    ) -> Result<RecommendationSummary, BorsaError> {
255        let s = instrument.symbol_str();
256        Ok(fixtures::analysis::recommendations_summary_by_symbol(s))
257    }
258}
259
260#[async_trait]
261impl AnalystPriceTargetProvider for MockConnector {
262    async fn analyst_price_target(
263        &self,
264        instrument: &Instrument,
265    ) -> Result<borsa_core::PriceTarget, BorsaError> {
266        let _s = instrument.symbol_str();
267        Ok(fixtures::analysis::price_target_by_symbol(_s))
268    }
269}
270
271#[async_trait]
272impl UpgradesDowngradesProvider for MockConnector {
273    async fn upgrades_downgrades(
274        &self,
275        instrument: &Instrument,
276    ) -> Result<Vec<UpgradeDowngradeRow>, BorsaError> {
277        let s = instrument.symbol_str();
278        Ok(fixtures::analysis::upgrades_downgrades_by_symbol(s))
279    }
280}
281
282#[async_trait]
283impl EsgProvider for MockConnector {
284    async fn sustainability(&self, instrument: &Instrument) -> Result<EsgScores, BorsaError> {
285        let s = instrument.symbol_str();
286        Ok(fixtures::esg::by_symbol(s))
287    }
288}
289
290#[async_trait]
291impl NewsProvider for MockConnector {
292    async fn news(
293        &self,
294        instrument: &Instrument,
295        req: NewsRequest,
296    ) -> Result<Vec<types::NewsArticle>, BorsaError> {
297        let s = instrument.symbol_str();
298        Ok(fixtures::news::by_symbol(s, req))
299    }
300}