1use polyoxide_core::{HttpClient, QueryBuilder, Request};
2use serde::{Deserialize, Serialize};
3
4use crate::error::DataApiError;
5
6pub use crate::types::TimePeriod;
8
9#[derive(Clone)]
11pub struct BuildersApi {
12 pub(crate) http_client: HttpClient,
13}
14
15impl BuildersApi {
16 pub fn leaderboard(&self) -> GetBuilderLeaderboard {
18 let request = Request::new(self.http_client.clone(), "/v1/builders/leaderboard");
19
20 GetBuilderLeaderboard { request }
21 }
22
23 pub fn volume(&self) -> GetBuilderVolume {
25 let request = Request::new(self.http_client.clone(), "/v1/builders/volume");
26
27 GetBuilderVolume { request }
28 }
29}
30
31pub struct GetBuilderLeaderboard {
33 request: Request<Vec<BuilderRanking>, DataApiError>,
34}
35
36impl GetBuilderLeaderboard {
37 pub fn time_period(mut self, period: TimePeriod) -> Self {
39 self.request = self.request.query("timePeriod", period);
40 self
41 }
42
43 pub fn limit(mut self, limit: u32) -> Self {
45 self.request = self.request.query("limit", limit);
46 self
47 }
48
49 pub fn offset(mut self, offset: u32) -> Self {
51 self.request = self.request.query("offset", offset);
52 self
53 }
54
55 pub async fn send(self) -> Result<Vec<BuilderRanking>, DataApiError> {
57 self.request.send().await
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(rename_all(deserialize = "camelCase"))]
64pub struct BuilderRanking {
65 pub rank: String,
67 pub builder: String,
69 pub volume: f64,
71 pub active_users: u64,
73 pub verified: bool,
75 pub builder_logo: Option<String>,
77}
78
79pub struct GetBuilderVolume {
81 request: Request<Vec<BuilderVolume>, DataApiError>,
82}
83
84impl GetBuilderVolume {
85 pub fn time_period(mut self, period: TimePeriod) -> Self {
87 self.request = self.request.query("timePeriod", period);
88 self
89 }
90
91 pub async fn send(self) -> Result<Vec<BuilderVolume>, DataApiError> {
93 self.request.send().await
94 }
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99#[serde(rename_all(deserialize = "camelCase"))]
100pub struct BuilderVolume {
101 pub dt: String,
103 pub builder: String,
105 pub builder_logo: Option<String>,
107 pub verified: bool,
109 pub volume: f64,
111 pub active_users: u64,
113 pub rank: String,
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn time_period_display_matches_serde() {
123 let variants = [
124 TimePeriod::Day,
125 TimePeriod::Week,
126 TimePeriod::Month,
127 TimePeriod::All,
128 ];
129 for variant in variants {
130 let serialized = serde_json::to_value(variant).unwrap();
131 let display = variant.to_string();
132 assert_eq!(
133 format!("\"{}\"", display),
134 serialized.to_string(),
135 "Display mismatch for {:?}",
136 variant
137 );
138 }
139 }
140
141 #[test]
142 fn time_period_serde_roundtrip() {
143 for variant in [
144 TimePeriod::Day,
145 TimePeriod::Week,
146 TimePeriod::Month,
147 TimePeriod::All,
148 ] {
149 let json = serde_json::to_string(&variant).unwrap();
150 let deserialized: TimePeriod = serde_json::from_str(&json).unwrap();
151 assert_eq!(variant, deserialized);
152 }
153 }
154
155 #[test]
156 fn time_period_default_is_day() {
157 assert_eq!(TimePeriod::default(), TimePeriod::Day);
158 }
159
160 #[test]
161 fn time_period_specific_values() {
162 assert_eq!(TimePeriod::Day.to_string(), "DAY");
163 assert_eq!(TimePeriod::Week.to_string(), "WEEK");
164 assert_eq!(TimePeriod::Month.to_string(), "MONTH");
165 assert_eq!(TimePeriod::All.to_string(), "ALL");
166 }
167
168 #[test]
169 fn deserialize_builder_ranking() {
170 let json = r#"{
171 "rank": "1",
172 "builder": "polymarket-app",
173 "volume": 1500000.50,
174 "activeUsers": 25000,
175 "verified": true,
176 "builderLogo": "https://example.com/logo.png"
177 }"#;
178
179 let ranking: BuilderRanking = serde_json::from_str(json).unwrap();
180 assert_eq!(ranking.rank, "1");
181 assert_eq!(ranking.builder, "polymarket-app");
182 assert!((ranking.volume - 1500000.50).abs() < f64::EPSILON);
183 assert_eq!(ranking.active_users, 25000);
184 assert!(ranking.verified);
185 assert_eq!(
186 ranking.builder_logo,
187 Some("https://example.com/logo.png".to_string())
188 );
189 }
190
191 #[test]
192 fn deserialize_builder_ranking_null_logo() {
193 let json = r#"{
194 "rank": "5",
195 "builder": "unknown-builder",
196 "volume": 100.0,
197 "activeUsers": 10,
198 "verified": false,
199 "builderLogo": null
200 }"#;
201
202 let ranking: BuilderRanking = serde_json::from_str(json).unwrap();
203 assert_eq!(ranking.rank, "5");
204 assert!(!ranking.verified);
205 assert!(ranking.builder_logo.is_none());
206 }
207
208 #[test]
209 fn deserialize_builder_volume() {
210 let json = r#"{
211 "dt": "2025-01-15T00:00:00Z",
212 "builder": "top-builder",
213 "builderLogo": null,
214 "verified": true,
215 "volume": 500000.0,
216 "activeUsers": 1200,
217 "rank": "3"
218 }"#;
219
220 let vol: BuilderVolume = serde_json::from_str(json).unwrap();
221 assert_eq!(vol.dt, "2025-01-15T00:00:00Z");
222 assert_eq!(vol.builder, "top-builder");
223 assert!(vol.verified);
224 assert!((vol.volume - 500000.0).abs() < f64::EPSILON);
225 assert_eq!(vol.active_users, 1200);
226 assert_eq!(vol.rank, "3");
227 assert!(vol.builder_logo.is_none());
228 }
229
230 #[test]
231 fn deserialize_builder_ranking_list() {
232 let json = r#"[
233 {"rank": "1", "builder": "a", "volume": 100.0, "activeUsers": 5, "verified": true, "builderLogo": null},
234 {"rank": "2", "builder": "b", "volume": 50.0, "activeUsers": 3, "verified": false, "builderLogo": null}
235 ]"#;
236
237 let rankings: Vec<BuilderRanking> = serde_json::from_str(json).unwrap();
238 assert_eq!(rankings.len(), 2);
239 assert_eq!(rankings[0].rank, "1");
240 assert_eq!(rankings[1].rank, "2");
241 }
242}