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.api_post("unlimited/scrape", data, content_type).await?;
1120        parse_response(res).await
1121    }
1122
1123    /// Crawls a URL on the Unlimited plan. Takes the same parameters and
1124    /// returns the same responses as [`Spider::crawl_url`].
1125    ///
1126    /// Requires an active Unlimited subscription — a flat monthly rate billed by
1127    /// purchased concurrency seats (the number of requests in flight at once)
1128    /// instead of per-request credits. Without one the API returns `403` with
1129    /// `{"error":"unlimited_plan_required"}` or `{"error":"unlimited_plan_inactive"}`.
1130    ///
1131    /// There is no queueing: when all purchased seats are in flight the API
1132    /// returns an immediate `429` with a `Retry-After` header — callers should
1133    /// retry with backoff (this client already retries with exponential backoff).
1134    /// Every response carries `X-Concurrency-Limit` and `X-Concurrency-Active`
1135    /// headers. AI/LLM extraction params (`prompt`, `extraction_schema`,
1136    /// model/vision params) are not allowed and are rejected with `400`.
1137    ///
1138    /// Docs: <https://spider.cloud/docs/api/unlimited> — Pricing:
1139    /// <https://spider.cloud/pricing?plan=unlimited>
1140    ///
1141    /// # Arguments
1142    ///
1143    /// * `url` - The URL to crawl.
1144    /// * `params` - Optional request parameters.
1145    /// * `stream` - Whether streaming is enabled.
1146    /// * `content_type` - The content type of the request.
1147    /// * `callback` - Optional callback function to handle each streamed chunk.
1148    ///
1149    /// # Returns
1150    ///
1151    /// The response from the API as a JSON value.
1152    pub async fn unlimited_crawl(
1153        &self,
1154        url: &str,
1155        params: Option<RequestParams>,
1156        stream: bool,
1157        content_type: &str,
1158        callback: Option<impl Fn(serde_json::Value) + Send>,
1159    ) -> Result<serde_json::Value, reqwest::Error> {
1160        use tokio_util::codec::{FramedRead, LinesCodec};
1161
1162        let mut data = HashMap::new();
1163
1164        if let Ok(params) = serde_json::to_value(params) {
1165            if let Some(ref p) = params.as_object() {
1166                data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1167            }
1168        }
1169
1170        data.insert("url".into(), serde_json::Value::String(url.to_string()));
1171
1172        let res = self.api_post("unlimited/crawl", data, content_type).await?;
1173
1174        if stream {
1175            if let Some(callback) = callback {
1176                let stream = res.bytes_stream();
1177
1178                let stream_reader = tokio_util::io::StreamReader::new(
1179                    stream
1180                        .map(|r| r.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))),
1181                );
1182
1183                let mut lines = FramedRead::new(stream_reader, LinesCodec::new());
1184
1185                while let Some(line_result) = lines.next().await {
1186                    match line_result {
1187                        Ok(line) => match serde_json::from_str::<serde_json::Value>(&line) {
1188                            Ok(value) => {
1189                                callback(value);
1190                            }
1191                            Err(_e) => {
1192                                continue;
1193                            }
1194                        },
1195                        Err(_e) => return Ok(serde_json::Value::Null),
1196                    }
1197                }
1198
1199                Ok(serde_json::Value::Null)
1200            } else {
1201                Ok(serde_json::Value::Null)
1202            }
1203        } else {
1204            parse_response(res).await
1205        }
1206    }
1207
1208    /// Fetches links from a URL on the Unlimited plan. Takes the same parameters
1209    /// and returns the same responses as [`Spider::links`].
1210    ///
1211    /// Requires an active Unlimited subscription — a flat monthly rate billed by
1212    /// purchased concurrency seats (the number of requests in flight at once)
1213    /// instead of per-request credits. Without one the API returns `403` with
1214    /// `{"error":"unlimited_plan_required"}` or `{"error":"unlimited_plan_inactive"}`.
1215    ///
1216    /// There is no queueing: when all purchased seats are in flight the API
1217    /// returns an immediate `429` with a `Retry-After` header — callers should
1218    /// retry with backoff (this client already retries with exponential backoff).
1219    /// Every response carries `X-Concurrency-Limit` and `X-Concurrency-Active`
1220    /// headers. AI/LLM extraction params (`prompt`, `extraction_schema`,
1221    /// model/vision params) are not allowed and are rejected with `400`.
1222    ///
1223    /// Docs: <https://spider.cloud/docs/api/unlimited> — Pricing:
1224    /// <https://spider.cloud/pricing?plan=unlimited>
1225    ///
1226    /// # Arguments
1227    ///
1228    /// * `url` - The URL to fetch links from.
1229    /// * `params` - Optional request parameters.
1230    /// * `stream` - Whether streaming is enabled.
1231    /// * `content_type` - The content type of the request.
1232    ///
1233    /// # Returns
1234    ///
1235    /// The response from the API as a JSON value.
1236    pub async fn unlimited_links(
1237        &self,
1238        url: &str,
1239        params: Option<RequestParams>,
1240        _stream: bool,
1241        content_type: &str,
1242    ) -> Result<serde_json::Value, reqwest::Error> {
1243        let mut data = HashMap::new();
1244
1245        if let Ok(params) = serde_json::to_value(params) {
1246            if let Some(ref p) = params.as_object() {
1247                data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1248            }
1249        }
1250
1251        data.insert("url".into(), serde_json::Value::String(url.to_string()));
1252
1253        let res = self.api_post("unlimited/links", data, content_type).await?;
1254        parse_response(res).await
1255    }
1256
1257    /// Unblock a URL.
1258    ///
1259    /// # Arguments
1260    ///
1261    /// * `url` - The URL to scrape.
1262    /// * `params` - Optional request parameters.
1263    /// * `stream` - Whether streaming is enabled.
1264    /// * `content_type` - The content type of the request.
1265    ///
1266    /// # Returns
1267    ///
1268    /// The response from the API as a JSON value.
1269    pub async fn unblock_url(
1270        &self,
1271        url: &str,
1272        params: Option<RequestParams>,
1273        content_type: &str,
1274    ) -> Result<serde_json::Value, reqwest::Error> {
1275        let mut data = HashMap::new();
1276
1277        if let Ok(params) = serde_json::to_value(params) {
1278            if let Some(ref p) = params.as_object() {
1279                data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1280            }
1281        }
1282
1283        if !url.is_empty() {
1284            data.insert(
1285                "url".to_string(),
1286                serde_json::Value::String(url.to_string()),
1287            );
1288        }
1289
1290        let res = self.api_post("unblocker", data, content_type).await?;
1291        parse_response(res).await
1292    }
1293
1294    /// Unblock multi URLs.
1295    ///
1296    /// # Arguments
1297    ///
1298    /// * `url` - The URL to scrape.
1299    /// * `params` - Optional request parameters.
1300    /// * `stream` - Whether streaming is enabled.
1301    /// * `content_type` - The content type of the request.
1302    ///
1303    /// # Returns
1304    ///
1305    /// The response from the API as a JSON value.
1306    pub async fn multi_unblock_url(
1307        &self,
1308        params: Option<Vec<RequestParams>>,
1309        content_type: &str,
1310    ) -> Result<serde_json::Value, reqwest::Error> {
1311        let mut data = HashMap::new();
1312
1313        if let Ok(mut params) = serde_json::to_value(params) {
1314            if let Some(obj) = params.as_object_mut() {
1315                obj.insert("limit".to_string(), serde_json::Value::Number(1.into()));
1316                data.extend(obj.iter().map(|(k, v)| (k.clone(), v.clone())));
1317            }
1318        }
1319        let res = self.api_post("unblocker", data, content_type).await?;
1320        parse_response(res).await
1321    }
1322
1323    /// Transforms data.
1324    ///
1325    /// # Arguments
1326    ///
1327    /// * `data` - The data to transform.
1328    /// * `params` - Optional request parameters.
1329    /// * `stream` - Whether streaming is enabled.
1330    /// * `content_type` - The content type of the request.
1331    ///
1332    /// # Returns
1333    ///
1334    /// The response from the API as a JSON value.
1335    pub async fn transform(
1336        &self,
1337        data: Vec<HashMap<&str, &str>>,
1338        params: Option<TransformParams>,
1339        _stream: bool,
1340        content_type: &str,
1341    ) -> Result<serde_json::Value, reqwest::Error> {
1342        let mut payload = HashMap::new();
1343
1344        if let Ok(params) = serde_json::to_value(params) {
1345            if let Some(ref p) = params.as_object() {
1346                payload.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1347            }
1348        }
1349
1350        if let Ok(d) = serde_json::to_value(data) {
1351            payload.insert("data".into(), d);
1352        }
1353
1354        let res = self.api_post("transform", payload, content_type).await?;
1355
1356        parse_response(res).await
1357    }
1358
1359    /// Get the account credits left.
1360    pub async fn get_credits(&self) -> Result<serde_json::Value, reqwest::Error> {
1361        self.api_get::<serde_json::Value>("data/credits", None)
1362            .await
1363    }
1364
1365    /// Send a request for a data record.
1366    pub async fn data_post(
1367        &self,
1368        table: &str,
1369        data: Option<RequestParams>,
1370    ) -> Result<serde_json::Value, reqwest::Error> {
1371        let res = self
1372            .api_post(&format!("data/{}", table), data, "application/json")
1373            .await?;
1374        parse_response(res).await
1375    }
1376
1377    /// Get a table record.
1378    pub async fn data_get(
1379        &self,
1380        table: &str,
1381        params: Option<RequestParams>,
1382    ) -> Result<serde_json::Value, reqwest::Error> {
1383        let mut payload = HashMap::new();
1384
1385        if let Some(params) = params {
1386            if let Ok(p) = serde_json::to_value(params) {
1387                if let Some(o) = p.as_object() {
1388                    payload.extend(o.iter().map(|(k, v)| (k.as_str(), v.clone())));
1389                }
1390            }
1391        }
1392
1393        let res = self
1394            .api_get::<serde_json::Value>(&format!("data/{}", table), None)
1395            .await?;
1396        Ok(res)
1397    }
1398}
1399
1400#[cfg(test)]
1401mod tests {
1402    use super::*;
1403    use dotenv::dotenv;
1404    use lazy_static::lazy_static;
1405    use reqwest::ClientBuilder;
1406
1407    lazy_static! {
1408        static ref SPIDER_CLIENT: Spider = {
1409            dotenv().ok();
1410            let client = ClientBuilder::new();
1411            let client = client.user_agent("SpiderBot").build().unwrap();
1412
1413            Spider::new_with_client(None, client).expect("client to build")
1414        };
1415    }
1416
1417    #[tokio::test]
1418    #[ignore]
1419    async fn test_scrape_url() {
1420        let response = SPIDER_CLIENT
1421            .scrape_url("https://example.com", None, "application/json")
1422            .await;
1423        assert!(response.is_ok());
1424    }
1425
1426    #[tokio::test]
1427    async fn test_crawl_url() {
1428        let response = SPIDER_CLIENT
1429            .crawl_url(
1430                "https://example.com",
1431                None,
1432                false,
1433                "application/json",
1434                None::<fn(serde_json::Value)>,
1435            )
1436            .await;
1437        assert!(response.is_ok());
1438    }
1439
1440    #[tokio::test]
1441    #[ignore]
1442    async fn test_links() {
1443        let response: Result<serde_json::Value, Error> = SPIDER_CLIENT
1444            .links("https://example.com", None, false, "application/json")
1445            .await;
1446        assert!(response.is_ok());
1447    }
1448
1449    #[tokio::test]
1450    #[ignore]
1451    async fn test_ai_scrape() {
1452        let response = SPIDER_CLIENT
1453            .ai_scrape(
1454                "https://example.com",
1455                "Extract the page title",
1456                None,
1457                "application/json",
1458            )
1459            .await;
1460        assert!(response.is_ok());
1461    }
1462
1463    #[tokio::test]
1464    #[ignore]
1465    async fn test_ai_search() {
1466        let response = SPIDER_CLIENT
1467            .ai_search("Find the top sports websites", None, "application/json")
1468            .await;
1469        assert!(response.is_ok());
1470    }
1471
1472    #[tokio::test]
1473    #[ignore]
1474    async fn test_unlimited_scrape() {
1475        let response = SPIDER_CLIENT
1476            .unlimited_scrape("https://example.com", None, "application/json")
1477            .await;
1478        assert!(response.is_ok());
1479    }
1480
1481    #[tokio::test]
1482    #[ignore]
1483    async fn test_unlimited_crawl() {
1484        let response = SPIDER_CLIENT
1485            .unlimited_crawl(
1486                "https://example.com",
1487                None,
1488                false,
1489                "application/json",
1490                None::<fn(serde_json::Value)>,
1491            )
1492            .await;
1493        assert!(response.is_ok());
1494    }
1495
1496    #[tokio::test]
1497    #[ignore]
1498    async fn test_unlimited_links() {
1499        let response: Result<serde_json::Value, Error> = SPIDER_CLIENT
1500            .unlimited_links("https://example.com", None, false, "application/json")
1501            .await;
1502        assert!(response.is_ok());
1503    }
1504
1505    #[tokio::test]
1506    #[ignore]
1507    async fn test_screenshot() {
1508        let mut params = RequestParams::default();
1509        params.limit = Some(1);
1510
1511        let response = SPIDER_CLIENT
1512            .screenshot(
1513                "https://example.com",
1514                Some(params),
1515                false,
1516                "application/json",
1517            )
1518            .await;
1519        assert!(response.is_ok());
1520    }
1521
1522    // #[tokio::test(flavor = "multi_thread")]
1523    // async fn test_search() {
1524    //     let mut params = SearchRequestParams::default();
1525
1526    //     params.search_limit = Some(1);
1527    //     params.num = Some(1);
1528    //     params.fetch_page_content = Some(false);
1529
1530    //     let response = SPIDER_CLIENT
1531    //         .search("a sports website", Some(params), false, "application/json")
1532    //         .await;
1533
1534    //     assert!(response.is_ok());
1535    // }
1536
1537    #[tokio::test]
1538    #[ignore]
1539    async fn test_transform() {
1540        let data = vec![HashMap::from([(
1541            "<html><body><h1>Transformation</h1></body></html>".into(),
1542            "".into(),
1543        )])];
1544        let response = SPIDER_CLIENT
1545            .transform(data, None, false, "application/json")
1546            .await;
1547        assert!(response.is_ok());
1548    }
1549
1550    #[tokio::test]
1551    async fn test_get_credits() {
1552        let response = SPIDER_CLIENT.get_credits().await;
1553        assert!(response.is_ok());
1554    }
1555}