1use std::{
2 fmt,
3 time::{Duration, Instant},
4};
5
6use reqwest::header::HeaderValue;
7use serde::{Serialize, de::DeserializeOwned};
8use tracing::{debug, info, warn};
9
10use crate::{
11 ClientConfig, CreateTaskResponse, Error, PollingConfig, Result, Solution, Solved, TaskResult,
12 TaskStatus, TaskTypeName,
13 request::{CreateTaskRequest, QueryBalanceRequest, QueryTaskResultRequest, TaskPayload},
14 response::BalanceResponse,
15 transport::{
16 CLIENT_KEY_HEADER, client_key_header, endpoint, log_request, log_response,
17 parse_api_response,
18 },
19};
20
21#[derive(Clone)]
23pub struct AsyncEzCapSolverClient {
24 client_key: String,
25 client_key_header: HeaderValue,
26 config: ClientConfig,
27 http: reqwest::Client,
28}
29
30impl fmt::Debug for AsyncEzCapSolverClient {
31 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
32 formatter
33 .debug_struct("AsyncEzCapSolverClient")
34 .field("client_key", &"[REDACTED]")
35 .field("config", &self.config)
36 .finish_non_exhaustive()
37 }
38}
39
40impl AsyncEzCapSolverClient {
41 pub fn new() -> Result<Self> {
45 Self::with_config(ClientConfig::default())
46 }
47
48 pub fn with_config(config: ClientConfig) -> Result<Self> {
53 let client_key = config.resolve_client_key()?;
54 let client_key_header = client_key_header(&client_key)?;
55 config.validate()?;
56 let http = build_http_client(&config)?;
57 Ok(Self {
58 client_key,
59 client_key_header,
60 config,
61 http,
62 })
63 }
64
65 pub fn with_client_key(client_key: impl Into<String>) -> Result<Self> {
67 Self::with_config(ClientConfig::default().with_client_key(client_key))
68 }
69
70 pub fn with_http_client(config: ClientConfig, http: reqwest::Client) -> Result<Self> {
77 let client_key = config.resolve_client_key()?;
78 let client_key_header = client_key_header(&client_key)?;
79 config.validate()?;
80 Ok(Self {
81 client_key,
82 client_key_header,
83 config,
84 http,
85 })
86 }
87
88 #[must_use]
90 pub const fn config(&self) -> &ClientConfig {
91 &self.config
92 }
93
94 pub async fn create_task<N, T>(&self, task_type: N, task: &T) -> Result<CreateTaskResponse>
101 where
102 N: TaskTypeName,
103 T: Serialize + ?Sized,
104 {
105 let task_type = task_type.task_type_name();
106 debug!(task_type, "creating task");
107 let request = CreateTaskRequest {
108 client_key: &self.client_key,
109 app_id: self.config.app_id,
110 task: TaskPayload {
111 task_type,
112 params: task,
113 },
114 };
115 let response: CreateTaskResponse = self
116 .post(
117 &endpoint(&self.config.async_base_url, "/createTask"),
118 &request,
119 self.config.timeout,
120 )
121 .await?;
122 if response.task_id.trim().is_empty() {
123 return Err(Error::UnexpectedResponse(
124 "createTask returned an empty taskId".to_owned(),
125 ));
126 }
127 info!(task_id = %response.task_id, task_type, "task created");
128 Ok(response)
129 }
130
131 pub async fn get_task_result(&self, task_id: &str) -> Result<TaskResult> {
136 let request = QueryTaskResultRequest {
137 client_key: &self.client_key,
138 task_id,
139 };
140 self.post(
141 &endpoint(&self.config.async_base_url, "/getTaskResult"),
142 &request,
143 self.config.timeout,
144 )
145 .await
146 }
147
148 pub async fn wait_for_result(&self, task_id: &str) -> Result<TaskResult> {
150 self.wait_for_result_with(task_id, self.config.polling)
151 .await
152 }
153
154 pub async fn wait_for_result_with(
156 &self,
157 task_id: &str,
158 polling: PollingConfig,
159 ) -> Result<TaskResult> {
160 polling.validate()?;
161 for attempt in 1..=polling.max_attempts {
162 tokio::time::sleep(polling.interval).await;
165 debug!(task_id = %task_id, attempt, "polling task result");
166 let result = match self.get_task_result(task_id).await {
167 Ok(result) => result,
168 Err(Error::EzError(error)) if error.is_rate_limited() => {
174 warn!(
175 task_id = %task_id,
176 attempt,
177 error_code = error.error_code.as_deref().unwrap_or_default(),
178 "polling throttled, retrying"
179 );
180 continue;
181 }
182 Err(error) => return Err(error),
183 };
184 match &result.status {
185 TaskStatus::Ready => {
186 info!(
187 task_id = %task_id,
188 attempts = attempt,
189 "task completed"
190 );
191 return Ok(result);
192 }
193 TaskStatus::Processing => {}
194 TaskStatus::Error => {
195 return Err(Error::UnexpectedResponse(format!(
196 "task `{task_id}` returned status `error` without an API error"
197 )));
198 }
199 }
200 }
201 Err(Error::PollingExhausted {
202 task_id: task_id.to_owned(),
203 attempts: polling.max_attempts,
204 interval: polling.interval,
205 })
206 }
207
208 pub async fn solve<N, T>(&self, task_type: N, task: &T) -> Result<Solved<Solution>>
214 where
215 N: TaskTypeName,
216 T: Serialize + ?Sized,
217 {
218 self.solve_with(task_type, task, self.config.polling).await
219 }
220
221 pub async fn solve_with<N, T>(
223 &self,
224 task_type: N,
225 task: &T,
226 polling: PollingConfig,
227 ) -> Result<Solved<Solution>>
228 where
229 N: TaskTypeName,
230 T: Serialize + ?Sized,
231 {
232 let created = self.create_task(task_type, task).await?;
233 let task_id = created.task_id;
234 let request_id = created.meta.request_id;
235 let result = self
236 .wait_for_result_with(&task_id, polling)
237 .await
238 .map_err(|error| error.with_task_context(task_id.clone(), request_id.clone()))?;
239 Ok(Solved {
240 task_id: Some(task_id),
241 request_id: result.meta.request_id.or(request_id),
245 solution: result.solution.clone(),
248 raw: result.solution,
249 })
250 }
251
252 pub async fn sync_solve<N, T>(&self, task_type: N, task: &T) -> Result<Solved<Solution>>
261 where
262 N: TaskTypeName,
263 T: Serialize + ?Sized,
264 {
265 let result = self.create_sync_task(task_type, task).await?;
266 if result.status == TaskStatus::Error {
267 return Err(Error::UnexpectedResponse(
268 "task result returned status `error` without an API error".to_owned(),
269 ));
270 }
271 Ok(Solved {
272 task_id: result.task_id,
273 request_id: result.meta.request_id,
274 solution: result.solution.clone(),
275 raw: result.solution,
276 })
277 }
278
279 pub async fn create_sync_task<N, T>(&self, task_type: N, task: &T) -> Result<TaskResult>
284 where
285 N: TaskTypeName,
286 T: Serialize + ?Sized,
287 {
288 let task_type = task_type.task_type_name();
289 debug!(task_type, "creating sync task");
290 let request = CreateTaskRequest {
291 client_key: &self.client_key,
292 app_id: self.config.app_id,
293 task: TaskPayload {
294 task_type,
295 params: task,
296 },
297 };
298 let result: TaskResult = self
299 .post(
300 &endpoint(&self.config.sync_base_url, "/createSyncTask"),
301 &request,
302 self.config.sync_timeout,
303 )
304 .await?;
305 if result.status == TaskStatus::Processing {
306 return Err(Error::UnexpectedResponse(
307 "createSyncTask returned status `processing`".to_owned(),
308 ));
309 }
310 info!(task_type, "sync task completed");
311 Ok(result)
312 }
313
314 pub async fn balance(&self) -> Result<f64> {
316 debug!("querying account balance");
317 let request = QueryBalanceRequest {
318 client_key: &self.client_key,
319 };
320 let response: BalanceResponse = self
321 .post(
322 &endpoint(&self.config.async_base_url, "/getBalance"),
323 &request,
324 self.config.timeout,
325 )
326 .await?;
327 Ok(response.balance)
328 }
329
330 async fn post<B, T>(&self, url: &str, body: &B, timeout: Duration) -> Result<T>
331 where
332 B: Serialize + ?Sized,
333 T: DeserializeOwned,
334 {
335 log_request(url, body);
336 let started = Instant::now();
337 let response = self
338 .http
339 .post(url)
340 .timeout(timeout)
341 .header(CLIENT_KEY_HEADER, self.client_key_header.clone())
342 .json(body)
343 .send()
344 .await?;
345 let status = response.status().as_u16();
346 let bytes = response.bytes().await?;
347 log_response(url, status, started.elapsed(), &bytes);
348 parse_api_response(status, &bytes)
349 }
350}
351
352fn build_http_client(config: &ClientConfig) -> Result<reqwest::Client> {
353 let mut builder = reqwest::Client::builder()
354 .user_agent(config.user_agent.clone());
357 if let Some(proxy) = &config.proxy {
358 let proxy = reqwest::Proxy::all(proxy)
359 .map_err(|error| Error::config(format!("invalid SDK proxy URL: {error}")))?;
360 builder = builder.proxy(proxy);
361 }
362 Ok(builder.build()?)
363}
364
365mod solvers;
366
367#[cfg(test)]
368mod tests;