1use crate::cli::crawl::{self, Period};
4use crate::web::AppState;
5use axum::Json;
6use axum::extract::State;
7use axum::http::StatusCode;
8use axum::response::IntoResponse;
9use serde::Deserialize;
10use std::sync::Arc;
11
12#[derive(Debug, Deserialize)]
14pub struct CrawlRequest {
15 pub token: String,
17 #[serde(default = "default_chain")]
19 pub chain: String,
20 #[serde(default)]
22 pub period: Option<String>,
23 #[serde(default = "default_holders_limit")]
25 pub holders_limit: u32,
26}
27
28fn default_chain() -> String {
29 "ethereum".to_string()
30}
31
32fn default_holders_limit() -> u32 {
33 10
34}
35
36pub async fn handle(
38 State(state): State<Arc<AppState>>,
39 Json(req): Json<CrawlRequest>,
40) -> impl IntoResponse {
41 let period = match req.period.as_deref() {
42 Some("1h") => Period::Hour1,
43 Some("7d") => Period::Day7,
44 Some("30d") => Period::Day30,
45 _ => Period::Hour24,
46 };
47
48 match crawl::fetch_analytics_for_input(
49 &req.token,
50 &req.chain,
51 period,
52 req.holders_limit,
53 &state.factory,
54 None,
55 )
56 .await
57 {
58 Ok(analytics) => Json(serde_json::json!(analytics)).into_response(),
59 Err(e) => (
60 StatusCode::INTERNAL_SERVER_ERROR,
61 Json(serde_json::json!({ "error": e.to_string() })),
62 )
63 .into_response(),
64 }
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70
71 #[test]
72 fn test_deserialize_full() {
73 let json = serde_json::json!({
74 "token": "USDC",
75 "chain": "polygon",
76 "period": "7d",
77 "holders_limit": 20
78 });
79 let req: CrawlRequest = serde_json::from_value(json).unwrap();
80 assert_eq!(req.token, "USDC");
81 assert_eq!(req.chain, "polygon");
82 assert_eq!(req.period, Some("7d".to_string()));
83 assert_eq!(req.holders_limit, 20);
84 }
85
86 #[test]
87 fn test_deserialize_minimal() {
88 let json = serde_json::json!({
89 "token": "ETH"
90 });
91 let req: CrawlRequest = serde_json::from_value(json).unwrap();
92 assert_eq!(req.token, "ETH");
93 assert_eq!(req.chain, "ethereum");
94 assert_eq!(req.period, None);
95 assert_eq!(req.holders_limit, 10);
96 }
97
98 #[test]
99 fn test_defaults() {
100 assert_eq!(default_chain(), "ethereum");
101 assert_eq!(default_holders_limit(), 10);
102 }
103
104 #[test]
105 fn test_period_variations() {
106 let json_1h = serde_json::json!({
107 "token": "USDT",
108 "period": "1h"
109 });
110 let req_1h: CrawlRequest = serde_json::from_value(json_1h).unwrap();
111 assert_eq!(req_1h.period, Some("1h".to_string()));
112
113 let json_24h = serde_json::json!({
114 "token": "USDT",
115 "period": "24h"
116 });
117 let req_24h: CrawlRequest = serde_json::from_value(json_24h).unwrap();
118 assert_eq!(req_24h.period, Some("24h".to_string()));
119
120 let json_30d = serde_json::json!({
121 "token": "USDT",
122 "period": "30d"
123 });
124 let req_30d: CrawlRequest = serde_json::from_value(json_30d).unwrap();
125 assert_eq!(req_30d.period, Some("30d".to_string()));
126
127 let json_no_period = serde_json::json!({
128 "token": "USDT"
129 });
130 let req_no_period: CrawlRequest = serde_json::from_value(json_no_period).unwrap();
131 assert_eq!(req_no_period.period, None);
132 }
133
134 #[tokio::test]
135 async fn test_handle_crawl_direct() {
136 use crate::chains::DefaultClientFactory;
137 use crate::config::Config;
138 use crate::web::AppState;
139 use axum::extract::State;
140 use axum::response::IntoResponse;
141
142 let config = Config::default();
143 let factory = DefaultClientFactory {
144 chains_config: config.chains.clone(),
145 };
146 let state = std::sync::Arc::new(AppState { config, factory });
147 let req = CrawlRequest {
148 token: "USDC".to_string(),
149 chain: "ethereum".to_string(),
150 period: Some("24h".to_string()),
151 holders_limit: 5,
152 };
153 let response = handle(State(state), axum::Json(req)).await.into_response();
154 let status = response.status();
155 assert!(status.is_success() || status.is_client_error() || status.is_server_error());
156 }
157}