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 )
55 .await
56 {
57 Ok(analytics) => Json(serde_json::json!(analytics)).into_response(),
58 Err(e) => (
59 StatusCode::INTERNAL_SERVER_ERROR,
60 Json(serde_json::json!({ "error": e.to_string() })),
61 )
62 .into_response(),
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 #[test]
71 fn test_deserialize_full() {
72 let json = serde_json::json!({
73 "token": "USDC",
74 "chain": "polygon",
75 "period": "7d",
76 "holders_limit": 20
77 });
78 let req: CrawlRequest = serde_json::from_value(json).unwrap();
79 assert_eq!(req.token, "USDC");
80 assert_eq!(req.chain, "polygon");
81 assert_eq!(req.period, Some("7d".to_string()));
82 assert_eq!(req.holders_limit, 20);
83 }
84
85 #[test]
86 fn test_deserialize_minimal() {
87 let json = serde_json::json!({
88 "token": "ETH"
89 });
90 let req: CrawlRequest = serde_json::from_value(json).unwrap();
91 assert_eq!(req.token, "ETH");
92 assert_eq!(req.chain, "ethereum");
93 assert_eq!(req.period, None);
94 assert_eq!(req.holders_limit, 10);
95 }
96
97 #[test]
98 fn test_defaults() {
99 assert_eq!(default_chain(), "ethereum");
100 assert_eq!(default_holders_limit(), 10);
101 }
102
103 #[test]
104 fn test_period_variations() {
105 let json_1h = serde_json::json!({
106 "token": "USDT",
107 "period": "1h"
108 });
109 let req_1h: CrawlRequest = serde_json::from_value(json_1h).unwrap();
110 assert_eq!(req_1h.period, Some("1h".to_string()));
111
112 let json_24h = serde_json::json!({
113 "token": "USDT",
114 "period": "24h"
115 });
116 let req_24h: CrawlRequest = serde_json::from_value(json_24h).unwrap();
117 assert_eq!(req_24h.period, Some("24h".to_string()));
118
119 let json_30d = serde_json::json!({
120 "token": "USDT",
121 "period": "30d"
122 });
123 let req_30d: CrawlRequest = serde_json::from_value(json_30d).unwrap();
124 assert_eq!(req_30d.period, Some("30d".to_string()));
125
126 let json_no_period = serde_json::json!({
127 "token": "USDT"
128 });
129 let req_no_period: CrawlRequest = serde_json::from_value(json_no_period).unwrap();
130 assert_eq!(req_no_period.period, None);
131 }
132
133 #[tokio::test]
134 async fn test_handle_crawl_direct() {
135 use crate::chains::DefaultClientFactory;
136 use crate::config::Config;
137 use crate::web::AppState;
138 use axum::extract::State;
139 use axum::response::IntoResponse;
140
141 let config = Config::default();
142 let factory = DefaultClientFactory {
143 chains_config: config.chains.clone(),
144 };
145 let state = std::sync::Arc::new(AppState { config, factory });
146 let req = CrawlRequest {
147 token: "USDC".to_string(),
148 chain: "ethereum".to_string(),
149 period: Some("24h".to_string()),
150 holders_limit: 5,
151 };
152 let response = handle(State(state), axum::Json(req)).await.into_response();
153 let status = response.status();
154 assert!(status.is_success() || status.is_client_error() || status.is_server_error());
155 }
156}