fmp_rs/endpoints/
crypto.rs

1//! Cryptocurrency endpoints
2
3use crate::{
4    client::FmpClient,
5    error::Result,
6    models::crypto::{CryptoHistorical, CryptoIntraday, CryptoQuote, CryptoSymbol},
7};
8use serde::{Deserialize, Serialize};
9
10/// Crypto API endpoints
11pub struct Crypto {
12    client: FmpClient,
13}
14
15impl Crypto {
16    pub(crate) fn new(client: FmpClient) -> Self {
17        Self { client }
18    }
19
20    /// Get list of available cryptocurrencies
21    ///
22    /// Returns all available cryptocurrency symbols and their information.
23    ///
24    /// # Example
25    /// ```no_run
26    /// # use fmp_rs::FmpClient;
27    /// # #[tokio::main]
28    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
29    /// let client = FmpClient::new()?;
30    /// let cryptos = client.crypto().get_crypto_list().await?;
31    /// for crypto in cryptos.iter().take(10) {
32    ///     println!("{}: {}", crypto.symbol, crypto.name.as_deref().unwrap_or("N/A"));
33    /// }
34    /// # Ok(())
35    /// # }
36    /// ```
37    pub async fn get_crypto_list(&self) -> Result<Vec<CryptoSymbol>> {
38        #[derive(Serialize)]
39        struct Query<'a> {
40            apikey: &'a str,
41        }
42
43        let url = self.client.build_url("/symbol/available-cryptocurrencies");
44        self.client
45            .get_with_query(
46                &url,
47                &Query {
48                    apikey: self.client.api_key(),
49                },
50            )
51            .await
52    }
53
54    /// Get real-time cryptocurrency quote
55    ///
56    /// Returns current price and market data for a cryptocurrency.
57    ///
58    /// # Arguments
59    /// * `symbol` - Crypto symbol (e.g., "BTCUSD", "ETHUSD")
60    ///
61    /// # Example
62    /// ```no_run
63    /// # use fmp_rs::FmpClient;
64    /// # #[tokio::main]
65    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
66    /// let client = FmpClient::new()?;
67    /// let quote = client.crypto().get_crypto_quote("BTCUSD").await?;
68    /// if let Some(q) = quote.first() {
69    ///     println!("BTC Price: ${:.2}", q.price.unwrap_or(0.0));
70    ///     println!("24h Change: {:+.2}%", q.changes_percentage.unwrap_or(0.0));
71    /// }
72    /// # Ok(())
73    /// # }
74    /// ```
75    pub async fn get_crypto_quote(&self, symbol: &str) -> Result<Vec<CryptoQuote>> {
76        #[derive(Serialize)]
77        struct Query<'a> {
78            apikey: &'a str,
79        }
80
81        let url = self.client.build_url(&format!("/quote/{}", symbol));
82        self.client
83            .get_with_query(
84                &url,
85                &Query {
86                    apikey: self.client.api_key(),
87                },
88            )
89            .await
90    }
91
92    /// Get historical cryptocurrency prices
93    ///
94    /// Returns daily historical price data for a cryptocurrency.
95    ///
96    /// # Arguments
97    /// * `symbol` - Crypto symbol (e.g., "BTCUSD")
98    /// * `from` - Start date (optional, format: YYYY-MM-DD)
99    /// * `to` - End date (optional, format: YYYY-MM-DD)
100    ///
101    /// # Example
102    /// ```no_run
103    /// # use fmp_rs::FmpClient;
104    /// # #[tokio::main]
105    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
106    /// let client = FmpClient::new()?;
107    /// let history = client.crypto().get_crypto_historical("BTCUSD", None, None).await?;
108    /// for day in history.iter().take(5) {
109    ///     println!("{}: ${:.2} (Vol: {})", day.date, day.close, day.volume);
110    /// }
111    /// # Ok(())
112    /// # }
113    /// ```
114    pub async fn get_crypto_historical(
115        &self,
116        symbol: &str,
117        from: Option<&str>,
118        to: Option<&str>,
119    ) -> Result<Vec<CryptoHistorical>> {
120        #[derive(Serialize)]
121        struct Query<'a> {
122            #[serde(skip_serializing_if = "Option::is_none")]
123            from: Option<&'a str>,
124            #[serde(skip_serializing_if = "Option::is_none")]
125            to: Option<&'a str>,
126            apikey: &'a str,
127        }
128
129        let url = self
130            .client
131            .build_url(&format!("/historical-price-full/{}", symbol));
132
133        #[derive(Deserialize)]
134        struct Response {
135            historical: Vec<CryptoHistorical>,
136        }
137
138        let response: Response = self
139            .client
140            .get_with_query(
141                &url,
142                &Query {
143                    from,
144                    to,
145                    apikey: self.client.api_key(),
146                },
147            )
148            .await?;
149
150        Ok(response.historical)
151    }
152
153    /// Get intraday cryptocurrency prices
154    ///
155    /// Returns intraday price data at various intervals (1min, 5min, 15min, 30min, 1hour, 4hour).
156    ///
157    /// # Arguments
158    /// * `symbol` - Crypto symbol (e.g., "BTCUSD")
159    /// * `interval` - Time interval ("1min", "5min", "15min", "30min", "1hour", "4hour")
160    ///
161    /// # Example
162    /// ```no_run
163    /// # use fmp_rs::FmpClient;
164    /// # #[tokio::main]
165    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
166    /// let client = FmpClient::new()?;
167    /// let intraday = client.crypto().get_crypto_intraday("BTCUSD", "5min").await?;
168    /// for tick in intraday.iter().take(10) {
169    ///     println!("{}: ${:.2}", tick.date, tick.close);
170    /// }
171    /// # Ok(())
172    /// # }
173    /// ```
174    pub async fn get_crypto_intraday(
175        &self,
176        symbol: &str,
177        interval: &str,
178    ) -> Result<Vec<CryptoIntraday>> {
179        #[derive(Serialize)]
180        struct Query<'a> {
181            apikey: &'a str,
182        }
183
184        let url = self
185            .client
186            .build_url(&format!("/historical-chart/{}/{}", interval, symbol));
187        self.client
188            .get_with_query(
189                &url,
190                &Query {
191                    apikey: self.client.api_key(),
192                },
193            )
194            .await
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    // Golden path tests
203    #[tokio::test]
204    #[ignore = "requires FMP API key"]
205    async fn test_get_crypto_list() {
206        let client = FmpClient::new().unwrap();
207        let result = client.crypto().get_crypto_list().await;
208        assert!(result.is_ok());
209        let cryptos = result.unwrap();
210        assert!(!cryptos.is_empty());
211    }
212
213    #[tokio::test]
214    #[ignore = "requires FMP API key"]
215    async fn test_get_crypto_quote() {
216        let client = FmpClient::new().unwrap();
217        let result = client.crypto().get_crypto_quote("BTCUSD").await;
218        assert!(result.is_ok());
219        let quotes = result.unwrap();
220        assert!(!quotes.is_empty());
221        assert!(quotes[0].price.is_some());
222    }
223
224    #[tokio::test]
225    #[ignore = "requires FMP API key"]
226    async fn test_get_crypto_historical() {
227        let client = FmpClient::new().unwrap();
228        let result = client
229            .crypto()
230            .get_crypto_historical("BTCUSD", None, None)
231            .await;
232        assert!(result.is_ok());
233        let history = result.unwrap();
234        assert!(!history.is_empty());
235    }
236
237    #[tokio::test]
238    #[ignore = "requires FMP API key"]
239    async fn test_get_crypto_intraday() {
240        let client = FmpClient::new().unwrap();
241        let result = client.crypto().get_crypto_intraday("BTCUSD", "5min").await;
242        assert!(result.is_ok());
243        let intraday = result.unwrap();
244        assert!(!intraday.is_empty());
245    }
246
247    // Edge case tests
248    #[tokio::test]
249    #[ignore = "requires FMP API key"]
250    async fn test_crypto_historical_with_dates() {
251        let client = FmpClient::new().unwrap();
252        let result = client
253            .crypto()
254            .get_crypto_historical("BTCUSD", Some("2024-01-01"), Some("2024-01-31"))
255            .await;
256        assert!(result.is_ok());
257    }
258
259    #[tokio::test]
260    #[ignore = "requires FMP API key"]
261    async fn test_various_crypto_quotes() {
262        let client = FmpClient::new().unwrap();
263        for symbol in &["BTCUSD", "ETHUSD", "ADAUSD"] {
264            let result = client.crypto().get_crypto_quote(symbol).await;
265            assert!(result.is_ok());
266        }
267    }
268
269    #[tokio::test]
270    #[ignore = "requires FMP API key"]
271    async fn test_various_intervals() {
272        let client = FmpClient::new().unwrap();
273        for interval in &["1min", "5min", "15min", "1hour"] {
274            let result = client
275                .crypto()
276                .get_crypto_intraday("BTCUSD", interval)
277                .await;
278            assert!(result.is_ok());
279        }
280    }
281
282    // Error handling tests
283    #[tokio::test]
284    async fn test_invalid_api_key() {
285        let client = FmpClient::builder()
286            .api_key("invalid_key_12345")
287            .build()
288            .unwrap();
289        let result = client.crypto().get_crypto_list().await;
290        assert!(result.is_err());
291    }
292
293    #[tokio::test]
294    #[ignore = "requires FMP API key"]
295    async fn test_invalid_symbol() {
296        let client = FmpClient::new().unwrap();
297        let result = client.crypto().get_crypto_quote("INVALIDCRYPTO123").await;
298        // Should return empty or error
299        match result {
300            Ok(data) => assert!(data.is_empty()),
301            Err(_) => {} // Error is acceptable for invalid crypto
302        }
303    }
304}