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