Skip to main content

fmp_rust_sdk/
endpoints.rs

1//! Typed convenience methods and explicit Stable API query options.
2//!
3//! Endpoint access remains authoritative at FMP: a configured subscription never
4//! suppresses a request. Use `Client::get` with your own models for other Stable
5//! endpoints documented by FMP.
6
7use chrono::NaiveDate;
8use serde::{Deserialize, Serialize};
9
10use crate::{Client, Error, Result, models::*};
11
12/// The reporting periods accepted by the statement endpoints.
13#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
14pub enum Period {
15    #[serde(rename = "annual")]
16    Annual,
17    #[serde(rename = "quarter")]
18    Quarter,
19    #[serde(rename = "Q1")]
20    Q1,
21    #[serde(rename = "Q2")]
22    Q2,
23    #[serde(rename = "Q3")]
24    Q3,
25    #[serde(rename = "Q4")]
26    Q4,
27}
28
29/// The analyst estimate endpoint accepts only annual or quarterly periods.
30#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
31pub enum EstimatePeriod {
32    #[serde(rename = "annual")]
33    Annual,
34    #[serde(rename = "quarter")]
35    Quarter,
36}
37
38/// Documented intraday chart intervals.
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40pub enum IntradayInterval {
41    OneMinute,
42    FiveMinutes,
43    FifteenMinutes,
44    ThirtyMinutes,
45    OneHour,
46    FourHours,
47}
48
49impl IntradayInterval {
50    fn path(self) -> &'static str {
51        match self {
52            Self::OneMinute => "historical-chart/1min",
53            Self::FiveMinutes => "historical-chart/5min",
54            Self::FifteenMinutes => "historical-chart/15min",
55            Self::ThirtyMinutes => "historical-chart/30min",
56            Self::OneHour => "historical-chart/1hour",
57            Self::FourHours => "historical-chart/4hour",
58        }
59    }
60}
61
62/// Optional inclusive date bounds. `None` leaves the bound to FMP.
63#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
64pub struct DateRange {
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub from: Option<NaiveDate>,
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub to: Option<NaiveDate>,
69}
70
71/// An explicit page and record limit; page numbering starts at zero.
72#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
73pub struct Pagination {
74    pub page: u32,
75    pub limit: u32,
76}
77
78/// Optional search filters for `search-symbol` and `search-name`.
79#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
80pub struct SearchOptions<'a> {
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub limit: Option<u32>,
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub exchange: Option<&'a str>,
85}
86
87/// An explicit period and number of statements, ratios, or metrics.
88#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
89pub struct StatementOptions {
90    pub period: Period,
91    pub limit: u32,
92}
93
94/// Optional date bounds and explicit pagination for news queries.
95#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
96pub struct NewsOptions {
97    #[serde(flatten)]
98    pub dates: DateRange,
99    #[serde(flatten)]
100    pub pagination: Pagination,
101}
102
103#[derive(Serialize)]
104struct EmptyQuery {}
105
106#[derive(Serialize)]
107struct SymbolQuery<'a> {
108    symbol: &'a str,
109}
110
111#[derive(Serialize)]
112struct SymbolsQuery<'a> {
113    symbols: &'a str,
114}
115
116#[derive(Serialize)]
117struct SearchQuery<'a, 'b> {
118    query: &'a str,
119    #[serde(flatten)]
120    options: &'b SearchOptions<'a>,
121}
122
123#[derive(Serialize)]
124struct StatementQuery<'a> {
125    symbol: &'a str,
126    #[serde(flatten)]
127    options: StatementOptions,
128}
129
130#[derive(Serialize)]
131struct HistoricalQuery<'a> {
132    symbol: &'a str,
133    #[serde(flatten)]
134    dates: DateRange,
135}
136
137#[derive(Serialize)]
138struct HistoricalMarketCapQuery<'a> {
139    symbol: &'a str,
140    #[serde(flatten)]
141    dates: DateRange,
142    limit: u32,
143}
144
145#[derive(Serialize)]
146struct NewsQuery<'a> {
147    symbols: &'a str,
148    #[serde(flatten)]
149    options: NewsOptions,
150}
151
152#[derive(Serialize)]
153struct AnalystQuery<'a> {
154    symbol: &'a str,
155    period: EstimatePeriod,
156    #[serde(flatten)]
157    pagination: Pagination,
158}
159
160#[derive(Serialize)]
161struct TranscriptQuery<'a> {
162    symbol: &'a str,
163    year: u16,
164    quarter: u8,
165}
166
167#[derive(Serialize)]
168struct SectorSnapshotQuery<'a> {
169    date: NaiveDate,
170    #[serde(skip_serializing_if = "Option::is_none")]
171    exchange: Option<&'a str>,
172    #[serde(skip_serializing_if = "Option::is_none")]
173    sector: Option<&'a str>,
174}
175
176#[derive(Serialize)]
177struct IndustrySnapshotQuery<'a> {
178    date: NaiveDate,
179    #[serde(skip_serializing_if = "Option::is_none")]
180    exchange: Option<&'a str>,
181    #[serde(skip_serializing_if = "Option::is_none")]
182    industry: Option<&'a str>,
183}
184
185/// Validate a required query value before sending it to FMP.
186fn required_value(name: &str, value: &str) -> Result<()> {
187    if value.trim().is_empty() {
188        return Err(Error::Configuration {
189            reason: format!("request parameter '{name}' must not be empty"),
190        });
191    }
192    Ok(())
193}
194
195fn positive_limit(limit: u32) -> Result<()> {
196    if limit == 0 {
197        return Err(Error::Configuration {
198            reason: "request parameter 'limit' must be greater than zero".to_owned(),
199        });
200    }
201    Ok(())
202}
203
204fn valid_dates(dates: DateRange) -> Result<()> {
205    if let (Some(from), Some(to)) = (dates.from, dates.to)
206        && from > to
207    {
208        return Err(Error::Configuration {
209            reason: format!("request date range is reversed: from={from}, to={to}"),
210        });
211    }
212    Ok(())
213}
214
215fn joined_symbols(symbols: &[&str]) -> Result<String> {
216    if symbols.is_empty() {
217        return Err(Error::Configuration {
218            reason: "request parameter 'symbols' must contain at least one symbol".to_owned(),
219        });
220    }
221    for symbol in symbols {
222        required_value("symbols", symbol)?;
223        if symbol.contains(',') {
224            return Err(Error::Configuration {
225                reason: format!("each batch symbol must be a single ticker, received '{symbol}'"),
226            });
227        }
228    }
229    Ok(symbols.join(","))
230}
231
232macro_rules! symbol_endpoint {
233    ($name:ident, $path:literal, $response:ty, $doc:literal) => {
234        impl Client {
235            #[doc = $doc]
236            pub async fn $name(&self, symbol: &str) -> Result<Vec<$response>> {
237                required_value("symbol", symbol)?;
238                self.get($path, &SymbolQuery { symbol }).await
239            }
240        }
241    };
242}
243
244macro_rules! batch_endpoint {
245    ($name:ident, $path:literal, $response:ty, $doc:literal) => {
246        impl Client {
247            #[doc = $doc]
248            pub async fn $name(&self, symbols: &[&str]) -> Result<Vec<$response>> {
249                let symbols = joined_symbols(symbols)?;
250                self.get($path, &SymbolsQuery { symbols: &symbols }).await
251            }
252        }
253    };
254}
255
256macro_rules! directory_endpoint {
257    ($name:ident, $path:literal, $response:ty, $doc:literal) => {
258        impl Client {
259            #[doc = $doc]
260            pub async fn $name(&self) -> Result<Vec<$response>> {
261                self.get($path, &EmptyQuery {}).await
262            }
263        }
264    };
265}
266
267macro_rules! statement_endpoint {
268    ($name:ident, $path:literal, $response:ty, $doc:literal) => {
269        impl Client {
270            #[doc = $doc]
271            pub async fn $name(
272                &self,
273                symbol: &str,
274                options: StatementOptions,
275            ) -> Result<Vec<$response>> {
276                required_value("symbol", symbol)?;
277                positive_limit(options.limit)?;
278                self.get($path, &StatementQuery { symbol, options }).await
279            }
280        }
281    };
282}
283
284mod methods;