use serde::{Deserialize, Serialize};
use crate::adapters::common::encode_path_segment;
use crate::error::{FinanceError, Result};
use super::build_client;
use super::models::PaginatedResponse;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TickerRef {
pub ticker: Option<String>,
pub name: Option<String>,
pub market: Option<String>,
pub locale: Option<String>,
pub primary_exchange: Option<String>,
#[serde(rename = "type")]
pub asset_type: Option<String>,
pub active: Option<bool>,
pub currency_name: Option<String>,
pub cik: Option<String>,
pub composite_figi: Option<String>,
pub share_class_figi: Option<String>,
pub last_updated_utc: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TickerDetails {
pub ticker: Option<String>,
pub name: Option<String>,
pub market: Option<String>,
pub locale: Option<String>,
pub primary_exchange: Option<String>,
#[serde(rename = "type")]
pub asset_type: Option<String>,
pub active: Option<bool>,
pub currency_name: Option<String>,
pub cik: Option<String>,
pub sic_code: Option<String>,
pub sic_description: Option<String>,
pub description: Option<String>,
pub homepage_url: Option<String>,
pub total_employees: Option<u64>,
pub market_cap: Option<f64>,
pub phone_number: Option<String>,
pub address: Option<serde_json::Value>,
pub branding: Option<serde_json::Value>,
pub list_date: Option<String>,
pub share_class_shares_outstanding: Option<f64>,
pub weighted_shares_outstanding: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TickerDetailsResponse {
pub request_id: Option<String>,
pub status: Option<String>,
pub results: Option<TickerDetails>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TickerType {
pub code: Option<String>,
pub description: Option<String>,
pub asset_class: Option<String>,
pub locale: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RelatedTicker {
pub ticker: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Exchange {
pub id: Option<i64>,
#[serde(rename = "type")]
pub exchange_type: Option<String>,
pub asset_class: Option<String>,
pub locale: Option<String>,
pub name: Option<String>,
pub mic: Option<String>,
pub operating_mic: Option<String>,
pub participant_id: Option<String>,
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Condition {
pub id: Option<i32>,
#[serde(rename = "type")]
pub condition_type: Option<String>,
pub name: Option<String>,
pub description: Option<String>,
pub asset_class: Option<String>,
pub sip_mapping: Option<serde_json::Value>,
pub data_types: Option<Vec<String>>,
pub legacy: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct MarketHoliday {
pub name: Option<String>,
pub date: Option<String>,
pub exchange: Option<String>,
pub status: Option<String>,
pub open: Option<String>,
pub close: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct MarketStatusResponse {
#[serde(rename = "afterHours")]
pub after_hours: Option<bool>,
#[serde(rename = "earlyHours")]
pub early_hours: Option<bool>,
pub market: Option<String>,
#[serde(rename = "serverTime")]
pub server_time: Option<String>,
pub exchanges: Option<serde_json::Value>,
pub currencies: Option<serde_json::Value>,
}
pub async fn all_tickers(params: &[(&str, &str)]) -> Result<PaginatedResponse<TickerRef>> {
let client = build_client()?;
client.get("/v3/reference/tickers", params).await
}
pub async fn ticker_details(ticker: &str) -> Result<TickerDetailsResponse> {
let client = build_client()?;
let path = format!("/v3/reference/tickers/{}", encode_path_segment(ticker));
let json = client.get_raw(&path, &[]).await?;
serde_json::from_value(json).map_err(|e| FinanceError::ResponseStructureError {
field: "ticker_details".to_string(),
context: format!("Failed to parse ticker details: {e}"),
})
}
pub async fn ticker_types(params: &[(&str, &str)]) -> Result<PaginatedResponse<TickerType>> {
let client = build_client()?;
client.get("/v3/reference/tickers/types", params).await
}
pub async fn related_tickers(ticker: &str) -> Result<PaginatedResponse<RelatedTicker>> {
let client = build_client()?;
let path = format!("/v1/related-companies/{}", encode_path_segment(ticker));
client.get(&path, &[]).await
}
pub async fn exchanges(params: &[(&str, &str)]) -> Result<PaginatedResponse<Exchange>> {
let client = build_client()?;
client.get("/v3/reference/exchanges", params).await
}
pub async fn condition_codes(params: &[(&str, &str)]) -> Result<PaginatedResponse<Condition>> {
let client = build_client()?;
client.get("/v3/reference/conditions", params).await
}
pub async fn market_holidays() -> Result<Vec<MarketHoliday>> {
let client = build_client()?;
let json = client.get_raw("/v1/marketstatus/upcoming", &[]).await?;
serde_json::from_value(json).map_err(|e| FinanceError::ResponseStructureError {
field: "market_holidays".to_string(),
context: format!("Failed to parse market holidays: {e}"),
})
}
pub async fn market_status() -> Result<MarketStatusResponse> {
let client = build_client()?;
let json = client.get_raw("/v1/marketstatus/now", &[]).await?;
serde_json::from_value(json).map_err(|e| FinanceError::ResponseStructureError {
field: "market_status".to_string(),
context: format!("Failed to parse market status: {e}"),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_ticker_details_mock() {
let mut server = mockito::Server::new_async().await;
let _mock = server
.mock("GET", "/v3/reference/tickers/AAPL")
.match_query(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("apiKey".into(), "test-key".into()),
]))
.with_status(200)
.with_body(serde_json::json!({
"request_id": "abc",
"status": "OK",
"results": {
"ticker": "AAPL",
"name": "Apple Inc.",
"market": "stocks",
"locale": "us",
"primary_exchange": "XNAS",
"type": "CS",
"active": true,
"currency_name": "usd",
"market_cap": 2850000000000.0,
"description": "Apple Inc. designs, manufactures, and markets smartphones..."
}
}).to_string())
.create_async().await;
let client = super::super::build_test_client(&server.url()).unwrap();
let json = client
.get_raw("/v3/reference/tickers/AAPL", &[])
.await
.unwrap();
let resp: TickerDetailsResponse = serde_json::from_value(json).unwrap();
let details = resp.results.unwrap();
assert_eq!(details.name.as_deref(), Some("Apple Inc."));
assert_eq!(details.ticker.as_deref(), Some("AAPL"));
assert!((details.market_cap.unwrap() - 2850000000000.0).abs() < 1.0);
}
#[tokio::test]
async fn test_market_status_mock() {
let mut server = mockito::Server::new_async().await;
let _mock = server
.mock("GET", "/v1/marketstatus/now")
.match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
"apiKey".into(),
"test-key".into(),
)]))
.with_status(200)
.with_body(
serde_json::json!({
"market": "open",
"earlyHours": false,
"afterHours": false,
"serverTime": "2024-01-15T12:00:00-05:00"
})
.to_string(),
)
.create_async()
.await;
let client = super::super::build_test_client(&server.url()).unwrap();
let json = client.get_raw("/v1/marketstatus/now", &[]).await.unwrap();
let resp: MarketStatusResponse = serde_json::from_value(json).unwrap();
assert_eq!(resp.market.as_deref(), Some("open"));
}
}