Skip to main content

spider_client/
lib.rs

1//! The `spider-client` module provides the primary interface and
2//! functionalities for the Spider web crawler library, which is
3//! designed for rapid and efficient crawling of web pages to gather
4//! links using isolated contexts.
5//!
6//! ### Features
7//!
8//! - **Multi-threaded Crawling:** Spider can utilize multiple
9//!   threads to parallelize the crawling process, drastically
10//!   improving performance and allowing the ability to gather
11//!   millions of pages in a short time.
12//!
13//! - **Configurable:** The library provides various options to
14//!   configure the crawling behavior, such as setting the depth
15//!   of crawling, user-agent strings, delays between requests,
16//!   and more.
17//!
18//! - **Link Gathering:** One of the primary objectives of Spider is to
19//!   gather and manage links from the web pages it crawls,
20//!   compiling them into a structured format for further use.
21//!
22//! ### Examples
23//!
24//! Basic usage of the Spider client might look like this:
25//!
26//! ```rust
27//! use spider_client::{Spider, RequestType, RequestParams};
28//! use tokio;
29//!
30//!  # #[ignore]
31//! #[tokio::main]
32//! async fn main() {
33//!     let spider = Spider::new(Some("myspiderapikey".into())).expect("API key must be provided");
34//!
35//!     let url = "https://spider.cloud";
36//!
37//!     // Scrape a single URL
38//!     let scraped_data = spider.scrape_url(url, None, "application/json").await.expect("Failed to scrape the URL");
39//!
40//!     println!("Scraped Data: {:?}", scraped_data);
41//!
42//!     // Crawl a website
43//!     let crawler_params = RequestParams {
44//!         limit: Some(1),
45//!         proxy_enabled: Some(true),
46//!         metadata: Some(false),
47//!         request: Some(RequestType::Http),
48//!         ..Default::default()
49//!     };
50//!
51//!     let crawl_result = spider.crawl_url(url, Some(crawler_params), false, "application/json", None::<fn(serde_json::Value)>).await.expect("Failed to crawl the URL");
52//!
53//!     println!("Crawl Result: {:?}", crawl_result);
54//! }
55//! ```
56//!
57//! ### Modules
58//!
59//! - `config`: Contains the configuration options for the Spider client.
60//! - `utils`: Utility functions used by the Spider client.
61//!
62
63pub mod browser;
64pub mod shapes;
65
66use backon::ExponentialBuilder;
67use backon::Retryable;
68use reqwest::Client;
69use reqwest::{Error, Response};
70use serde::Serialize;
71pub use browser::*;
72pub use shapes::{request::*, response::*};
73use std::collections::HashMap;
74use std::sync::atomic::{AtomicU32, Ordering};
75use std::sync::OnceLock;
76use tokio_stream::StreamExt;
77
78/// Rate limit state from API response headers.
79/// Uses atomics so it can be updated from `&self`.
80#[derive(Debug, Default)]
81pub struct RateLimitInfo {
82    /// Maximum requests allowed per minute.
83    pub limit: AtomicU32,
84    /// Requests remaining in the current window.
85    pub remaining: AtomicU32,
86    /// Seconds until the rate limit window resets.
87    pub reset_seconds: AtomicU32,
88}
89
90impl RateLimitInfo {
91    /// Read the current rate limit snapshot.
92    pub fn snapshot(&self) -> (u32, u32, u32) {
93        (
94            self.limit.load(Ordering::Relaxed),
95            self.remaining.load(Ordering::Relaxed),
96            self.reset_seconds.load(Ordering::Relaxed),
97        )
98    }
99}
100
101static API_URL: OnceLock<String> = OnceLock::new();
102
103/// The API endpoint.
104pub fn get_api_url() -> &'static str {
105    API_URL.get_or_init(|| {
106        std::env::var("SPIDER_API_URL").unwrap_or_else(|_| "https://api.spider.cloud".to_string())
107    })
108}
109
110/// Represents a Spider with API key and HTTP client.
111#[derive(Debug, Default)]
112pub struct Spider {
113    /// The Spider API key.
114    pub api_key: String,
115    /// The Spider Client to re-use.
116    pub client: Client,
117    /// The latest rate limit info from API responses.
118    pub rate_limit: RateLimitInfo,
119}
120
121/// Handle the json response.
122pub async fn handle_json(res: reqwest::Response) -> Result<serde_json::Value, reqwest::Error> {
123    res.json().await
124}
125
126/// Handle the jsonl response.
127pub async fn handle_jsonl(res: reqwest::Response) -> Result<serde_json::Value, reqwest::Error> {
128    let text = res.text().await?;
129    let lines = text
130        .lines()
131        .filter_map(|line| serde_json::from_str::<serde_json::Value>(line).ok())
132        .collect::<Vec<_>>();
133    Ok(serde_json::Value::Array(lines))
134}
135
136/// Handle the CSV response.
137#[cfg(feature = "csv")]
138pub async fn handle_csv(res: reqwest::Response) -> Result<serde_json::Value, reqwest::Error> {
139    use std::collections::HashMap;
140    let text = res.text().await?;
141    let mut rdr = csv::Reader::from_reader(text.as_bytes());
142    let records: Vec<HashMap<String, String>> = rdr.deserialize().filter_map(Result::ok).collect();
143
144    if let Ok(record) = serde_json::to_value(records) {
145        Ok(record)
146    } else {
147        Ok(serde_json::Value::String(text))
148    }
149}
150
151#[cfg(not(feature = "csv"))]
152pub async fn handle_csv(res: reqwest::Response) -> Result<serde_json::Value, reqwest::Error> {
153    handle_text(res).await
154}
155
156/// Basic handle response to text
157pub async fn handle_text(res: reqwest::Response) -> Result<serde_json::Value, reqwest::Error> {
158    Ok(serde_json::Value::String(
159        res.text().await.unwrap_or_default(),
160    ))
161}
162
163/// Handle the XML response.
164#[cfg(feature = "csv")]
165pub async fn handle_xml(res: reqwest::Response) -> Result<serde_json::Value, reqwest::Error> {
166    let text = res.text().await?;
167    match quick_xml::de::from_str::<serde_json::Value>(&text) {
168        Ok(val) => Ok(val),
169        Err(_) => Ok(serde_json::Value::String(text)),
170    }
171}
172
173#[cfg(not(feature = "csv"))]
174/// Handle the XML response.
175pub async fn handle_xml(res: reqwest::Response) -> Result<serde_json::Value, reqwest::Error> {
176    handle_text(res).await
177}
178
179pub async fn parse_response(res: reqwest::Response) -> Result<serde_json::Value, reqwest::Error> {
180    let content_type = res
181        .headers()
182        .get(reqwest::header::CONTENT_TYPE)
183        .and_then(|v| v.to_str().ok())
184        .unwrap_or_default()
185        .to_ascii_lowercase();
186
187    if content_type.contains("json") && !content_type.contains("jsonl") {
188        handle_json(res).await
189    } else if content_type.contains("jsonl") || content_type.contains("ndjson") {
190        handle_jsonl(res).await
191    } else if content_type.contains("csv") {
192        handle_csv(res).await
193    } else if content_type.contains("xml") {
194        handle_xml(res).await
195    } else {
196        handle_text(res).await
197    }
198}
199
200impl Spider {
201    /// Creates a new instance of Spider.
202    ///
203    /// # Arguments
204    ///
205    /// * `api_key` - An optional API key. Defaults to using the 'SPIDER_API_KEY' env variable.
206    ///
207    /// # Returns
208    ///
209    /// A new instance of Spider or an error string if no API key is provided.
210    pub fn new(api_key: Option<String>) -> Result<Self, &'static str> {
211        let api_key = api_key.or_else(|| std::env::var("SPIDER_API_KEY").ok());
212
213        match api_key {
214            Some(key) => Ok(Self {
215                api_key: key,
216                client: Client::new(),
217                rate_limit: RateLimitInfo::default(),
218            }),
219            None => Err("No API key provided"),
220        }
221    }
222
223    /// Creates a new instance of Spider.
224    ///
225    /// # Arguments
226    ///
227    /// * `api_key` - An optional API key. Defaults to using the 'SPIDER_API_KEY' env variable.
228    /// * `client` - A custom client to pass in.
229    ///
230    /// # Returns
231    ///
232    /// A new instance of Spider or an error string if no API key is provided.
233    pub fn new_with_client(api_key: Option<String>, client: Client) -> Result<Self, &'static str> {
234        let api_key = api_key.or_else(|| std::env::var("SPIDER_API_KEY").ok());
235
236        match api_key {
237            Some(key) => Ok(Self {
238                api_key: key,
239                client,
240                rate_limit: RateLimitInfo::default(),
241            }),
242            None => Err("No API key provided"),
243        }
244    }
245
246    /// Update rate limit state from response headers.
247    fn update_rate_limit(&self, headers: &reqwest::header::HeaderMap) {
248        if let Some(v) = headers.get("RateLimit-Limit").and_then(|v| v.to_str().ok()) {
249            if let Ok(n) = v.parse::<u32>() {
250                self.rate_limit.limit.store(n, Ordering::Relaxed);
251            }
252        }
253        if let Some(v) = headers
254            .get("RateLimit-Remaining")
255            .and_then(|v| v.to_str().ok())
256        {
257            if let Ok(n) = v.parse::<u32>() {
258                self.rate_limit.remaining.store(n, Ordering::Relaxed);
259            }
260        }
261        if let Some(v) = headers.get("RateLimit-Reset").and_then(|v| v.to_str().ok()) {
262            if let Ok(n) = v.parse::<u32>() {
263                self.rate_limit.reset_seconds.store(n, Ordering::Relaxed);
264            }
265        }
266    }
267
268    /// Sends a POST request to the API.
269    ///
270    /// # Arguments
271    ///
272    /// * `endpoint` - The API endpoint.
273    /// * `data` - The request data as a HashMap.
274    /// * `stream` - Whether streaming is enabled.
275    /// * `content_type` - The content type of the request.
276    ///
277    /// # Returns
278    ///
279    /// The response from the API.
280    async fn api_post_base(
281        &self,
282        endpoint: &str,
283        data: impl Serialize + Sized + std::fmt::Debug,
284        content_type: &str,
285    ) -> Result<Response, Error> {
286        let url: String = format!("{}/{}", get_api_url(), endpoint);
287
288        let resp = self
289            .client
290            .post(&url)
291            .header(
292                "User-Agent",
293                format!("Spider-Client/{}", env!("CARGO_PKG_VERSION")),
294            )
295            .header("Content-Type", content_type)
296            .header("Authorization", format!("Bearer {}", self.api_key))
297            .json(&data)
298            .send()
299            .await?;
300
301        self.update_rate_limit(resp.headers());
302
303        if resp.status() == reqwest::StatusCode::TOO_MANY_REQUESTS {
304            let retry_after = resp
305                .headers()
306                .get("Retry-After")
307                .and_then(|v| v.to_str().ok())
308                .and_then(|v| v.parse::<u64>().ok())
309                .unwrap_or(1);
310            tokio::time::sleep(std::time::Duration::from_secs(retry_after)).await;
311            return resp.error_for_status();
312        }
313
314        Ok(resp)
315    }
316
317    /// Sends a POST request to the API.
318    ///
319    /// # Arguments
320    ///
321    /// * `endpoint` - The API endpoint.
322    /// * `data` - The request data as a HashMap.
323    /// * `stream` - Whether streaming is enabled.
324    /// * `content_type` - The content type of the request.
325    ///
326    /// # Returns
327    ///
328    /// The response from the API.
329    pub async fn api_post(
330        &self,
331        endpoint: &str,
332        data: impl Serialize + std::fmt::Debug + Clone + Send + Sync,
333        content_type: &str,
334    ) -> Result<Response, Error> {
335        let fetch = || async {
336            self.api_post_base(endpoint, data.to_owned(), content_type)
337                .await
338        };
339
340        fetch
341            .retry(ExponentialBuilder::default().with_max_times(5))
342            .when(|err: &reqwest::Error| {
343                if let Some(status) = err.status() {
344                    status.is_server_error() || status == reqwest::StatusCode::TOO_MANY_REQUESTS
345                } else {
346                    err.is_timeout()
347                }
348            })
349            .await
350    }
351
352    /// Sends a GET request to the API.
353    ///
354    /// # Arguments
355    ///
356    /// * `endpoint` - The API endpoint.
357    ///
358    /// # Returns
359    ///
360    /// The response from the API as a JSON value.
361    async fn api_get_base<T: Serialize>(
362        &self,
363        endpoint: &str,
364        query_params: Option<&T>,
365    ) -> Result<serde_json::Value, reqwest::Error> {
366        let url = format!("{}/{}", get_api_url(), endpoint);
367        let res = self
368            .client
369            .get(&url)
370            .query(&query_params)
371            .header(
372                "User-Agent",
373                format!("Spider-Client/{}", env!("CARGO_PKG_VERSION")),
374            )
375            .header("Content-Type", "application/json")
376            .header("Authorization", format!("Bearer {}", self.api_key))
377            .send()
378            .await?;
379
380        self.update_rate_limit(res.headers());
381
382        if res.status() == reqwest::StatusCode::TOO_MANY_REQUESTS {
383            let retry_after = res
384                .headers()
385                .get("Retry-After")
386                .and_then(|v| v.to_str().ok())
387                .and_then(|v| v.parse::<u64>().ok())
388                .unwrap_or(1);
389            tokio::time::sleep(std::time::Duration::from_secs(retry_after)).await;
390            return Err(res.error_for_status().unwrap_err());
391        }
392
393        parse_response(res).await
394    }
395
396    /// Sends a GET request to the API.
397    ///
398    /// # Arguments
399    ///
400    /// * `endpoint` - The API endpoint.
401    ///
402    /// # Returns
403    ///
404    /// The response from the API as a JSON value.
405    pub async fn api_get<T: Serialize>(
406        &self,
407        endpoint: &str,
408        query_params: Option<&T>,
409    ) -> Result<serde_json::Value, reqwest::Error> {
410        let fetch = || async { self.api_get_base(endpoint, query_params.to_owned()).await };
411
412        fetch
413            .retry(ExponentialBuilder::default().with_max_times(5))
414            .when(|err: &reqwest::Error| {
415                if let Some(status) = err.status() {
416                    status.is_server_error() || status == reqwest::StatusCode::TOO_MANY_REQUESTS
417                } else {
418                    err.is_timeout()
419                }
420            })
421            .await
422    }
423
424    /// Sends a DELETE request to the API.
425    ///
426    /// # Arguments
427    ///
428    /// * `endpoint` - The API endpoint.
429    /// * `params` - Optional request parameters.
430    /// * `stream` - Whether streaming is enabled.
431    /// * `content_type` - The content type of the request.
432    ///
433    /// # Returns
434    ///
435    /// The response from the API.
436    async fn api_delete_base(
437        &self,
438        endpoint: &str,
439        params: Option<HashMap<String, serde_json::Value>>,
440    ) -> Result<Response, Error> {
441        let url = format!("{}/v1/{}", get_api_url(), endpoint);
442        let request_builder = self
443            .client
444            .delete(&url)
445            .header(
446                "User-Agent",
447                format!("Spider-Client/{}", env!("CARGO_PKG_VERSION")),
448            )
449            .header("Content-Type", "application/json")
450            .header("Authorization", format!("Bearer {}", self.api_key));
451
452        let request_builder = if let Some(params) = params {
453            request_builder.json(&params)
454        } else {
455            request_builder
456        };
457
458        request_builder.send().await
459    }
460
461    /// Sends a DELETE request to the API.
462    ///
463    /// # Arguments
464    ///
465    /// * `endpoint` - The API endpoint.
466    /// * `params` - Optional request parameters.
467    /// * `stream` - Whether streaming is enabled.
468    /// * `content_type` - The content type of the request.
469    ///
470    /// # Returns
471    ///
472    /// The response from the API.
473    pub async fn api_delete(
474        &self,
475        endpoint: &str,
476        params: Option<HashMap<String, serde_json::Value>>,
477    ) -> Result<Response, Error> {
478        let fetch = || async { self.api_delete_base(endpoint, params.to_owned()).await };
479
480        fetch
481            .retry(ExponentialBuilder::default().with_max_times(5))
482            .when(|err: &reqwest::Error| {
483                if let Some(status) = err.status() {
484                    status.is_server_error()
485                } else {
486                    err.is_timeout()
487                }
488            })
489            .await
490    }
491
492    /// Scrapes a URL.
493    ///
494    /// # Arguments
495    ///
496    /// * `url` - The URL to scrape.
497    /// * `params` - Optional request parameters.
498    /// * `stream` - Whether streaming is enabled.
499    /// * `content_type` - The content type of the request.
500    ///
501    /// # Returns
502    ///
503    /// The response from the API as a JSON value.
504    pub async fn scrape_url(
505        &self,
506        url: &str,
507        params: Option<RequestParams>,
508        content_type: &str,
509    ) -> Result<serde_json::Value, reqwest::Error> {
510        let mut data = HashMap::new();
511
512        if let Ok(params) = serde_json::to_value(params) {
513            if let Some(ref p) = params.as_object() {
514                data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
515            }
516        }
517
518        if !url.is_empty() {
519            data.insert(
520                "url".to_string(),
521                serde_json::Value::String(url.to_string()),
522            );
523        }
524
525        let res = self.api_post("scrape", data, content_type).await?;
526        parse_response(res).await
527    }
528
529    /// Scrapes multi URLs.
530    ///
531    /// # Arguments
532    ///
533    /// * `url` - The URL to scrape.
534    /// * `params` - Optional request parameters.
535    /// * `stream` - Whether streaming is enabled.
536    /// * `content_type` - The content type of the request.
537    ///
538    /// # Returns
539    ///
540    /// The response from the API as a JSON value.
541    pub async fn multi_scrape_url(
542        &self,
543        params: Option<Vec<RequestParams>>,
544        content_type: &str,
545    ) -> Result<serde_json::Value, reqwest::Error> {
546        let mut data = HashMap::new();
547
548        if let Ok(mut params) = serde_json::to_value(params) {
549            if let Some(obj) = params.as_object_mut() {
550                obj.insert("limit".to_string(), serde_json::Value::Number(1.into()));
551                data.extend(obj.iter().map(|(k, v)| (k.clone(), v.clone())));
552            }
553        }
554        let res = self.api_post("scrape", data, content_type).await?;
555        parse_response(res).await
556    }
557
558    /// Crawls a URL.
559    ///
560    /// # Arguments
561    ///
562    /// * `url` - The URL to crawl.
563    /// * `params` - Optional request parameters.
564    /// * `stream` - Whether streaming is enabled.
565    /// * `content_type` - The content type of the request.
566    /// * `callback` - Optional callback function to handle each streamed chunk.
567    ///
568    /// # Returns
569    ///
570    /// The response from the API as a JSON value.
571    pub async fn crawl_url(
572        &self,
573        url: &str,
574        params: Option<RequestParams>,
575        stream: bool,
576        content_type: &str,
577        callback: Option<impl Fn(serde_json::Value) + Send>,
578    ) -> Result<serde_json::Value, reqwest::Error> {
579        use tokio_util::codec::{FramedRead, LinesCodec};
580
581        let mut data = HashMap::new();
582
583        if let Ok(params) = serde_json::to_value(params) {
584            if let Some(ref p) = params.as_object() {
585                data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
586            }
587        }
588
589        data.insert("url".into(), serde_json::Value::String(url.to_string()));
590
591        let res = self.api_post("crawl", data, content_type).await?;
592
593        if stream {
594            if let Some(callback) = callback {
595                let stream = res.bytes_stream();
596
597                let stream_reader = tokio_util::io::StreamReader::new(
598                    stream
599                        .map(|r| r.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))),
600                );
601
602                let mut lines = FramedRead::new(stream_reader, LinesCodec::new());
603
604                while let Some(line_result) = lines.next().await {
605                    match line_result {
606                        Ok(line) => match serde_json::from_str::<serde_json::Value>(&line) {
607                            Ok(value) => {
608                                callback(value);
609                            }
610                            Err(_e) => {
611                                continue;
612                            }
613                        },
614                        Err(_e) => return Ok(serde_json::Value::Null),
615                    }
616                }
617
618                Ok(serde_json::Value::Null)
619            } else {
620                Ok(serde_json::Value::Null)
621            }
622        } else {
623            parse_response(res).await
624        }
625    }
626
627    /// Crawls multiple URLs.
628    ///
629    /// # Arguments
630    ///
631    /// * `url` - The URL to crawl.
632    /// * `params` - Optional request parameters.
633    /// * `stream` - Whether streaming is enabled.
634    /// * `content_type` - The content type of the request.
635    /// * `callback` - Optional callback function to handle each streamed chunk.
636    ///
637    /// # Returns
638    ///
639    /// The response from the API as a JSON value.
640    pub async fn multi_crawl_url(
641        &self,
642        params: Option<Vec<RequestParams>>,
643        stream: bool,
644        content_type: &str,
645        callback: Option<impl Fn(serde_json::Value) + Send>,
646    ) -> Result<serde_json::Value, reqwest::Error> {
647        use tokio_util::codec::{FramedRead, LinesCodec};
648
649        let res = self.api_post("crawl", params, content_type).await?;
650
651        if stream {
652            if let Some(callback) = callback {
653                let stream = res.bytes_stream();
654
655                let stream_reader = tokio_util::io::StreamReader::new(
656                    stream
657                        .map(|r| r.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))),
658                );
659
660                let mut lines = FramedRead::new(stream_reader, LinesCodec::new());
661
662                while let Some(line_result) = lines.next().await {
663                    match line_result {
664                        Ok(line) => match serde_json::from_str::<serde_json::Value>(&line) {
665                            Ok(value) => {
666                                callback(value);
667                            }
668                            Err(_e) => {
669                                continue;
670                            }
671                        },
672                        Err(_e) => return Ok(serde_json::Value::Null),
673                    }
674                }
675
676                Ok(serde_json::Value::Null)
677            } else {
678                Ok(serde_json::Value::Null)
679            }
680        } else {
681            parse_response(res).await
682        }
683    }
684
685    /// Fetches links from a URL.
686    ///
687    /// # Arguments
688    ///
689    /// * `url` - The URL to fetch links from.
690    /// * `params` - Optional request parameters.
691    /// * `stream` - Whether streaming is enabled.
692    /// * `content_type` - The content type of the request.
693    ///
694    /// # Returns
695    ///
696    /// The response from the API as a JSON value.
697    pub async fn links(
698        &self,
699        url: &str,
700        params: Option<RequestParams>,
701        _stream: bool,
702        content_type: &str,
703    ) -> Result<serde_json::Value, reqwest::Error> {
704        let mut data = HashMap::new();
705
706        if let Ok(params) = serde_json::to_value(params) {
707            if let Some(ref p) = params.as_object() {
708                data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
709            }
710        }
711
712        data.insert("url".into(), serde_json::Value::String(url.to_string()));
713
714        let res = self.api_post("links", data, content_type).await?;
715        parse_response(res).await
716    }
717
718    /// Fetches links from a URLs.
719    ///
720    /// # Arguments
721    ///
722    /// * `url` - The URL to fetch links from.
723    /// * `params` - Optional request parameters.
724    /// * `stream` - Whether streaming is enabled.
725    /// * `content_type` - The content type of the request.
726    ///
727    /// # Returns
728    ///
729    /// The response from the API as a JSON value.
730    pub async fn multi_links(
731        &self,
732        params: Option<Vec<RequestParams>>,
733        _stream: bool,
734        content_type: &str,
735    ) -> Result<serde_json::Value, reqwest::Error> {
736        let res = self.api_post("links", params, content_type).await?;
737        parse_response(res).await
738    }
739
740    /// Takes a screenshot of a URL.
741    ///
742    /// # Arguments
743    ///
744    /// * `url` - The URL to take a screenshot of.
745    /// * `params` - Optional request parameters.
746    /// * `stream` - Whether streaming is enabled.
747    /// * `content_type` - The content type of the request.
748    ///
749    /// # Returns
750    ///
751    /// The response from the API as a JSON value.
752    pub async fn screenshot(
753        &self,
754        url: &str,
755        params: Option<RequestParams>,
756        _stream: bool,
757        content_type: &str,
758    ) -> Result<serde_json::Value, reqwest::Error> {
759        let mut data = HashMap::new();
760
761        if let Ok(params) = serde_json::to_value(params) {
762            if let Some(ref p) = params.as_object() {
763                data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
764            }
765        }
766
767        data.insert("url".into(), serde_json::Value::String(url.to_string()));
768
769        let res = self.api_post("screenshot", data, content_type).await?;
770        parse_response(res).await
771    }
772
773    /// Takes a screenshot of multiple URLs.
774    ///
775    /// # Arguments
776    ///
777    /// * `url` - The URL to take a screenshot of.
778    /// * `params` - Optional request parameters.
779    /// * `stream` - Whether streaming is enabled.
780    /// * `content_type` - The content type of the request.
781    ///
782    /// # Returns
783    ///
784    /// The response from the API as a JSON value.
785    pub async fn multi_screenshot(
786        &self,
787        params: Option<Vec<RequestParams>>,
788        _stream: bool,
789        content_type: &str,
790    ) -> Result<serde_json::Value, reqwest::Error> {
791        let res = self.api_post("screenshot", params, content_type).await?;
792        parse_response(res).await
793    }
794
795    /// Searches for a query.
796    ///
797    /// # Arguments
798    ///
799    /// * `q` - The query to search for.
800    /// * `params` - Optional request parameters.
801    /// * `stream` - Whether streaming is enabled.
802    /// * `content_type` - The content type of the request.
803    ///
804    /// # Returns
805    ///
806    /// The response from the API as a JSON value.
807    pub async fn search(
808        &self,
809        q: &str,
810        params: Option<SearchRequestParams>,
811        _stream: bool,
812        content_type: &str,
813    ) -> Result<serde_json::Value, reqwest::Error> {
814        let body = match params {
815            Some(mut params) => {
816                params.search = q.to_string();
817                params
818            }
819            _ => {
820                let mut params = SearchRequestParams::default();
821                params.search = q.to_string();
822                params
823            }
824        };
825
826        let res = self.api_post("search", body, content_type).await?;
827
828        parse_response(res).await
829    }
830
831    /// Searches for multiple querys.
832    ///
833    /// # Arguments
834    ///
835    /// * `q` - The query to search for.
836    /// * `params` - Optional request parameters.
837    /// * `stream` - Whether streaming is enabled.
838    /// * `content_type` - The content type of the request.
839    ///
840    /// # Returns
841    ///
842    /// The response from the API as a JSON value.
843    pub async fn multi_search(
844        &self,
845        params: Option<Vec<SearchRequestParams>>,
846        content_type: &str,
847    ) -> Result<serde_json::Value, reqwest::Error> {
848        let res = self.api_post("search", params, content_type).await?;
849        parse_response(res).await
850    }
851
852    /// AI-guided crawling of a website using a natural language prompt.
853    ///
854    /// Requires an active AI Studio subscription (billed separately from credits):
855    /// <https://spider.cloud/ai/pricing>
856    ///
857    /// # Arguments
858    ///
859    /// * `url` - The URL to start crawling.
860    /// * `prompt` - Natural language instruction for what to crawl and extract.
861    /// * `params` - Optional request parameters.
862    /// * `content_type` - The content type of the request.
863    ///
864    /// # Returns
865    ///
866    /// The response from the API as a JSON value.
867    pub async fn ai_crawl(
868        &self,
869        url: &str,
870        prompt: &str,
871        params: Option<RequestParams>,
872        content_type: &str,
873    ) -> Result<serde_json::Value, reqwest::Error> {
874        let mut data = HashMap::new();
875
876        if let Ok(params) = serde_json::to_value(params) {
877            if let Some(ref p) = params.as_object() {
878                data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
879            }
880        }
881
882        if !url.is_empty() {
883            data.insert(
884                "url".to_string(),
885                serde_json::Value::String(url.to_string()),
886            );
887        }
888
889        data.insert(
890            "prompt".to_string(),
891            serde_json::Value::String(prompt.to_string()),
892        );
893
894        let res = self.api_post("ai/crawl", data, content_type).await?;
895        parse_response(res).await
896    }
897
898    /// AI-guided scraping of a URL using a natural language prompt.
899    ///
900    /// Requires an active AI Studio subscription (billed separately from credits):
901    /// <https://spider.cloud/ai/pricing>
902    ///
903    /// # Arguments
904    ///
905    /// * `url` - The URL to scrape.
906    /// * `prompt` - Natural language description of the data to extract.
907    /// * `params` - Optional request parameters.
908    /// * `content_type` - The content type of the request.
909    ///
910    /// # Returns
911    ///
912    /// The response from the API as a JSON value.
913    pub async fn ai_scrape(
914        &self,
915        url: &str,
916        prompt: &str,
917        params: Option<RequestParams>,
918        content_type: &str,
919    ) -> Result<serde_json::Value, reqwest::Error> {
920        let mut data = HashMap::new();
921
922        if let Ok(params) = serde_json::to_value(params) {
923            if let Some(ref p) = params.as_object() {
924                data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
925            }
926        }
927
928        if !url.is_empty() {
929            data.insert(
930                "url".to_string(),
931                serde_json::Value::String(url.to_string()),
932            );
933        }
934
935        data.insert(
936            "prompt".to_string(),
937            serde_json::Value::String(prompt.to_string()),
938        );
939
940        let res = self.api_post("ai/scrape", data, content_type).await?;
941        parse_response(res).await
942    }
943
944    /// AI-enhanced web search using a natural language query.
945    ///
946    /// Requires an active AI Studio subscription (billed separately from credits):
947    /// <https://spider.cloud/ai/pricing>
948    ///
949    /// # Arguments
950    ///
951    /// * `prompt` - Natural language search query.
952    /// * `params` - Optional request parameters.
953    /// * `content_type` - The content type of the request.
954    ///
955    /// # Returns
956    ///
957    /// The response from the API as a JSON value.
958    pub async fn ai_search(
959        &self,
960        prompt: &str,
961        params: Option<RequestParams>,
962        content_type: &str,
963    ) -> Result<serde_json::Value, reqwest::Error> {
964        let mut data = HashMap::new();
965
966        if let Ok(params) = serde_json::to_value(params) {
967            if let Some(ref p) = params.as_object() {
968                data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
969            }
970        }
971
972        data.insert(
973            "prompt".to_string(),
974            serde_json::Value::String(prompt.to_string()),
975        );
976
977        let res = self.api_post("ai/search", data, content_type).await?;
978        parse_response(res).await
979    }
980
981    /// AI-guided browser automation using natural language commands.
982    ///
983    /// Requires an active AI Studio subscription (billed separately from credits):
984    /// <https://spider.cloud/ai/pricing>
985    ///
986    /// # Arguments
987    ///
988    /// * `url` - The URL to automate.
989    /// * `prompt` - Natural language description of the browser actions.
990    /// * `params` - Optional request parameters.
991    /// * `content_type` - The content type of the request.
992    ///
993    /// # Returns
994    ///
995    /// The response from the API as a JSON value.
996    pub async fn ai_browser(
997        &self,
998        url: &str,
999        prompt: &str,
1000        params: Option<RequestParams>,
1001        content_type: &str,
1002    ) -> Result<serde_json::Value, reqwest::Error> {
1003        let mut data = HashMap::new();
1004
1005        if let Ok(params) = serde_json::to_value(params) {
1006            if let Some(ref p) = params.as_object() {
1007                data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1008            }
1009        }
1010
1011        if !url.is_empty() {
1012            data.insert(
1013                "url".to_string(),
1014                serde_json::Value::String(url.to_string()),
1015            );
1016        }
1017
1018        data.insert(
1019            "prompt".to_string(),
1020            serde_json::Value::String(prompt.to_string()),
1021        );
1022
1023        let res = self.api_post("ai/browser", data, content_type).await?;
1024        parse_response(res).await
1025    }
1026
1027    /// AI-guided link extraction and filtering using a natural language prompt.
1028    ///
1029    /// Requires an active AI Studio subscription (billed separately from credits):
1030    /// <https://spider.cloud/ai/pricing>
1031    ///
1032    /// # Arguments
1033    ///
1034    /// * `url` - The URL to extract links from.
1035    /// * `prompt` - Natural language description of the links to find.
1036    /// * `params` - Optional request parameters.
1037    /// * `content_type` - The content type of the request.
1038    ///
1039    /// # Returns
1040    ///
1041    /// The response from the API as a JSON value.
1042    pub async fn ai_links(
1043        &self,
1044        url: &str,
1045        prompt: &str,
1046        params: Option<RequestParams>,
1047        content_type: &str,
1048    ) -> Result<serde_json::Value, reqwest::Error> {
1049        let mut data = HashMap::new();
1050
1051        if let Ok(params) = serde_json::to_value(params) {
1052            if let Some(ref p) = params.as_object() {
1053                data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1054            }
1055        }
1056
1057        if !url.is_empty() {
1058            data.insert(
1059                "url".to_string(),
1060                serde_json::Value::String(url.to_string()),
1061            );
1062        }
1063
1064        data.insert(
1065            "prompt".to_string(),
1066            serde_json::Value::String(prompt.to_string()),
1067        );
1068
1069        let res = self.api_post("ai/links", data, content_type).await?;
1070        parse_response(res).await
1071    }
1072
1073    /// Scrapes a URL on the Unlimited plan. Takes the same parameters and
1074    /// returns the same responses as [`Spider::scrape_url`].
1075    ///
1076    /// Requires an active Unlimited subscription — a flat monthly rate billed by
1077    /// purchased concurrency seats (the number of requests in flight at once)
1078    /// instead of per-request credits. Without one the API returns `403` with
1079    /// `{"error":"unlimited_plan_required"}` or `{"error":"unlimited_plan_inactive"}`.
1080    ///
1081    /// There is no queueing: when all purchased seats are in flight the API
1082    /// returns an immediate `429` with a `Retry-After` header — callers should
1083    /// retry with backoff (this client already retries with exponential backoff).
1084    /// Every response carries `X-Concurrency-Limit` and `X-Concurrency-Active`
1085    /// headers. AI/LLM extraction params (`prompt`, `extraction_schema`,
1086    /// model/vision params) are not allowed and are rejected with `400`.
1087    ///
1088    /// Docs: <https://spider.cloud/docs/api/unlimited> — Pricing:
1089    /// <https://spider.cloud/pricing?plan=unlimited>
1090    ///
1091    /// # Arguments
1092    ///
1093    /// * `url` - The URL to scrape.
1094    /// * `params` - Optional request parameters.
1095    /// * `content_type` - The content type of the request.
1096    ///
1097    /// # Returns
1098    ///
1099    /// The response from the API as a JSON value.
1100    pub async fn unlimited_scrape(
1101        &self,
1102        url: &str,
1103        params: Option<RequestParams>,
1104        content_type: &str,
1105    ) -> Result<serde_json::Value, reqwest::Error> {
1106        let mut data = HashMap::new();
1107
1108        if let Ok(params) = serde_json::to_value(params) {
1109            if let Some(ref p) = params.as_object() {
1110                data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1111            }
1112        }
1113
1114        if !url.is_empty() {
1115            data.insert(
1116                "url".to_string(),
1117                serde_json::Value::String(url.to_string()),
1118            );
1119        }
1120
1121        let res = self
1122            .api_post("unlimited/scrape", data, content_type)
1123            .await?;
1124        parse_response(res).await
1125    }
1126
1127    /// Crawls a URL on the Unlimited plan. Takes the same parameters and
1128    /// returns the same responses as [`Spider::crawl_url`].
1129    ///
1130    /// Requires an active Unlimited subscription — a flat monthly rate billed by
1131    /// purchased concurrency seats (the number of requests in flight at once)
1132    /// instead of per-request credits. Without one the API returns `403` with
1133    /// `{"error":"unlimited_plan_required"}` or `{"error":"unlimited_plan_inactive"}`.
1134    ///
1135    /// There is no queueing: when all purchased seats are in flight the API
1136    /// returns an immediate `429` with a `Retry-After` header — callers should
1137    /// retry with backoff (this client already retries with exponential backoff).
1138    /// Every response carries `X-Concurrency-Limit` and `X-Concurrency-Active`
1139    /// headers. AI/LLM extraction params (`prompt`, `extraction_schema`,
1140    /// model/vision params) are not allowed and are rejected with `400`.
1141    ///
1142    /// Docs: <https://spider.cloud/docs/api/unlimited> — Pricing:
1143    /// <https://spider.cloud/pricing?plan=unlimited>
1144    ///
1145    /// # Arguments
1146    ///
1147    /// * `url` - The URL to crawl.
1148    /// * `params` - Optional request parameters.
1149    /// * `stream` - Whether streaming is enabled.
1150    /// * `content_type` - The content type of the request.
1151    /// * `callback` - Optional callback function to handle each streamed chunk.
1152    ///
1153    /// # Returns
1154    ///
1155    /// The response from the API as a JSON value.
1156    pub async fn unlimited_crawl(
1157        &self,
1158        url: &str,
1159        params: Option<RequestParams>,
1160        stream: bool,
1161        content_type: &str,
1162        callback: Option<impl Fn(serde_json::Value) + Send>,
1163    ) -> Result<serde_json::Value, reqwest::Error> {
1164        use tokio_util::codec::{FramedRead, LinesCodec};
1165
1166        let mut data = HashMap::new();
1167
1168        if let Ok(params) = serde_json::to_value(params) {
1169            if let Some(ref p) = params.as_object() {
1170                data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1171            }
1172        }
1173
1174        data.insert("url".into(), serde_json::Value::String(url.to_string()));
1175
1176        let res = self.api_post("unlimited/crawl", data, content_type).await?;
1177
1178        if stream {
1179            if let Some(callback) = callback {
1180                let stream = res.bytes_stream();
1181
1182                let stream_reader = tokio_util::io::StreamReader::new(
1183                    stream
1184                        .map(|r| r.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))),
1185                );
1186
1187                let mut lines = FramedRead::new(stream_reader, LinesCodec::new());
1188
1189                while let Some(line_result) = lines.next().await {
1190                    match line_result {
1191                        Ok(line) => match serde_json::from_str::<serde_json::Value>(&line) {
1192                            Ok(value) => {
1193                                callback(value);
1194                            }
1195                            Err(_e) => {
1196                                continue;
1197                            }
1198                        },
1199                        Err(_e) => return Ok(serde_json::Value::Null),
1200                    }
1201                }
1202
1203                Ok(serde_json::Value::Null)
1204            } else {
1205                Ok(serde_json::Value::Null)
1206            }
1207        } else {
1208            parse_response(res).await
1209        }
1210    }
1211
1212    /// Fetches links from a URL on the Unlimited plan. Takes the same parameters
1213    /// and returns the same responses as [`Spider::links`].
1214    ///
1215    /// Requires an active Unlimited subscription — a flat monthly rate billed by
1216    /// purchased concurrency seats (the number of requests in flight at once)
1217    /// instead of per-request credits. Without one the API returns `403` with
1218    /// `{"error":"unlimited_plan_required"}` or `{"error":"unlimited_plan_inactive"}`.
1219    ///
1220    /// There is no queueing: when all purchased seats are in flight the API
1221    /// returns an immediate `429` with a `Retry-After` header — callers should
1222    /// retry with backoff (this client already retries with exponential backoff).
1223    /// Every response carries `X-Concurrency-Limit` and `X-Concurrency-Active`
1224    /// headers. AI/LLM extraction params (`prompt`, `extraction_schema`,
1225    /// model/vision params) are not allowed and are rejected with `400`.
1226    ///
1227    /// Docs: <https://spider.cloud/docs/api/unlimited> — Pricing:
1228    /// <https://spider.cloud/pricing?plan=unlimited>
1229    ///
1230    /// # Arguments
1231    ///
1232    /// * `url` - The URL to fetch links from.
1233    /// * `params` - Optional request parameters.
1234    /// * `stream` - Whether streaming is enabled.
1235    /// * `content_type` - The content type of the request.
1236    ///
1237    /// # Returns
1238    ///
1239    /// The response from the API as a JSON value.
1240    pub async fn unlimited_links(
1241        &self,
1242        url: &str,
1243        params: Option<RequestParams>,
1244        _stream: bool,
1245        content_type: &str,
1246    ) -> Result<serde_json::Value, reqwest::Error> {
1247        let mut data = HashMap::new();
1248
1249        if let Ok(params) = serde_json::to_value(params) {
1250            if let Some(ref p) = params.as_object() {
1251                data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1252            }
1253        }
1254
1255        data.insert("url".into(), serde_json::Value::String(url.to_string()));
1256
1257        let res = self.api_post("unlimited/links", data, content_type).await?;
1258        parse_response(res).await
1259    }
1260
1261    /// Unblock a URL.
1262    ///
1263    /// # Arguments
1264    ///
1265    /// * `url` - The URL to scrape.
1266    /// * `params` - Optional request parameters.
1267    /// * `stream` - Whether streaming is enabled.
1268    /// * `content_type` - The content type of the request.
1269    ///
1270    /// # Returns
1271    ///
1272    /// The response from the API as a JSON value.
1273    pub async fn unblock_url(
1274        &self,
1275        url: &str,
1276        params: Option<RequestParams>,
1277        content_type: &str,
1278    ) -> Result<serde_json::Value, reqwest::Error> {
1279        let mut data = HashMap::new();
1280
1281        if let Ok(params) = serde_json::to_value(params) {
1282            if let Some(ref p) = params.as_object() {
1283                data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1284            }
1285        }
1286
1287        if !url.is_empty() {
1288            data.insert(
1289                "url".to_string(),
1290                serde_json::Value::String(url.to_string()),
1291            );
1292        }
1293
1294        let res = self.api_post("unblocker", data, content_type).await?;
1295        parse_response(res).await
1296    }
1297
1298    /// Unblock multi URLs.
1299    ///
1300    /// # Arguments
1301    ///
1302    /// * `url` - The URL to scrape.
1303    /// * `params` - Optional request parameters.
1304    /// * `stream` - Whether streaming is enabled.
1305    /// * `content_type` - The content type of the request.
1306    ///
1307    /// # Returns
1308    ///
1309    /// The response from the API as a JSON value.
1310    pub async fn multi_unblock_url(
1311        &self,
1312        params: Option<Vec<RequestParams>>,
1313        content_type: &str,
1314    ) -> Result<serde_json::Value, reqwest::Error> {
1315        let mut data = HashMap::new();
1316
1317        if let Ok(mut params) = serde_json::to_value(params) {
1318            if let Some(obj) = params.as_object_mut() {
1319                obj.insert("limit".to_string(), serde_json::Value::Number(1.into()));
1320                data.extend(obj.iter().map(|(k, v)| (k.clone(), v.clone())));
1321            }
1322        }
1323        let res = self.api_post("unblocker", data, content_type).await?;
1324        parse_response(res).await
1325    }
1326
1327    /// Transforms data.
1328    ///
1329    /// # Arguments
1330    ///
1331    /// * `data` - The data to transform.
1332    /// * `params` - Optional request parameters.
1333    /// * `stream` - Whether streaming is enabled.
1334    /// * `content_type` - The content type of the request.
1335    ///
1336    /// # Returns
1337    ///
1338    /// The response from the API as a JSON value.
1339    pub async fn transform(
1340        &self,
1341        data: Vec<HashMap<&str, &str>>,
1342        params: Option<TransformParams>,
1343        _stream: bool,
1344        content_type: &str,
1345    ) -> Result<serde_json::Value, reqwest::Error> {
1346        let mut payload = HashMap::new();
1347
1348        if let Ok(params) = serde_json::to_value(params) {
1349            if let Some(ref p) = params.as_object() {
1350                payload.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1351            }
1352        }
1353
1354        if let Ok(d) = serde_json::to_value(data) {
1355            payload.insert("data".into(), d);
1356        }
1357
1358        let res = self.api_post("transform", payload, content_type).await?;
1359
1360        parse_response(res).await
1361    }
1362
1363    /// Get the account credits left.
1364    pub async fn get_credits(&self) -> Result<serde_json::Value, reqwest::Error> {
1365        self.api_get::<serde_json::Value>("data/credits", None)
1366            .await
1367    }
1368
1369    /// Send a request for a data record.
1370    pub async fn data_post(
1371        &self,
1372        table: &str,
1373        data: Option<RequestParams>,
1374    ) -> Result<serde_json::Value, reqwest::Error> {
1375        let res = self
1376            .api_post(&format!("data/{}", table), data, "application/json")
1377            .await?;
1378        parse_response(res).await
1379    }
1380
1381    /// Get a table record.
1382    pub async fn data_get(
1383        &self,
1384        table: &str,
1385        params: Option<RequestParams>,
1386    ) -> Result<serde_json::Value, reqwest::Error> {
1387        let mut payload = HashMap::new();
1388
1389        if let Some(params) = params {
1390            if let Ok(p) = serde_json::to_value(params) {
1391                if let Some(o) = p.as_object() {
1392                    payload.extend(o.iter().map(|(k, v)| (k.as_str(), v.clone())));
1393                }
1394            }
1395        }
1396
1397        let res = self
1398            .api_get::<serde_json::Value>(&format!("data/{}", table), None)
1399            .await?;
1400        Ok(res)
1401    }
1402}
1403
1404#[cfg(test)]
1405mod tests {
1406    use super::*;
1407    use dotenv::dotenv;
1408    use lazy_static::lazy_static;
1409    use reqwest::ClientBuilder;
1410
1411    lazy_static! {
1412        static ref SPIDER_CLIENT: Spider = {
1413            dotenv().ok();
1414            let client = ClientBuilder::new();
1415            let client = client.user_agent("SpiderBot").build().unwrap();
1416
1417            Spider::new_with_client(None, client).expect("client to build")
1418        };
1419    }
1420
1421    #[tokio::test]
1422    #[ignore]
1423    async fn test_scrape_url() {
1424        let response = SPIDER_CLIENT
1425            .scrape_url("https://example.com", None, "application/json")
1426            .await;
1427        assert!(response.is_ok());
1428    }
1429
1430    #[tokio::test]
1431    async fn test_crawl_url() {
1432        let response = SPIDER_CLIENT
1433            .crawl_url(
1434                "https://example.com",
1435                None,
1436                false,
1437                "application/json",
1438                None::<fn(serde_json::Value)>,
1439            )
1440            .await;
1441        assert!(response.is_ok());
1442    }
1443
1444    #[tokio::test]
1445    #[ignore]
1446    async fn test_links() {
1447        let response: Result<serde_json::Value, Error> = SPIDER_CLIENT
1448            .links("https://example.com", None, false, "application/json")
1449            .await;
1450        assert!(response.is_ok());
1451    }
1452
1453    #[tokio::test]
1454    #[ignore]
1455    async fn test_ai_scrape() {
1456        let response = SPIDER_CLIENT
1457            .ai_scrape(
1458                "https://example.com",
1459                "Extract the page title",
1460                None,
1461                "application/json",
1462            )
1463            .await;
1464        assert!(response.is_ok());
1465    }
1466
1467    #[tokio::test]
1468    #[ignore]
1469    async fn test_ai_search() {
1470        let response = SPIDER_CLIENT
1471            .ai_search("Find the top sports websites", None, "application/json")
1472            .await;
1473        assert!(response.is_ok());
1474    }
1475
1476    #[tokio::test]
1477    #[ignore]
1478    async fn test_unlimited_scrape() {
1479        let response = SPIDER_CLIENT
1480            .unlimited_scrape("https://example.com", None, "application/json")
1481            .await;
1482        assert!(response.is_ok());
1483    }
1484
1485    #[tokio::test]
1486    #[ignore]
1487    async fn test_unlimited_crawl() {
1488        let response = SPIDER_CLIENT
1489            .unlimited_crawl(
1490                "https://example.com",
1491                None,
1492                false,
1493                "application/json",
1494                None::<fn(serde_json::Value)>,
1495            )
1496            .await;
1497        assert!(response.is_ok());
1498    }
1499
1500    #[tokio::test]
1501    #[ignore]
1502    async fn test_unlimited_links() {
1503        let response: Result<serde_json::Value, Error> = SPIDER_CLIENT
1504            .unlimited_links("https://example.com", None, false, "application/json")
1505            .await;
1506        assert!(response.is_ok());
1507    }
1508
1509    #[tokio::test]
1510    #[ignore]
1511    async fn test_screenshot() {
1512        let mut params = RequestParams::default();
1513        params.limit = Some(1);
1514
1515        let response = SPIDER_CLIENT
1516            .screenshot(
1517                "https://example.com",
1518                Some(params),
1519                false,
1520                "application/json",
1521            )
1522            .await;
1523        assert!(response.is_ok());
1524    }
1525
1526    // #[tokio::test(flavor = "multi_thread")]
1527    // async fn test_search() {
1528    //     let mut params = SearchRequestParams::default();
1529
1530    //     params.search_limit = Some(1);
1531    //     params.num = Some(1);
1532    //     params.fetch_page_content = Some(false);
1533
1534    //     let response = SPIDER_CLIENT
1535    //         .search("a sports website", Some(params), false, "application/json")
1536    //         .await;
1537
1538    //     assert!(response.is_ok());
1539    // }
1540
1541    #[tokio::test]
1542    #[ignore]
1543    async fn test_transform() {
1544        let data = vec![HashMap::from([(
1545            "<html><body><h1>Transformation</h1></body></html>".into(),
1546            "".into(),
1547        )])];
1548        let response = SPIDER_CLIENT
1549            .transform(data, None, false, "application/json")
1550            .await;
1551        assert!(response.is_ok());
1552    }
1553
1554    #[tokio::test]
1555    async fn test_get_credits() {
1556        let response = SPIDER_CLIENT.get_credits().await;
1557        assert!(response.is_ok());
1558    }
1559}