finance_query/adapters/coingecko/
mod.rs1pub(crate) mod chart; mod client;
29pub(crate) mod discovery; mod models;
31
32use client::CoinGeckoClient;
33use std::sync::OnceLock;
34
35use crate::error::Result;
36pub use crate::models::crypto::CoinQuote;
37pub use discovery::fetch_symbol_search_response;
38
39static COINGECKO_CLIENT: OnceLock<CoinGeckoClient> = OnceLock::new();
41
42fn client() -> Result<&'static CoinGeckoClient> {
43 if COINGECKO_CLIENT.get().is_none() {
44 let _ = COINGECKO_CLIENT.set(CoinGeckoClient::new()?);
45 }
46 Ok(COINGECKO_CLIENT.get().expect("just set above"))
47}
48
49pub async fn coins(vs_currency: &str, count: usize) -> Result<Vec<CoinQuote>> {
60 client()?.coins(vs_currency, count).await
61}
62
63pub async fn coin(id: &str, vs_currency: &str) -> Result<CoinQuote> {
72 client()?.coin(id, vs_currency).await
73}
74
75pub async fn fetch_crypto_quote_response(
81 id: &str,
82 vs_currency: &str,
83) -> Result<crate::models::crypto::CryptoQuote> {
84 let quote = coin(id, vs_currency).await?;
85 Ok(crate::models::crypto::CryptoQuote {
86 id: quote.id,
87 symbol: quote.symbol,
88 name: quote.name,
89 price: quote.current_price,
90 market_cap: quote.market_cap,
91 volume_24h: quote.total_volume,
92 change_24h: None,
93 change_percent_24h: quote.price_change_percentage_24h,
94 high_24h: None,
95 low_24h: None,
96 circulating_supply: quote.circulating_supply,
97 })
98}
99
100fn to_trending_coin(w: models::TrendingCoinWrapperDTO) -> crate::models::crypto::TrendingCoin {
102 crate::models::crypto::TrendingCoin {
103 id: Some(w.item.id),
104 symbol: Some(w.item.symbol.to_ascii_uppercase()),
105 name: Some(w.item.name),
106 market_cap_rank: w.item.market_cap_rank,
107 price_btc: w.item.price_btc,
108 score: w.item.score,
109 }
110}
111
112pub async fn fetch_crypto_trending_response() -> Result<Vec<crate::models::crypto::TrendingCoin>> {
114 let resp = client()?.trending().await?;
115 Ok(resp.coins.into_iter().map(to_trending_coin).collect())
116}
117
118fn to_global_crypto_stats(data: models::GlobalDataDTO) -> crate::models::crypto::GlobalCryptoStats {
120 crate::models::crypto::GlobalCryptoStats {
121 active_cryptocurrencies: data.active_cryptocurrencies,
122 markets: data.markets,
123 total_market_cap_usd: data.total_market_cap.get("usd").copied(),
124 total_volume_usd: data.total_volume.get("usd").copied(),
125 btc_dominance: data.market_cap_percentage.get("btc").copied(),
126 eth_dominance: data.market_cap_percentage.get("eth").copied(),
127 market_cap_change_percentage_24h_usd: data.market_cap_change_percentage_24h_usd,
128 }
129}
130
131pub async fn fetch_crypto_global_response() -> Result<crate::models::crypto::GlobalCryptoStats> {
133 let resp = client()?.global().await?;
134 Ok(to_global_crypto_stats(resp.data))
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn maps_trending_wrapper_and_uppercases_symbol() {
143 let dto: models::TrendingResponseDTO = serde_json::from_value(serde_json::json!({
144 "coins": [{
145 "item": {
146 "id": "bitcoin",
147 "name": "Bitcoin",
148 "symbol": "btc",
149 "market_cap_rank": 1,
150 "price_btc": 1.0,
151 "score": 0
152 }
153 }]
154 }))
155 .unwrap();
156
157 let coins: Vec<_> = dto.coins.into_iter().map(to_trending_coin).collect();
158 assert_eq!(coins.len(), 1);
159 assert_eq!(coins[0].id.as_deref(), Some("bitcoin"));
160 assert_eq!(coins[0].symbol.as_deref(), Some("BTC"));
161 assert_eq!(coins[0].market_cap_rank, Some(1));
162 assert_eq!(coins[0].score, Some(0));
163 }
164
165 #[test]
166 fn maps_global_stats_extracting_usd_and_dominance() {
167 let dto: models::GlobalResponseDTO = serde_json::from_value(serde_json::json!({
168 "data": {
169 "active_cryptocurrencies": 18137,
170 "markets": 1510,
171 "total_market_cap": { "usd": 3_000_000_000_000.0_f64, "btc": 30_000_000.0_f64 },
172 "total_volume": { "usd": 100_000_000_000.0_f64 },
173 "market_cap_percentage": { "btc": 45.2, "eth": 18.1 },
174 "market_cap_change_percentage_24h_usd": 0.64
175 }
176 }))
177 .unwrap();
178
179 let stats = to_global_crypto_stats(dto.data);
180 assert_eq!(stats.active_cryptocurrencies, Some(18137));
181 assert_eq!(stats.total_market_cap_usd, Some(3_000_000_000_000.0));
182 assert_eq!(stats.total_volume_usd, Some(100_000_000_000.0));
183 assert_eq!(stats.btc_dominance, Some(45.2));
184 assert_eq!(stats.eth_dominance, Some(18.1));
185 assert_eq!(stats.market_cap_change_percentage_24h_usd, Some(0.64));
186 }
187
188 #[test]
189 fn global_stats_missing_currency_key_yields_none() {
190 let dto: models::GlobalResponseDTO = serde_json::from_value(serde_json::json!({
191 "data": {
192 "active_cryptocurrencies": null,
193 "markets": null,
194 "total_market_cap": {},
195 "total_volume": {},
196 "market_cap_percentage": {},
197 "market_cap_change_percentage_24h_usd": null
198 }
199 }))
200 .unwrap();
201
202 let stats = to_global_crypto_stats(dto.data);
203 assert_eq!(stats.total_market_cap_usd, None);
204 assert_eq!(stats.btc_dominance, None);
205 }
206}