1use chrono::NaiveDate;
8use serde::{Deserialize, Serialize};
9
10use crate::{Client, Error, Result, models::*};
11
12#[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#[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#[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#[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#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
73pub struct Pagination {
74 pub page: u32,
75 pub limit: u32,
76}
77
78#[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#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
89pub struct StatementOptions {
90 pub period: Period,
91 pub limit: u32,
92}
93
94#[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
185fn 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;