use crate::{FinanceError, Result};
use serde::{Deserialize, Serialize};
pub(crate) fn date(value: &str) -> Result<()> {
if chrono::NaiveDate::parse_from_str(value, "%Y-%m-%d")
.is_ok_and(|d| d.format("%Y-%m-%d").to_string() == value)
{
Ok(())
} else {
Err(FinanceError::InvalidParameter {
param: "date".into(),
reason: "expected a valid YYYY-MM-DD date".into(),
})
}
}
#[derive(Debug, Clone, Serialize)]
pub struct StockListingRequest {
pub(crate) date: String,
pub(crate) active: bool,
pub(crate) stock_type: Option<String>,
pub(crate) locale: String,
pub(crate) limit: u32,
}
impl StockListingRequest {
pub fn new(as_of: &str, active: bool) -> Result<Self> {
date(as_of)?;
Ok(Self {
date: as_of.into(),
active,
stock_type: None,
locale: "us".into(),
limit: 1000,
})
}
pub fn with_stock_type(mut self, code: impl Into<String>) -> Self {
self.stock_type = Some(code.into());
self
}
pub fn with_locale(mut self, locale: impl Into<String>) -> Self {
self.locale = locale.into();
self
}
pub fn as_of(&self) -> &str {
&self.date
}
pub fn active(&self) -> bool {
self.active
}
pub fn stock_type(&self) -> Option<&str> {
self.stock_type.as_deref()
}
pub fn locale(&self) -> &str {
&self.locale
}
pub fn page_size(&self) -> u32 {
self.limit
}
pub(crate) fn cursor_identity(&self) -> String {
serde_json::json!([
self.date,
self.active,
self.stock_type,
self.locale,
self.limit
])
.to_string()
}
pub fn with_page_size(mut self, limit: u32) -> Result<Self> {
if !(1..=1000).contains(&limit) {
return Err(FinanceError::InvalidParameter {
param: "page_size".into(),
reason: "expected 1..=1000".into(),
});
}
self.limit = limit;
Ok(self)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct StockListing {
pub symbol: String,
pub name: Option<String>,
pub exchange: Option<String>,
pub stock_type: Option<String>,
pub locale: Option<String>,
pub active: Option<bool>,
pub cik: Option<String>,
pub composite_figi: Option<String>,
pub share_class_figi: Option<String>,
pub list_date: Option<String>,
pub delisted_utc: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TickerChange {
pub date: String,
pub ticker: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct StockType {
pub code: String,
pub description: Option<String>,
pub asset_class: Option<String>,
pub locale: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cursor_identity_is_pinned_to_the_query_fields() {
let request = StockListingRequest::new("2020-01-02", false)
.unwrap()
.with_stock_type("CS")
.with_page_size(2)
.unwrap();
assert_eq!(
request.cursor_identity(),
r#"["2020-01-02",false,"CS","us",2]"#
);
}
}