polyoxide_gamma/
client.rs1use polyoxide_core::{
2 HttpClient, HttpClientBuilder, RateLimiter, RetryConfig, DEFAULT_POOL_SIZE, DEFAULT_TIMEOUT_MS,
3};
4
5use crate::{
6 api::{
7 comments::Comments, events::Events, health::Health, markets::Markets, search::Search,
8 series::Series, sports::Sports, tags::Tags, user::User,
9 },
10 error::GammaError,
11};
12
13const DEFAULT_BASE_URL: &str = "https://gamma-api.polymarket.com";
14
15#[derive(Clone)]
17pub struct Gamma {
18 pub(crate) http_client: HttpClient,
19}
20
21impl Gamma {
22 pub fn new() -> Result<Self, GammaError> {
24 Self::builder().build()
25 }
26
27 pub fn builder() -> GammaBuilder {
29 GammaBuilder::new()
30 }
31
32 pub fn markets(&self) -> Markets {
34 Markets {
35 http_client: self.http_client.clone(),
36 }
37 }
38
39 pub fn events(&self) -> Events {
41 Events {
42 http_client: self.http_client.clone(),
43 }
44 }
45
46 pub fn series(&self) -> Series {
48 Series {
49 http_client: self.http_client.clone(),
50 }
51 }
52
53 pub fn tags(&self) -> Tags {
55 Tags {
56 http_client: self.http_client.clone(),
57 }
58 }
59
60 pub fn sports(&self) -> Sports {
62 Sports {
63 http_client: self.http_client.clone(),
64 }
65 }
66
67 pub fn comments(&self) -> Comments {
69 Comments {
70 http_client: self.http_client.clone(),
71 }
72 }
73
74 pub fn search(&self) -> Search {
76 Search {
77 http_client: self.http_client.clone(),
78 }
79 }
80
81 pub fn user(&self) -> User {
83 User {
84 http_client: self.http_client.clone(),
85 }
86 }
87
88 pub fn health(&self) -> Health {
90 Health {
91 http_client: self.http_client.clone(),
92 }
93 }
94}
95
96pub struct GammaBuilder {
98 base_url: String,
99 timeout_ms: u64,
100 pool_size: usize,
101 retry_config: Option<RetryConfig>,
102 max_concurrent: Option<usize>,
103}
104
105impl GammaBuilder {
106 fn new() -> Self {
107 Self {
108 base_url: DEFAULT_BASE_URL.to_string(),
109 timeout_ms: DEFAULT_TIMEOUT_MS,
110 pool_size: DEFAULT_POOL_SIZE,
111 retry_config: None,
112 max_concurrent: None,
113 }
114 }
115
116 pub fn base_url(mut self, url: impl Into<String>) -> Self {
118 self.base_url = url.into();
119 self
120 }
121
122 pub fn timeout_ms(mut self, timeout: u64) -> Self {
124 self.timeout_ms = timeout;
125 self
126 }
127
128 pub fn pool_size(mut self, size: usize) -> Self {
130 self.pool_size = size;
131 self
132 }
133
134 pub fn with_retry_config(mut self, config: RetryConfig) -> Self {
136 self.retry_config = Some(config);
137 self
138 }
139
140 pub fn max_concurrent(mut self, max: usize) -> Self {
144 self.max_concurrent = Some(max);
145 self
146 }
147
148 pub fn build(self) -> Result<Gamma, GammaError> {
150 let mut builder = HttpClientBuilder::new(&self.base_url)
151 .timeout_ms(self.timeout_ms)
152 .pool_size(self.pool_size)
153 .with_rate_limiter(RateLimiter::gamma_default())
154 .with_max_concurrent(self.max_concurrent.unwrap_or(4));
155 if let Some(config) = self.retry_config {
156 builder = builder.with_retry_config(config);
157 }
158 let http_client = builder.build()?;
159
160 Ok(Gamma { http_client })
161 }
162}
163
164impl Default for GammaBuilder {
165 fn default() -> Self {
166 Self::new()
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn test_builder_default() {
176 let builder = GammaBuilder::default();
177 assert_eq!(builder.base_url, DEFAULT_BASE_URL);
178 assert_eq!(builder.timeout_ms, DEFAULT_TIMEOUT_MS);
179 assert_eq!(builder.pool_size, DEFAULT_POOL_SIZE);
180 }
181
182 #[test]
183 fn test_builder_custom_url() {
184 let builder = GammaBuilder::new().base_url("https://custom.api.com");
185 assert_eq!(builder.base_url, "https://custom.api.com");
186 }
187
188 #[test]
189 fn test_builder_custom_timeout() {
190 let builder = GammaBuilder::new().timeout_ms(60_000);
191 assert_eq!(builder.timeout_ms, 60_000);
192 }
193
194 #[test]
195 fn test_builder_custom_pool_size() {
196 let builder = GammaBuilder::new().pool_size(20);
197 assert_eq!(builder.pool_size, 20);
198 }
199
200 #[test]
201 fn test_builder_custom_retry_config() {
202 let config = RetryConfig {
203 max_retries: 5,
204 initial_backoff_ms: 1000,
205 max_backoff_ms: 30_000,
206 };
207 let builder = GammaBuilder::new().with_retry_config(config);
208 let config = builder.retry_config.unwrap();
209 assert_eq!(config.max_retries, 5);
210 assert_eq!(config.initial_backoff_ms, 1000);
211 }
212
213 #[test]
214 fn test_builder_build_success() {
215 let gamma = Gamma::builder().build();
216 assert!(gamma.is_ok());
217 }
218
219 #[test]
220 fn test_builder_invalid_url() {
221 let result = Gamma::builder().base_url("://bad").build();
222 assert!(result.is_err());
223 }
224
225 #[test]
226 fn test_builder_custom_max_concurrent() {
227 let builder = GammaBuilder::new().max_concurrent(10);
228 assert_eq!(builder.max_concurrent, Some(10));
229 }
230
231 #[tokio::test]
232 async fn test_default_concurrency_limit_is_4() {
233 let gamma = Gamma::new().unwrap();
234 let mut permits = Vec::new();
236 for _ in 0..4 {
237 permits.push(gamma.http_client.acquire_concurrency().await);
238 }
239 assert!(permits.iter().all(|p| p.is_some()));
240
241 let result = tokio::time::timeout(
243 std::time::Duration::from_millis(50),
244 gamma.http_client.acquire_concurrency(),
245 )
246 .await;
247 assert!(
248 result.is_err(),
249 "5th permit should block with default limit of 4"
250 );
251 }
252
253 #[test]
254 fn test_new_creates_client() {
255 let gamma = Gamma::new();
256 assert!(gamma.is_ok());
257 }
258
259 #[test]
260 fn test_client_namespaces_accessible() {
261 let gamma = Gamma::new().unwrap();
262 let _markets = gamma.markets();
263 let _events = gamma.events();
264 let _series = gamma.series();
265 let _tags = gamma.tags();
266 let _sports = gamma.sports();
267 let _comments = gamma.comments();
268 let _search = gamma.search();
269 let _user = gamma.user();
270 let _health = gamma.health();
271 }
272}