finance_query/models/discovery/
listings.rs1use crate::{FinanceError, Result};
4use serde::{Deserialize, Serialize};
5
6pub(crate) fn date(value: &str) -> Result<()> {
7 if chrono::NaiveDate::parse_from_str(value, "%Y-%m-%d")
8 .is_ok_and(|d| d.format("%Y-%m-%d").to_string() == value)
9 {
10 Ok(())
11 } else {
12 Err(FinanceError::InvalidParameter {
13 param: "date".into(),
14 reason: "expected a valid YYYY-MM-DD date".into(),
15 })
16 }
17}
18
19#[derive(Debug, Clone, Serialize)]
21pub struct StockListingRequest {
22 pub(crate) date: String,
23 pub(crate) active: bool,
24 pub(crate) stock_type: Option<String>,
25 pub(crate) locale: String,
26 pub(crate) limit: u32,
27}
28
29impl StockListingRequest {
30 pub fn new(as_of: &str, active: bool) -> Result<Self> {
32 date(as_of)?;
33 Ok(Self {
34 date: as_of.into(),
35 active,
36 stock_type: None,
37 locale: "us".into(),
38 limit: 1000,
39 })
40 }
41
42 pub fn with_stock_type(mut self, code: impl Into<String>) -> Self {
44 self.stock_type = Some(code.into());
45 self
46 }
47
48 pub fn with_locale(mut self, locale: impl Into<String>) -> Self {
50 self.locale = locale.into();
51 self
52 }
53
54 pub fn as_of(&self) -> &str {
56 &self.date
57 }
58
59 pub fn active(&self) -> bool {
61 self.active
62 }
63
64 pub fn stock_type(&self) -> Option<&str> {
66 self.stock_type.as_deref()
67 }
68
69 pub fn locale(&self) -> &str {
71 &self.locale
72 }
73
74 pub fn page_size(&self) -> u32 {
76 self.limit
77 }
78
79 pub(crate) fn cursor_identity(&self) -> String {
81 serde_json::json!([
82 self.date,
83 self.active,
84 self.stock_type,
85 self.locale,
86 self.limit
87 ])
88 .to_string()
89 }
90
91 pub fn with_page_size(mut self, limit: u32) -> Result<Self> {
93 if !(1..=1000).contains(&limit) {
94 return Err(FinanceError::InvalidParameter {
95 param: "page_size".into(),
96 reason: "expected 1..=1000".into(),
97 });
98 }
99 self.limit = limit;
100 Ok(self)
101 }
102}
103
104#[derive(Debug, Clone, Default, Serialize, Deserialize)]
106#[non_exhaustive]
107pub struct StockListing {
108 pub symbol: String,
110 pub name: Option<String>,
112 pub exchange: Option<String>,
114 pub stock_type: Option<String>,
116 pub locale: Option<String>,
118 pub active: Option<bool>,
120 pub cik: Option<String>,
122 pub composite_figi: Option<String>,
124 pub share_class_figi: Option<String>,
126 pub list_date: Option<String>,
128 pub delisted_utc: Option<String>,
130}
131
132#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
134#[non_exhaustive]
135pub struct TickerChange {
136 pub date: String,
138 pub ticker: String,
140}
141
142#[derive(Debug, Clone, Default, Serialize, Deserialize)]
144#[non_exhaustive]
145pub struct StockType {
146 pub code: String,
148 pub description: Option<String>,
150 pub asset_class: Option<String>,
152 pub locale: Option<String>,
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn cursor_identity_is_pinned_to_the_query_fields() {
162 let request = StockListingRequest::new("2020-01-02", false)
163 .unwrap()
164 .with_stock_type("CS")
165 .with_page_size(2)
166 .unwrap();
167 assert_eq!(
168 request.cursor_identity(),
169 r#"["2020-01-02",false,"CS","us",2]"#
170 );
171 }
172}