Skip to main content

finance_query/providers/
config.rs

1use crate::adapters::yahoo::client::ClientConfig;
2use crate::error::Result;
3use crate::providers::{
4    Fetch, Provider, ProviderHealth, ProviderSet, RetryPolicy, Routes, build_providers,
5};
6use std::sync::Arc;
7use std::time::Duration;
8
9/// Central provider configuration shared across query handles.
10///
11/// Build once with [`Providers::builder`], then create lightweight
12/// [`Ticker`](crate::Ticker) handles that share the same underlying
13/// provider connections and authentication.
14///
15/// # Example
16///
17/// ```no_run
18/// use finance_query::{Providers, Provider, Fetch, Capability};
19///
20/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
21/// let providers = Providers::builder()
22///     .route(Capability::QUOTE, [Provider::Yahoo])
23///     .fetch(Fetch::Sequential)
24///     .build().await?;
25///
26/// // All Ticker handles share the same Arc<ProviderSet>
27/// let aapl = providers.ticker("AAPL").build().await?;
28/// let nvda = providers.ticker("NVDA").logo().build().await?;
29/// # Ok(())
30/// # }
31/// ```
32pub struct Providers {
33    pub(crate) set: Arc<ProviderSet>,
34    lang: String,
35}
36
37impl std::fmt::Debug for Providers {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        f.debug_struct("Providers")
40            .field("set", &self.set)
41            .field("lang", &self.lang)
42            .finish()
43    }
44}
45
46impl Providers {
47    /// Create a builder for configuring providers.
48    pub fn builder() -> ProvidersBuilder {
49        ProvidersBuilder::default()
50    }
51
52    /// Wrap a [`ProviderSet`] assembled by hand.
53    ///
54    /// The lower-level counterpart to [`builder`](Self::builder), for a caller
55    /// that has already built its own adapters and route table. Nothing is
56    /// initialised: [`ProviderAdapter::initialize`](crate::ProviderAdapter::initialize)
57    /// is the builder's job, so a hand-built set must be ready to use.
58    pub fn from_set(set: Arc<ProviderSet>) -> Self {
59        Self::from_set_with_lang(set, ClientConfig::default().lang)
60    }
61
62    /// [`from_set`](Self::from_set) with an explicit language for the handles
63    /// this set creates.
64    pub fn from_set_with_lang(set: Arc<ProviderSet>, lang: impl Into<String>) -> Self {
65        Self {
66            set,
67            lang: lang.into(),
68        }
69    }
70
71    /// The underlying set, for `TickerBuilder::with_provider_set` and friends.
72    pub fn provider_set(&self) -> &Arc<ProviderSet> {
73        &self.set
74    }
75
76    /// Create a [`TickerBuilder`](crate::TickerBuilder) pre-wired to this provider set.
77    ///
78    /// The returned builder accepts the same optional configuration as
79    /// [`Ticker::builder`](crate::Ticker::builder) (`.cache()`, `.logo()`,
80    /// `.format()`) before calling `.build()`.
81    ///
82    /// The language configured via [`ProvidersBuilder::lang`] or
83    /// [`ProvidersBuilder::region`] is inherited (override with `.lang()` on
84    /// the returned builder). With the `translation` feature, a non-English
85    /// language translates text fields automatically.
86    pub fn ticker(&self, symbol: impl Into<String>) -> crate::TickerBuilder {
87        crate::Ticker::builder(symbol)
88            .lang(self.lang.clone())
89            .with_provider_set(Arc::clone(&self.set))
90    }
91
92    /// Create a [`TickersBuilder`](crate::TickersBuilder) pre-wired to this provider set.
93    ///
94    /// The returned builder accepts the same optional configuration as
95    /// [`Tickers::builder`](crate::Tickers::builder) (`.cache()`,
96    /// `.max_concurrency()`, `.logo()`, `.format()`) before calling `.build()`.
97    ///
98    /// The language configured via [`ProvidersBuilder::lang`] or
99    /// [`ProvidersBuilder::region`] is inherited (override with `.lang()` on
100    /// the returned builder). With the `translation` feature, a non-English
101    /// language translates text fields automatically.
102    pub fn tickers<S, I>(&self, symbols: I) -> crate::TickersBuilder
103    where
104        S: Into<String>,
105        I: IntoIterator<Item = S>,
106    {
107        crate::Tickers::builder(symbols)
108            .lang(self.lang.clone())
109            .with_provider_set(Arc::clone(&self.set))
110    }
111
112    /// Create a [`CryptoCoin`](crate::CryptoCoin) handle backed by this provider set.
113    ///
114    /// Compiled in unconditionally. With no built-in provider for this
115    /// capability, register one with [`ProvidersBuilder::with_adapter`] or
116    /// enable its feature, or these calls return `NoProviderAvailable`.
117    pub fn crypto(&self, id: impl Into<String>) -> crate::domains::CryptoCoin {
118        crate::domains::CryptoCoin::with_providers(id.into().into(), Arc::clone(&self.set))
119    }
120
121    /// Create a [`ForexPair`](crate::ForexPair) handle backed by this provider set.
122    ///
123    /// Compiled in unconditionally. With no built-in provider for this
124    /// capability, register one with [`ProvidersBuilder::with_adapter`] or
125    /// enable its feature, or these calls return `NoProviderAvailable`.
126    pub fn forex(
127        &self,
128        from: impl Into<String>,
129        to: impl Into<String>,
130    ) -> crate::domains::ForexPair {
131        crate::domains::ForexPair::with_providers(
132            from.into().into(),
133            to.into().into(),
134            Arc::clone(&self.set),
135        )
136    }
137
138    /// Create an [`EconomicIndicator`](crate::EconomicIndicator) handle backed by this provider set.
139    ///
140    /// Compiled in unconditionally. With no built-in provider for this
141    /// capability, register one with [`ProvidersBuilder::with_adapter`] or
142    /// enable its feature, or these calls return `NoProviderAvailable`.
143    pub fn economic(&self, series_id: impl Into<String>) -> crate::domains::EconomicIndicator {
144        crate::domains::EconomicIndicator::with_providers(
145            series_id.into().into(),
146            Arc::clone(&self.set),
147        )
148    }
149
150    /// Create an [`Index`](crate::Index) handle backed by this provider set.
151    pub fn index(&self, symbol: impl Into<String>) -> crate::domains::Index {
152        crate::domains::Index::with_providers(symbol.into().into(), Arc::clone(&self.set))
153    }
154
155    /// Create a [`FuturesContract`](crate::FuturesContract) handle backed by this provider set.
156    pub fn futures(&self, symbol: impl Into<String>) -> crate::domains::FuturesContract {
157        crate::domains::FuturesContract::with_providers(symbol.into().into(), Arc::clone(&self.set))
158    }
159
160    /// Create a [`Commodity`](crate::Commodity) handle backed by this provider set.
161    pub fn commodity(&self, symbol: impl Into<String>) -> crate::domains::Commodity {
162        crate::domains::Commodity::with_providers(symbol.into().into(), Arc::clone(&self.set))
163    }
164
165    /// Create a [`Discovery`](crate::Discovery) handle backed by this provider set.
166    ///
167    /// Routes symbol search, reference data, and screening through
168    /// [`Capability::DISCOVERY`](crate::Capability::DISCOVERY). Distinct from
169    /// [`crate::finance::search`], which is a Yahoo-only shortcut.
170    pub fn discovery(&self) -> crate::domains::Discovery {
171        crate::domains::Discovery::with_providers(Arc::clone(&self.set))
172    }
173
174    /// Create a [`MarketCalendar`](crate::MarketCalendar) handle backed by this provider set.
175    ///
176    /// Routes market-wide earnings/IPO/dividend/split/economic calendars
177    /// through [`Capability::CALENDAR`](crate::Capability::CALENDAR).
178    pub fn calendar(&self) -> crate::domains::MarketCalendar {
179        crate::domains::MarketCalendar::with_providers(Arc::clone(&self.set))
180    }
181
182    /// Create a [`Market`](crate::Market) handle backed by this provider set.
183    ///
184    /// Routes sector/industry performance and movers through
185    /// [`Capability::MARKET`](crate::Capability::MARKET). Movers work on the
186    /// default keyless route (Yahoo screeners); the sector/industry
187    /// statistics need a keyed provider (FMP).
188    pub fn market(&self) -> crate::domains::Market {
189        crate::domains::Market::with_providers(Arc::clone(&self.set))
190    }
191
192    /// Create an [`EconomicCatalog`](crate::EconomicCatalog) handle backed by
193    /// this provider set.
194    ///
195    /// Routes series search and category/release browsing through
196    /// [`Capability::ECONOMIC`](crate::Capability::ECONOMIC). Unlike
197    /// [`economic`](Self::economic) it takes no series id — it is how you find
198    /// one.
199    ///
200    /// Compiled in unconditionally. With no built-in provider for this
201    /// capability, register one with [`ProvidersBuilder::with_adapter`] or
202    /// enable its feature, or these calls return `NoProviderAvailable`.
203    pub fn economic_catalog(&self) -> crate::domains::EconomicCatalog {
204        crate::domains::EconomicCatalog::with_providers(Arc::clone(&self.set))
205    }
206
207    /// Create a [`Snapshot`](crate::Snapshot) handle backed by this provider set.
208    ///
209    /// Routes cross-market snapshots through
210    /// [`Capability::QUOTE`](crate::Capability::QUOTE). Needs a provider whose
211    /// snapshot endpoint spans asset classes, currently Polygon alone.
212    ///
213    /// Compiled in unconditionally. With no built-in provider for this
214    /// capability, register one with [`ProvidersBuilder::with_adapter`] or
215    /// enable its feature, or these calls return `NoProviderAvailable`.
216    pub fn snapshot(&self) -> crate::domains::Snapshot {
217        crate::domains::Snapshot::with_providers(Arc::clone(&self.set))
218    }
219
220    /// Create a [`Filings`](crate::Filings) handle backed by this provider set.
221    ///
222    /// Always available — EDGAR is auto-injected when no other FILINGS provider
223    /// is configured.
224    pub fn filings(&self, symbol: impl Into<String>) -> crate::domains::Filings {
225        crate::domains::Filings::with_providers(symbol.into().into(), Arc::clone(&self.set))
226    }
227
228    /// Snapshot recent health for every configured provider.
229    ///
230    /// Each [`ProviderHealth`] entry reflects up to the last 20 dispatch
231    /// outcomes recorded in-process for that provider (recency window is
232    /// internal and unspecified beyond "recent"), plus a best-effort
233    /// rate-limit budget estimate where the provider exposes one. Purely
234    /// observational — it does not affect routing or retries.
235    ///
236    /// # Example
237    ///
238    /// ```no_run
239    /// use finance_query::Providers;
240    ///
241    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
242    /// let providers = Providers::builder().build().await?;
243    /// for health in providers.health() {
244    ///     println!("{:?}: healthy={}", health.provider, health.is_healthy);
245    /// }
246    /// # Ok(())
247    /// # }
248    /// ```
249    pub fn health(&self) -> Vec<ProviderHealth> {
250        self.set.health()
251    }
252}
253
254/// Builder for [`Providers`].
255pub struct ProvidersBuilder {
256    provider_ids: Vec<Provider>,
257    adapters: Vec<Arc<dyn crate::ProviderAdapter>>,
258    config: ClientConfig,
259    routes: Routes,
260    retry: Option<RetryPolicy>,
261    api_keys: std::collections::HashMap<&'static str, String>,
262}
263
264impl std::fmt::Debug for ProvidersBuilder {
265    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266        f.debug_struct("ProvidersBuilder")
267            .field("provider_ids", &self.provider_ids)
268            .field(
269                "adapters",
270                &self.adapters.iter().map(|a| a.id()).collect::<Vec<_>>(),
271            )
272            .field("routes", &self.routes)
273            .field("retry", &self.retry)
274            .field(
275                "api_keys",
276                &self.api_keys.keys().copied().collect::<Vec<_>>(),
277            )
278            .finish()
279    }
280}
281
282impl Default for ProvidersBuilder {
283    fn default() -> Self {
284        Self {
285            provider_ids: vec![Provider::Yahoo],
286            adapters: Vec::new(),
287            config: ClientConfig::default(),
288            routes: Routes::new(Fetch::Sequential),
289            retry: None,
290            api_keys: std::collections::HashMap::new(),
291        }
292    }
293}
294
295impl ProvidersBuilder {
296    /// Configure how providers are queried. Default: `Sequential`.
297    ///
298    /// Use [`Fetch::Sequential`] or [`Fetch::Parallel`].
299    pub fn fetch(mut self, mode: Fetch) -> Self {
300        self.routes.fetch = mode;
301        self
302    }
303
304    /// Set `provider`'s API key for the [`Providers`] this builds, instead of
305    /// the process-global key from its `init` function.
306    ///
307    /// Two `Providers` can hold different keys for the same provider, and a
308    /// key can be replaced without restarting. Each distinct key gets its own
309    /// rate-limit budget. Providers left unset fall back to the process-global
310    /// key, then to the environment variable.
311    pub fn api_key(mut self, provider: Provider, key: impl Into<String>) -> Self {
312        self.api_keys.insert(provider.as_str(), key.into());
313        self
314    }
315
316    /// Route a capability to a specific provider priority list.
317    ///
318    /// Providers referenced in the route are automatically added to the
319    /// initialisation list if not already present. If omitted for a capability,
320    /// Yahoo is used as default.
321    pub fn route(
322        self,
323        cap: crate::providers::Capability,
324        providers: impl IntoIterator<Item = Provider>,
325    ) -> Self {
326        self.insert_route(cap, providers, None)
327    }
328
329    /// Route a capability, overriding [`fetch`](Self::fetch) for it alone.
330    ///
331    /// Lets a quota-limited capability stay sequential while another races
332    /// its providers.
333    pub fn route_with(
334        self,
335        cap: crate::providers::Capability,
336        providers: impl IntoIterator<Item = Provider>,
337        fetch: Fetch,
338    ) -> Self {
339        self.insert_route(cap, providers, Some(fetch))
340    }
341
342    fn insert_route(
343        mut self,
344        cap: crate::providers::Capability,
345        providers: impl IntoIterator<Item = Provider>,
346        fetch: Option<Fetch>,
347    ) -> Self {
348        let providers: Vec<Provider> = providers.into_iter().collect();
349        for provider in &providers {
350            // A Custom id has no arm in `build_providers`; it arrives only
351            // through `with_adapter`.
352            if matches!(provider, Provider::Custom(_)) {
353                continue;
354            }
355            if !self.provider_ids.contains(provider) {
356                self.provider_ids.push(*provider);
357            }
358        }
359        self.routes
360            .map
361            .insert(cap, super::routes::Route { providers, fetch });
362        self
363    }
364
365    /// Set the region (automatically sets lang and region code).
366    pub fn region(mut self, region: crate::constants::Region) -> Self {
367        self.config.lang = region.lang().to_string();
368        self.config.region = region.region().to_string();
369        self
370    }
371
372    /// Set the language code (e.g., "en-US", "ja-JP").
373    ///
374    /// Inherited by every `Ticker`/`Tickers` handle created from the built
375    /// [`Providers`]. With the `translation` feature, a non-English language
376    /// translates text fields on those handles automatically.
377    pub fn lang(mut self, lang: impl Into<String>) -> Self {
378        self.config.lang = lang.into();
379        self
380    }
381
382    /// Set the region code (e.g., "US", "JP", "DE").
383    ///
384    /// For standard countries, prefer `.region()` instead to ensure correct
385    /// lang/region pairing.
386    pub fn region_code(mut self, region: impl Into<String>) -> Self {
387        self.config.region = region.into();
388        self
389    }
390
391    /// Set the HTTP request timeout.
392    pub fn timeout(mut self, t: Duration) -> Self {
393        self.config.timeout = t;
394        self
395    }
396
397    /// Set the proxy URL.
398    pub fn proxy(mut self, p: impl Into<String>) -> Self {
399        self.config.proxy = Some(p.into());
400        self
401    }
402
403    /// Opt into retrying `FinanceError::RateLimited` errors during dispatch
404    /// See [`RetryPolicy`] for the exact semantics.
405    ///
406    /// **Default is no retry** — omitting this call preserves the exact
407    /// prior behavior: a `RateLimited` error is treated like any other
408    /// failure and dispatch moves straight to the next routed provider.
409    pub fn retry(mut self, policy: RetryPolicy) -> Self {
410        self.retry = Some(policy);
411        self
412    }
413
414    /// Register an adapter this crate does not build itself.
415    ///
416    /// Route to it by the id its [`ProviderCore::id`](crate::ProviderCore::id)
417    /// returns, usually [`Provider::Custom`]. Registering alone does not route
418    /// anything: a capability with no explicit route still falls back to its
419    /// default provider.
420    ///
421    /// ```no_run
422    /// # use std::sync::Arc;
423    /// # use finance_query::{Capability, Provider, Providers, ProviderAdapter};
424    /// # async fn f(my_adapter: Arc<dyn ProviderAdapter>) -> finance_query::Result<()> {
425    /// let providers = Providers::builder()
426    ///     .with_adapter(my_adapter)
427    ///     .route(Capability::ECONOMIC, [Provider::custom("my-source")])
428    ///     .build()
429    ///     .await?;
430    /// # let _ = providers;
431    /// # Ok(())
432    /// # }
433    /// ```
434    #[must_use]
435    pub fn with_adapter(mut self, adapter: Arc<dyn crate::ProviderAdapter>) -> Self {
436        self.adapters.push(adapter);
437        self
438    }
439
440    /// Build the [`Providers`] instance, initialising all configured providers.
441    pub async fn build(self) -> Result<Providers> {
442        #[cfg(feature = "translation")]
443        crate::translation::Lang::parse(&self.config.lang)?;
444        for adapter in &self.adapters {
445            let id = adapter.id();
446            // `from_id_str` resolves interned custom ids too, so ask the
447            // built-in list directly.
448            if let Provider::Custom(custom) = id
449                && Provider::all()
450                    .iter()
451                    .any(|p| p.as_str() == custom.as_str())
452            {
453                return Err(crate::FinanceError::InvalidParameter {
454                    param: "adapter".to_string(),
455                    reason: format!(
456                        "custom provider id `{}` collides with a built-in",
457                        custom.as_str()
458                    ),
459                });
460            }
461            let duplicate = self.provider_ids.contains(&id)
462                || self.adapters.iter().filter(|a| a.id() == id).count() > 1;
463            if duplicate {
464                return Err(crate::FinanceError::InvalidParameter {
465                    param: "adapter".to_string(),
466                    reason: format!("provider id `{id}` is already configured"),
467                });
468            }
469        }
470        let lang = self.config.lang.clone();
471        let mut keys = crate::adapters::keys::KeyMap::new();
472        for (provider_key, api_key) in self.api_keys {
473            if api_key.trim().is_empty() {
474                return Err(crate::FinanceError::InvalidParameter {
475                    param: provider_key.to_string(),
476                    reason: "API key must not be empty".to_string(),
477                });
478            }
479            keys.insert(
480                provider_key,
481                crate::adapters::keys::ScopedKey::new(api_key, self.config.timeout),
482            );
483        }
484        // `initialize` builds each keyed client, so the scope has to cover
485        // construction as well as dispatch.
486        let set = crate::adapters::keys::scope(
487            Arc::new(keys.clone()),
488            build_providers(&self.provider_ids, self.adapters, &self.config, self.routes),
489        )
490        .await?
491        .with_api_keys(keys)
492        .with_retry_policy(self.retry);
493        Ok(Providers {
494            set: Arc::new(set),
495            lang,
496        })
497    }
498}