use crate::client::FmpClient;
use crate::error::Result;
use crate::models::quote::{PriceChange, ShortQuote, StockQuote};
use serde::Serialize;
pub struct Quote {
client: FmpClient,
}
impl Quote {
pub(crate) fn new(client: FmpClient) -> Self {
Self { client }
}
pub async fn get_quote(&self, symbol: &str) -> Result<Vec<StockQuote>> {
#[derive(Serialize)]
struct Query<'a> {
symbol: &'a str,
apikey: &'a str,
}
let url = self.client.build_url("/quote");
self.client
.get_with_query(
&url,
&Query {
symbol,
apikey: self.client.api_key(),
},
)
.await
}
pub async fn get_quote_short(&self, symbol: &str) -> Result<Vec<ShortQuote>> {
#[derive(Serialize)]
struct Query<'a> {
symbol: &'a str,
apikey: &'a str,
}
let url = self.client.build_url("/quote-short");
self.client
.get_with_query(
&url,
&Query {
symbol,
apikey: self.client.api_key(),
},
)
.await
}
pub async fn get_quote_batch(&self, symbols: &[&str]) -> Result<Vec<StockQuote>> {
#[derive(Serialize)]
struct Query<'a> {
symbols: String,
apikey: &'a str,
}
let url = self.client.build_url("/batch-quote");
self.client
.get_with_query(
&url,
&Query {
symbols: symbols.join(","),
apikey: self.client.api_key(),
},
)
.await
}
pub async fn get_price_change(&self, symbol: &str) -> Result<Vec<PriceChange>> {
#[derive(Serialize)]
struct Query<'a> {
symbol: &'a str,
apikey: &'a str,
}
let url = self.client.build_url("/stock-price-change");
self.client
.get_with_query(
&url,
&Query {
symbol,
apikey: self.client.api_key(),
},
)
.await
}
pub async fn get_exchange_quotes(&self, exchange: &str) -> Result<Vec<StockQuote>> {
#[derive(Serialize)]
struct Query<'a> {
exchange: &'a str,
apikey: &'a str,
}
let url = self.client.build_url("/batch-exchange-quote");
self.client
.get_with_query(
&url,
&Query {
exchange,
apikey: self.client.api_key(),
},
)
.await
}
pub async fn get_all_etf_quotes(&self) -> Result<Vec<StockQuote>> {
#[derive(Serialize)]
struct Query<'a> {
apikey: &'a str,
}
let url = self.client.build_url("/batch-etf-quotes");
self.client
.get_with_query(
&url,
&Query {
apikey: self.client.api_key(),
},
)
.await
}
pub async fn get_all_crypto_quotes(&self) -> Result<Vec<StockQuote>> {
#[derive(Serialize)]
struct Query<'a> {
apikey: &'a str,
}
let url = self.client.build_url("/batch-crypto-quotes");
self.client
.get_with_query(
&url,
&Query {
apikey: self.client.api_key(),
},
)
.await
}
pub async fn get_all_forex_quotes(&self) -> Result<Vec<StockQuote>> {
#[derive(Serialize)]
struct Query<'a> {
apikey: &'a str,
}
let url = self.client.build_url("/batch-forex-quotes");
self.client
.get_with_query(
&url,
&Query {
apikey: self.client.api_key(),
},
)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[ignore = "requires FMP API key"]
async fn test_get_quote() {
let client = FmpClient::new().unwrap();
let result = client.quote().get_quote("AAPL").await;
assert!(result.is_ok());
let quotes = result.unwrap();
assert!(!quotes.is_empty());
assert_eq!(quotes[0].symbol, "AAPL");
}
#[tokio::test]
#[ignore = "requires FMP API key"]
async fn test_get_quote_batch() {
let client = FmpClient::new().unwrap();
let result = client
.quote()
.get_quote_batch(&["AAPL", "MSFT", "GOOGL"])
.await;
assert!(result.is_ok());
let quotes = result.unwrap();
assert_eq!(quotes.len(), 3);
}
#[tokio::test]
#[ignore = "requires FMP API key"]
async fn test_get_quote_short() {
let client = FmpClient::new().unwrap();
let result = client.quote().get_quote_short("AAPL").await;
assert!(result.is_ok());
let quotes = result.unwrap();
assert!(!quotes.is_empty());
assert_eq!(quotes[0].symbol, "AAPL");
}
#[tokio::test]
#[ignore = "requires FMP API key"]
async fn test_get_price_change() {
let client = FmpClient::new().unwrap();
let result = client.quote().get_price_change("AAPL").await;
assert!(result.is_ok());
let changes = result.unwrap();
assert!(!changes.is_empty());
assert_eq!(changes[0].symbol, "AAPL");
}
#[tokio::test]
#[ignore = "requires FMP API key"]
async fn test_get_exchange_quotes() {
let client = FmpClient::new().unwrap();
let result = client.quote().get_exchange_quotes("NASDAQ").await;
assert!(result.is_ok());
let quotes = result.unwrap();
assert!(!quotes.is_empty());
let has_nasdaq = quotes.iter().any(|q| q.exchange.contains("NASDAQ"));
assert!(has_nasdaq || quotes.len() > 0); }
#[tokio::test]
#[ignore = "requires FMP API key"]
async fn test_get_all_etf_quotes() {
let client = FmpClient::new().unwrap();
let result = client.quote().get_all_etf_quotes().await;
assert!(result.is_ok());
let quotes = result.unwrap();
assert!(!quotes.is_empty());
assert!(quotes.iter().any(|q| q.symbol.len() >= 3));
}
#[tokio::test]
#[ignore = "requires FMP API key"]
async fn test_get_all_crypto_quotes() {
let client = FmpClient::new().unwrap();
let result = client.quote().get_all_crypto_quotes().await;
assert!(result.is_ok());
let quotes = result.unwrap();
assert!(!quotes.is_empty());
assert!(
quotes
.iter()
.any(|q| q.symbol.contains("USD") || q.symbol.contains("BTC"))
);
}
#[tokio::test]
#[ignore = "requires FMP API key"]
async fn test_get_all_forex_quotes() {
let client = FmpClient::new().unwrap();
let result = client.quote().get_all_forex_quotes().await;
assert!(result.is_ok());
let quotes = result.unwrap();
assert!(!quotes.is_empty());
assert!(
quotes
.iter()
.any(|q| q.symbol.len() >= 6 && q.symbol.len() <= 8)
);
}
#[tokio::test]
#[ignore = "requires FMP API key"]
async fn test_get_quote_invalid_symbol() {
let client = FmpClient::new().unwrap();
let result = client.quote().get_quote("INVALID_SYMBOL_12345").await;
match result {
Ok(quotes) => assert!(quotes.is_empty() || quotes[0].symbol == "INVALID_SYMBOL_12345"),
Err(_) => {} }
}
#[tokio::test]
#[ignore = "requires FMP API key"]
async fn test_get_quote_batch_mixed_valid_invalid() {
let client = FmpClient::new().unwrap();
let result = client
.quote()
.get_quote_batch(&["AAPL", "INVALID_SYM", "MSFT"])
.await;
assert!(result.is_ok());
let quotes = result.unwrap();
assert!(quotes.len() >= 2);
assert!(
quotes
.iter()
.any(|q| q.symbol == "AAPL" || q.symbol == "MSFT")
);
}
#[tokio::test]
#[ignore = "requires FMP API key"]
async fn test_get_quote_batch_empty_array() {
let client = FmpClient::new().unwrap();
let result = client.quote().get_quote_batch(&[]).await;
match result {
Ok(quotes) => assert!(quotes.is_empty()),
Err(_) => {} }
}
#[tokio::test]
#[ignore = "requires FMP API key"]
async fn test_get_exchange_quotes_invalid_exchange() {
let client = FmpClient::new().unwrap();
let result = client.quote().get_exchange_quotes("INVALID_EXCHANGE").await;
match result {
Ok(quotes) => assert!(quotes.is_empty()),
Err(_) => {} }
}
#[tokio::test]
#[ignore = "requires FMP API key"]
async fn test_get_quote_special_characters() {
let client = FmpClient::new().unwrap();
let result = client.quote().get_quote("BRK.A").await;
assert!(result.is_ok());
let quotes = result.unwrap();
if !quotes.is_empty() {
assert_eq!(quotes[0].symbol, "BRK.A");
}
}
}