1pub mod browser;
64pub mod shapes;
65
66use backon::ExponentialBuilder;
67use backon::Retryable;
68use reqwest::Client;
69use reqwest::{Error, Response};
70use serde::Serialize;
71pub use browser::*;
72pub use shapes::{request::*, response::*};
73use std::collections::HashMap;
74use std::sync::atomic::{AtomicU32, Ordering};
75use std::sync::OnceLock;
76use tokio_stream::StreamExt;
77
78#[derive(Debug, Default)]
81pub struct RateLimitInfo {
82 pub limit: AtomicU32,
84 pub remaining: AtomicU32,
86 pub reset_seconds: AtomicU32,
88}
89
90impl RateLimitInfo {
91 pub fn snapshot(&self) -> (u32, u32, u32) {
93 (
94 self.limit.load(Ordering::Relaxed),
95 self.remaining.load(Ordering::Relaxed),
96 self.reset_seconds.load(Ordering::Relaxed),
97 )
98 }
99}
100
101static API_URL: OnceLock<String> = OnceLock::new();
102
103pub fn get_api_url() -> &'static str {
105 API_URL.get_or_init(|| {
106 std::env::var("SPIDER_API_URL").unwrap_or_else(|_| "https://api.spider.cloud".to_string())
107 })
108}
109
110#[derive(Debug, Default)]
112pub struct Spider {
113 pub api_key: String,
115 pub client: Client,
117 pub rate_limit: RateLimitInfo,
119}
120
121pub async fn handle_json(res: reqwest::Response) -> Result<serde_json::Value, reqwest::Error> {
123 res.json().await
124}
125
126pub async fn handle_jsonl(res: reqwest::Response) -> Result<serde_json::Value, reqwest::Error> {
128 let text = res.text().await?;
129 let lines = text
130 .lines()
131 .filter_map(|line| serde_json::from_str::<serde_json::Value>(line).ok())
132 .collect::<Vec<_>>();
133 Ok(serde_json::Value::Array(lines))
134}
135
136#[cfg(feature = "csv")]
138pub async fn handle_csv(res: reqwest::Response) -> Result<serde_json::Value, reqwest::Error> {
139 use std::collections::HashMap;
140 let text = res.text().await?;
141 let mut rdr = csv::Reader::from_reader(text.as_bytes());
142 let records: Vec<HashMap<String, String>> = rdr.deserialize().filter_map(Result::ok).collect();
143
144 if let Ok(record) = serde_json::to_value(records) {
145 Ok(record)
146 } else {
147 Ok(serde_json::Value::String(text))
148 }
149}
150
151#[cfg(not(feature = "csv"))]
152pub async fn handle_csv(res: reqwest::Response) -> Result<serde_json::Value, reqwest::Error> {
153 handle_text(res).await
154}
155
156pub async fn handle_text(res: reqwest::Response) -> Result<serde_json::Value, reqwest::Error> {
158 Ok(serde_json::Value::String(
159 res.text().await.unwrap_or_default(),
160 ))
161}
162
163#[cfg(feature = "csv")]
165pub async fn handle_xml(res: reqwest::Response) -> Result<serde_json::Value, reqwest::Error> {
166 let text = res.text().await?;
167 match quick_xml::de::from_str::<serde_json::Value>(&text) {
168 Ok(val) => Ok(val),
169 Err(_) => Ok(serde_json::Value::String(text)),
170 }
171}
172
173#[cfg(not(feature = "csv"))]
174pub async fn handle_xml(res: reqwest::Response) -> Result<serde_json::Value, reqwest::Error> {
176 handle_text(res).await
177}
178
179pub async fn parse_response(res: reqwest::Response) -> Result<serde_json::Value, reqwest::Error> {
180 let content_type = res
181 .headers()
182 .get(reqwest::header::CONTENT_TYPE)
183 .and_then(|v| v.to_str().ok())
184 .unwrap_or_default()
185 .to_ascii_lowercase();
186
187 if content_type.contains("json") && !content_type.contains("jsonl") {
188 handle_json(res).await
189 } else if content_type.contains("jsonl") || content_type.contains("ndjson") {
190 handle_jsonl(res).await
191 } else if content_type.contains("csv") {
192 handle_csv(res).await
193 } else if content_type.contains("xml") {
194 handle_xml(res).await
195 } else {
196 handle_text(res).await
197 }
198}
199
200impl Spider {
201 pub fn new(api_key: Option<String>) -> Result<Self, &'static str> {
211 let api_key = api_key.or_else(|| std::env::var("SPIDER_API_KEY").ok());
212
213 match api_key {
214 Some(key) => Ok(Self {
215 api_key: key,
216 client: Client::new(),
217 rate_limit: RateLimitInfo::default(),
218 }),
219 None => Err("No API key provided"),
220 }
221 }
222
223 pub fn new_with_client(api_key: Option<String>, client: Client) -> Result<Self, &'static str> {
234 let api_key = api_key.or_else(|| std::env::var("SPIDER_API_KEY").ok());
235
236 match api_key {
237 Some(key) => Ok(Self {
238 api_key: key,
239 client,
240 rate_limit: RateLimitInfo::default(),
241 }),
242 None => Err("No API key provided"),
243 }
244 }
245
246 fn update_rate_limit(&self, headers: &reqwest::header::HeaderMap) {
248 if let Some(v) = headers.get("RateLimit-Limit").and_then(|v| v.to_str().ok()) {
249 if let Ok(n) = v.parse::<u32>() {
250 self.rate_limit.limit.store(n, Ordering::Relaxed);
251 }
252 }
253 if let Some(v) = headers
254 .get("RateLimit-Remaining")
255 .and_then(|v| v.to_str().ok())
256 {
257 if let Ok(n) = v.parse::<u32>() {
258 self.rate_limit.remaining.store(n, Ordering::Relaxed);
259 }
260 }
261 if let Some(v) = headers.get("RateLimit-Reset").and_then(|v| v.to_str().ok()) {
262 if let Ok(n) = v.parse::<u32>() {
263 self.rate_limit.reset_seconds.store(n, Ordering::Relaxed);
264 }
265 }
266 }
267
268 async fn api_post_base(
281 &self,
282 endpoint: &str,
283 data: impl Serialize + Sized + std::fmt::Debug,
284 content_type: &str,
285 ) -> Result<Response, Error> {
286 let url: String = format!("{}/{}", get_api_url(), endpoint);
287
288 let resp = self
289 .client
290 .post(&url)
291 .header(
292 "User-Agent",
293 format!("Spider-Client/{}", env!("CARGO_PKG_VERSION")),
294 )
295 .header("Content-Type", content_type)
296 .header("Authorization", format!("Bearer {}", self.api_key))
297 .json(&data)
298 .send()
299 .await?;
300
301 self.update_rate_limit(resp.headers());
302
303 if resp.status() == reqwest::StatusCode::TOO_MANY_REQUESTS {
304 let retry_after = resp
305 .headers()
306 .get("Retry-After")
307 .and_then(|v| v.to_str().ok())
308 .and_then(|v| v.parse::<u64>().ok())
309 .unwrap_or(1);
310 tokio::time::sleep(std::time::Duration::from_secs(retry_after)).await;
311 return resp.error_for_status();
312 }
313
314 Ok(resp)
315 }
316
317 pub async fn api_post(
330 &self,
331 endpoint: &str,
332 data: impl Serialize + std::fmt::Debug + Clone + Send + Sync,
333 content_type: &str,
334 ) -> Result<Response, Error> {
335 let fetch = || async {
336 self.api_post_base(endpoint, data.to_owned(), content_type)
337 .await
338 };
339
340 fetch
341 .retry(ExponentialBuilder::default().with_max_times(5))
342 .when(|err: &reqwest::Error| {
343 if let Some(status) = err.status() {
344 status.is_server_error() || status == reqwest::StatusCode::TOO_MANY_REQUESTS
345 } else {
346 err.is_timeout()
347 }
348 })
349 .await
350 }
351
352 async fn api_get_base<T: Serialize>(
362 &self,
363 endpoint: &str,
364 query_params: Option<&T>,
365 ) -> Result<serde_json::Value, reqwest::Error> {
366 let url = format!("{}/{}", get_api_url(), endpoint);
367 let res = self
368 .client
369 .get(&url)
370 .query(&query_params)
371 .header(
372 "User-Agent",
373 format!("Spider-Client/{}", env!("CARGO_PKG_VERSION")),
374 )
375 .header("Content-Type", "application/json")
376 .header("Authorization", format!("Bearer {}", self.api_key))
377 .send()
378 .await?;
379
380 self.update_rate_limit(res.headers());
381
382 if res.status() == reqwest::StatusCode::TOO_MANY_REQUESTS {
383 let retry_after = res
384 .headers()
385 .get("Retry-After")
386 .and_then(|v| v.to_str().ok())
387 .and_then(|v| v.parse::<u64>().ok())
388 .unwrap_or(1);
389 tokio::time::sleep(std::time::Duration::from_secs(retry_after)).await;
390 return Err(res.error_for_status().unwrap_err());
391 }
392
393 parse_response(res).await
394 }
395
396 pub async fn api_get<T: Serialize>(
406 &self,
407 endpoint: &str,
408 query_params: Option<&T>,
409 ) -> Result<serde_json::Value, reqwest::Error> {
410 let fetch = || async { self.api_get_base(endpoint, query_params.to_owned()).await };
411
412 fetch
413 .retry(ExponentialBuilder::default().with_max_times(5))
414 .when(|err: &reqwest::Error| {
415 if let Some(status) = err.status() {
416 status.is_server_error() || status == reqwest::StatusCode::TOO_MANY_REQUESTS
417 } else {
418 err.is_timeout()
419 }
420 })
421 .await
422 }
423
424 async fn api_delete_base(
437 &self,
438 endpoint: &str,
439 params: Option<HashMap<String, serde_json::Value>>,
440 ) -> Result<Response, Error> {
441 let url = format!("{}/v1/{}", get_api_url(), endpoint);
442 let request_builder = self
443 .client
444 .delete(&url)
445 .header(
446 "User-Agent",
447 format!("Spider-Client/{}", env!("CARGO_PKG_VERSION")),
448 )
449 .header("Content-Type", "application/json")
450 .header("Authorization", format!("Bearer {}", self.api_key));
451
452 let request_builder = if let Some(params) = params {
453 request_builder.json(¶ms)
454 } else {
455 request_builder
456 };
457
458 request_builder.send().await
459 }
460
461 pub async fn api_delete(
474 &self,
475 endpoint: &str,
476 params: Option<HashMap<String, serde_json::Value>>,
477 ) -> Result<Response, Error> {
478 let fetch = || async { self.api_delete_base(endpoint, params.to_owned()).await };
479
480 fetch
481 .retry(ExponentialBuilder::default().with_max_times(5))
482 .when(|err: &reqwest::Error| {
483 if let Some(status) = err.status() {
484 status.is_server_error()
485 } else {
486 err.is_timeout()
487 }
488 })
489 .await
490 }
491
492 pub async fn scrape_url(
505 &self,
506 url: &str,
507 params: Option<RequestParams>,
508 content_type: &str,
509 ) -> Result<serde_json::Value, reqwest::Error> {
510 let mut data = HashMap::new();
511
512 if let Ok(params) = serde_json::to_value(params) {
513 if let Some(ref p) = params.as_object() {
514 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
515 }
516 }
517
518 if !url.is_empty() {
519 data.insert(
520 "url".to_string(),
521 serde_json::Value::String(url.to_string()),
522 );
523 }
524
525 let res = self.api_post("scrape", data, content_type).await?;
526 parse_response(res).await
527 }
528
529 pub async fn multi_scrape_url(
542 &self,
543 params: Option<Vec<RequestParams>>,
544 content_type: &str,
545 ) -> Result<serde_json::Value, reqwest::Error> {
546 let mut data = HashMap::new();
547
548 if let Ok(mut params) = serde_json::to_value(params) {
549 if let Some(obj) = params.as_object_mut() {
550 obj.insert("limit".to_string(), serde_json::Value::Number(1.into()));
551 data.extend(obj.iter().map(|(k, v)| (k.clone(), v.clone())));
552 }
553 }
554 let res = self.api_post("scrape", data, content_type).await?;
555 parse_response(res).await
556 }
557
558 pub async fn crawl_url(
572 &self,
573 url: &str,
574 params: Option<RequestParams>,
575 stream: bool,
576 content_type: &str,
577 callback: Option<impl Fn(serde_json::Value) + Send>,
578 ) -> Result<serde_json::Value, reqwest::Error> {
579 use tokio_util::codec::{FramedRead, LinesCodec};
580
581 let mut data = HashMap::new();
582
583 if let Ok(params) = serde_json::to_value(params) {
584 if let Some(ref p) = params.as_object() {
585 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
586 }
587 }
588
589 data.insert("url".into(), serde_json::Value::String(url.to_string()));
590
591 let res = self.api_post("crawl", data, content_type).await?;
592
593 if stream {
594 if let Some(callback) = callback {
595 let stream = res.bytes_stream();
596
597 let stream_reader = tokio_util::io::StreamReader::new(
598 stream
599 .map(|r| r.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))),
600 );
601
602 let mut lines = FramedRead::new(stream_reader, LinesCodec::new());
603
604 while let Some(line_result) = lines.next().await {
605 match line_result {
606 Ok(line) => match serde_json::from_str::<serde_json::Value>(&line) {
607 Ok(value) => {
608 callback(value);
609 }
610 Err(_e) => {
611 continue;
612 }
613 },
614 Err(_e) => return Ok(serde_json::Value::Null),
615 }
616 }
617
618 Ok(serde_json::Value::Null)
619 } else {
620 Ok(serde_json::Value::Null)
621 }
622 } else {
623 parse_response(res).await
624 }
625 }
626
627 pub async fn multi_crawl_url(
641 &self,
642 params: Option<Vec<RequestParams>>,
643 stream: bool,
644 content_type: &str,
645 callback: Option<impl Fn(serde_json::Value) + Send>,
646 ) -> Result<serde_json::Value, reqwest::Error> {
647 use tokio_util::codec::{FramedRead, LinesCodec};
648
649 let res = self.api_post("crawl", params, content_type).await?;
650
651 if stream {
652 if let Some(callback) = callback {
653 let stream = res.bytes_stream();
654
655 let stream_reader = tokio_util::io::StreamReader::new(
656 stream
657 .map(|r| r.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))),
658 );
659
660 let mut lines = FramedRead::new(stream_reader, LinesCodec::new());
661
662 while let Some(line_result) = lines.next().await {
663 match line_result {
664 Ok(line) => match serde_json::from_str::<serde_json::Value>(&line) {
665 Ok(value) => {
666 callback(value);
667 }
668 Err(_e) => {
669 continue;
670 }
671 },
672 Err(_e) => return Ok(serde_json::Value::Null),
673 }
674 }
675
676 Ok(serde_json::Value::Null)
677 } else {
678 Ok(serde_json::Value::Null)
679 }
680 } else {
681 parse_response(res).await
682 }
683 }
684
685 pub async fn links(
698 &self,
699 url: &str,
700 params: Option<RequestParams>,
701 _stream: bool,
702 content_type: &str,
703 ) -> Result<serde_json::Value, reqwest::Error> {
704 let mut data = HashMap::new();
705
706 if let Ok(params) = serde_json::to_value(params) {
707 if let Some(ref p) = params.as_object() {
708 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
709 }
710 }
711
712 data.insert("url".into(), serde_json::Value::String(url.to_string()));
713
714 let res = self.api_post("links", data, content_type).await?;
715 parse_response(res).await
716 }
717
718 pub async fn multi_links(
731 &self,
732 params: Option<Vec<RequestParams>>,
733 _stream: bool,
734 content_type: &str,
735 ) -> Result<serde_json::Value, reqwest::Error> {
736 let res = self.api_post("links", params, content_type).await?;
737 parse_response(res).await
738 }
739
740 pub async fn screenshot(
753 &self,
754 url: &str,
755 params: Option<RequestParams>,
756 _stream: bool,
757 content_type: &str,
758 ) -> Result<serde_json::Value, reqwest::Error> {
759 let mut data = HashMap::new();
760
761 if let Ok(params) = serde_json::to_value(params) {
762 if let Some(ref p) = params.as_object() {
763 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
764 }
765 }
766
767 data.insert("url".into(), serde_json::Value::String(url.to_string()));
768
769 let res = self.api_post("screenshot", data, content_type).await?;
770 parse_response(res).await
771 }
772
773 pub async fn multi_screenshot(
786 &self,
787 params: Option<Vec<RequestParams>>,
788 _stream: bool,
789 content_type: &str,
790 ) -> Result<serde_json::Value, reqwest::Error> {
791 let res = self.api_post("screenshot", params, content_type).await?;
792 parse_response(res).await
793 }
794
795 pub async fn search(
808 &self,
809 q: &str,
810 params: Option<SearchRequestParams>,
811 _stream: bool,
812 content_type: &str,
813 ) -> Result<serde_json::Value, reqwest::Error> {
814 let body = match params {
815 Some(mut params) => {
816 params.search = q.to_string();
817 params
818 }
819 _ => {
820 let mut params = SearchRequestParams::default();
821 params.search = q.to_string();
822 params
823 }
824 };
825
826 let res = self.api_post("search", body, content_type).await?;
827
828 parse_response(res).await
829 }
830
831 pub async fn multi_search(
844 &self,
845 params: Option<Vec<SearchRequestParams>>,
846 content_type: &str,
847 ) -> Result<serde_json::Value, reqwest::Error> {
848 let res = self.api_post("search", params, content_type).await?;
849 parse_response(res).await
850 }
851
852 pub async fn ai_crawl(
868 &self,
869 url: &str,
870 prompt: &str,
871 params: Option<RequestParams>,
872 content_type: &str,
873 ) -> Result<serde_json::Value, reqwest::Error> {
874 let mut data = HashMap::new();
875
876 if let Ok(params) = serde_json::to_value(params) {
877 if let Some(ref p) = params.as_object() {
878 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
879 }
880 }
881
882 if !url.is_empty() {
883 data.insert(
884 "url".to_string(),
885 serde_json::Value::String(url.to_string()),
886 );
887 }
888
889 data.insert(
890 "prompt".to_string(),
891 serde_json::Value::String(prompt.to_string()),
892 );
893
894 let res = self.api_post("ai/crawl", data, content_type).await?;
895 parse_response(res).await
896 }
897
898 pub async fn ai_scrape(
914 &self,
915 url: &str,
916 prompt: &str,
917 params: Option<RequestParams>,
918 content_type: &str,
919 ) -> Result<serde_json::Value, reqwest::Error> {
920 let mut data = HashMap::new();
921
922 if let Ok(params) = serde_json::to_value(params) {
923 if let Some(ref p) = params.as_object() {
924 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
925 }
926 }
927
928 if !url.is_empty() {
929 data.insert(
930 "url".to_string(),
931 serde_json::Value::String(url.to_string()),
932 );
933 }
934
935 data.insert(
936 "prompt".to_string(),
937 serde_json::Value::String(prompt.to_string()),
938 );
939
940 let res = self.api_post("ai/scrape", data, content_type).await?;
941 parse_response(res).await
942 }
943
944 pub async fn ai_search(
959 &self,
960 prompt: &str,
961 params: Option<RequestParams>,
962 content_type: &str,
963 ) -> Result<serde_json::Value, reqwest::Error> {
964 let mut data = HashMap::new();
965
966 if let Ok(params) = serde_json::to_value(params) {
967 if let Some(ref p) = params.as_object() {
968 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
969 }
970 }
971
972 data.insert(
973 "prompt".to_string(),
974 serde_json::Value::String(prompt.to_string()),
975 );
976
977 let res = self.api_post("ai/search", data, content_type).await?;
978 parse_response(res).await
979 }
980
981 pub async fn ai_browser(
997 &self,
998 url: &str,
999 prompt: &str,
1000 params: Option<RequestParams>,
1001 content_type: &str,
1002 ) -> Result<serde_json::Value, reqwest::Error> {
1003 let mut data = HashMap::new();
1004
1005 if let Ok(params) = serde_json::to_value(params) {
1006 if let Some(ref p) = params.as_object() {
1007 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1008 }
1009 }
1010
1011 if !url.is_empty() {
1012 data.insert(
1013 "url".to_string(),
1014 serde_json::Value::String(url.to_string()),
1015 );
1016 }
1017
1018 data.insert(
1019 "prompt".to_string(),
1020 serde_json::Value::String(prompt.to_string()),
1021 );
1022
1023 let res = self.api_post("ai/browser", data, content_type).await?;
1024 parse_response(res).await
1025 }
1026
1027 pub async fn ai_links(
1043 &self,
1044 url: &str,
1045 prompt: &str,
1046 params: Option<RequestParams>,
1047 content_type: &str,
1048 ) -> Result<serde_json::Value, reqwest::Error> {
1049 let mut data = HashMap::new();
1050
1051 if let Ok(params) = serde_json::to_value(params) {
1052 if let Some(ref p) = params.as_object() {
1053 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1054 }
1055 }
1056
1057 if !url.is_empty() {
1058 data.insert(
1059 "url".to_string(),
1060 serde_json::Value::String(url.to_string()),
1061 );
1062 }
1063
1064 data.insert(
1065 "prompt".to_string(),
1066 serde_json::Value::String(prompt.to_string()),
1067 );
1068
1069 let res = self.api_post("ai/links", data, content_type).await?;
1070 parse_response(res).await
1071 }
1072
1073 pub async fn unlimited_scrape(
1101 &self,
1102 url: &str,
1103 params: Option<RequestParams>,
1104 content_type: &str,
1105 ) -> Result<serde_json::Value, reqwest::Error> {
1106 let mut data = HashMap::new();
1107
1108 if let Ok(params) = serde_json::to_value(params) {
1109 if let Some(ref p) = params.as_object() {
1110 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1111 }
1112 }
1113
1114 if !url.is_empty() {
1115 data.insert(
1116 "url".to_string(),
1117 serde_json::Value::String(url.to_string()),
1118 );
1119 }
1120
1121 let res = self
1122 .api_post("unlimited/scrape", data, content_type)
1123 .await?;
1124 parse_response(res).await
1125 }
1126
1127 pub async fn unlimited_crawl(
1157 &self,
1158 url: &str,
1159 params: Option<RequestParams>,
1160 stream: bool,
1161 content_type: &str,
1162 callback: Option<impl Fn(serde_json::Value) + Send>,
1163 ) -> Result<serde_json::Value, reqwest::Error> {
1164 use tokio_util::codec::{FramedRead, LinesCodec};
1165
1166 let mut data = HashMap::new();
1167
1168 if let Ok(params) = serde_json::to_value(params) {
1169 if let Some(ref p) = params.as_object() {
1170 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1171 }
1172 }
1173
1174 data.insert("url".into(), serde_json::Value::String(url.to_string()));
1175
1176 let res = self.api_post("unlimited/crawl", data, content_type).await?;
1177
1178 if stream {
1179 if let Some(callback) = callback {
1180 let stream = res.bytes_stream();
1181
1182 let stream_reader = tokio_util::io::StreamReader::new(
1183 stream
1184 .map(|r| r.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))),
1185 );
1186
1187 let mut lines = FramedRead::new(stream_reader, LinesCodec::new());
1188
1189 while let Some(line_result) = lines.next().await {
1190 match line_result {
1191 Ok(line) => match serde_json::from_str::<serde_json::Value>(&line) {
1192 Ok(value) => {
1193 callback(value);
1194 }
1195 Err(_e) => {
1196 continue;
1197 }
1198 },
1199 Err(_e) => return Ok(serde_json::Value::Null),
1200 }
1201 }
1202
1203 Ok(serde_json::Value::Null)
1204 } else {
1205 Ok(serde_json::Value::Null)
1206 }
1207 } else {
1208 parse_response(res).await
1209 }
1210 }
1211
1212 pub async fn unlimited_links(
1241 &self,
1242 url: &str,
1243 params: Option<RequestParams>,
1244 _stream: bool,
1245 content_type: &str,
1246 ) -> Result<serde_json::Value, reqwest::Error> {
1247 let mut data = HashMap::new();
1248
1249 if let Ok(params) = serde_json::to_value(params) {
1250 if let Some(ref p) = params.as_object() {
1251 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1252 }
1253 }
1254
1255 data.insert("url".into(), serde_json::Value::String(url.to_string()));
1256
1257 let res = self.api_post("unlimited/links", data, content_type).await?;
1258 parse_response(res).await
1259 }
1260
1261 pub async fn unblock_url(
1274 &self,
1275 url: &str,
1276 params: Option<RequestParams>,
1277 content_type: &str,
1278 ) -> Result<serde_json::Value, reqwest::Error> {
1279 let mut data = HashMap::new();
1280
1281 if let Ok(params) = serde_json::to_value(params) {
1282 if let Some(ref p) = params.as_object() {
1283 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1284 }
1285 }
1286
1287 if !url.is_empty() {
1288 data.insert(
1289 "url".to_string(),
1290 serde_json::Value::String(url.to_string()),
1291 );
1292 }
1293
1294 let res = self.api_post("unblocker", data, content_type).await?;
1295 parse_response(res).await
1296 }
1297
1298 pub async fn multi_unblock_url(
1311 &self,
1312 params: Option<Vec<RequestParams>>,
1313 content_type: &str,
1314 ) -> Result<serde_json::Value, reqwest::Error> {
1315 let mut data = HashMap::new();
1316
1317 if let Ok(mut params) = serde_json::to_value(params) {
1318 if let Some(obj) = params.as_object_mut() {
1319 obj.insert("limit".to_string(), serde_json::Value::Number(1.into()));
1320 data.extend(obj.iter().map(|(k, v)| (k.clone(), v.clone())));
1321 }
1322 }
1323 let res = self.api_post("unblocker", data, content_type).await?;
1324 parse_response(res).await
1325 }
1326
1327 pub async fn transform(
1340 &self,
1341 data: Vec<HashMap<&str, &str>>,
1342 params: Option<TransformParams>,
1343 _stream: bool,
1344 content_type: &str,
1345 ) -> Result<serde_json::Value, reqwest::Error> {
1346 let mut payload = HashMap::new();
1347
1348 if let Ok(params) = serde_json::to_value(params) {
1349 if let Some(ref p) = params.as_object() {
1350 payload.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1351 }
1352 }
1353
1354 if let Ok(d) = serde_json::to_value(data) {
1355 payload.insert("data".into(), d);
1356 }
1357
1358 let res = self.api_post("transform", payload, content_type).await?;
1359
1360 parse_response(res).await
1361 }
1362
1363 pub async fn get_credits(&self) -> Result<serde_json::Value, reqwest::Error> {
1365 self.api_get::<serde_json::Value>("data/credits", None)
1366 .await
1367 }
1368
1369 pub async fn data_post(
1371 &self,
1372 table: &str,
1373 data: Option<RequestParams>,
1374 ) -> Result<serde_json::Value, reqwest::Error> {
1375 let res = self
1376 .api_post(&format!("data/{}", table), data, "application/json")
1377 .await?;
1378 parse_response(res).await
1379 }
1380
1381 pub async fn data_get(
1383 &self,
1384 table: &str,
1385 params: Option<RequestParams>,
1386 ) -> Result<serde_json::Value, reqwest::Error> {
1387 let mut payload = HashMap::new();
1388
1389 if let Some(params) = params {
1390 if let Ok(p) = serde_json::to_value(params) {
1391 if let Some(o) = p.as_object() {
1392 payload.extend(o.iter().map(|(k, v)| (k.as_str(), v.clone())));
1393 }
1394 }
1395 }
1396
1397 let res = self
1398 .api_get::<serde_json::Value>(&format!("data/{}", table), None)
1399 .await?;
1400 Ok(res)
1401 }
1402}
1403
1404#[cfg(test)]
1405mod tests {
1406 use super::*;
1407 use dotenv::dotenv;
1408 use lazy_static::lazy_static;
1409 use reqwest::ClientBuilder;
1410
1411 lazy_static! {
1412 static ref SPIDER_CLIENT: Spider = {
1413 dotenv().ok();
1414 let client = ClientBuilder::new();
1415 let client = client.user_agent("SpiderBot").build().unwrap();
1416
1417 Spider::new_with_client(None, client).expect("client to build")
1418 };
1419 }
1420
1421 #[tokio::test]
1422 #[ignore]
1423 async fn test_scrape_url() {
1424 let response = SPIDER_CLIENT
1425 .scrape_url("https://example.com", None, "application/json")
1426 .await;
1427 assert!(response.is_ok());
1428 }
1429
1430 #[tokio::test]
1431 async fn test_crawl_url() {
1432 let response = SPIDER_CLIENT
1433 .crawl_url(
1434 "https://example.com",
1435 None,
1436 false,
1437 "application/json",
1438 None::<fn(serde_json::Value)>,
1439 )
1440 .await;
1441 assert!(response.is_ok());
1442 }
1443
1444 #[tokio::test]
1445 #[ignore]
1446 async fn test_links() {
1447 let response: Result<serde_json::Value, Error> = SPIDER_CLIENT
1448 .links("https://example.com", None, false, "application/json")
1449 .await;
1450 assert!(response.is_ok());
1451 }
1452
1453 #[tokio::test]
1454 #[ignore]
1455 async fn test_ai_scrape() {
1456 let response = SPIDER_CLIENT
1457 .ai_scrape(
1458 "https://example.com",
1459 "Extract the page title",
1460 None,
1461 "application/json",
1462 )
1463 .await;
1464 assert!(response.is_ok());
1465 }
1466
1467 #[tokio::test]
1468 #[ignore]
1469 async fn test_ai_search() {
1470 let response = SPIDER_CLIENT
1471 .ai_search("Find the top sports websites", None, "application/json")
1472 .await;
1473 assert!(response.is_ok());
1474 }
1475
1476 #[tokio::test]
1477 #[ignore]
1478 async fn test_unlimited_scrape() {
1479 let response = SPIDER_CLIENT
1480 .unlimited_scrape("https://example.com", None, "application/json")
1481 .await;
1482 assert!(response.is_ok());
1483 }
1484
1485 #[tokio::test]
1486 #[ignore]
1487 async fn test_unlimited_crawl() {
1488 let response = SPIDER_CLIENT
1489 .unlimited_crawl(
1490 "https://example.com",
1491 None,
1492 false,
1493 "application/json",
1494 None::<fn(serde_json::Value)>,
1495 )
1496 .await;
1497 assert!(response.is_ok());
1498 }
1499
1500 #[tokio::test]
1501 #[ignore]
1502 async fn test_unlimited_links() {
1503 let response: Result<serde_json::Value, Error> = SPIDER_CLIENT
1504 .unlimited_links("https://example.com", None, false, "application/json")
1505 .await;
1506 assert!(response.is_ok());
1507 }
1508
1509 #[tokio::test]
1510 #[ignore]
1511 async fn test_screenshot() {
1512 let mut params = RequestParams::default();
1513 params.limit = Some(1);
1514
1515 let response = SPIDER_CLIENT
1516 .screenshot(
1517 "https://example.com",
1518 Some(params),
1519 false,
1520 "application/json",
1521 )
1522 .await;
1523 assert!(response.is_ok());
1524 }
1525
1526 #[tokio::test]
1542 #[ignore]
1543 async fn test_transform() {
1544 let data = vec![HashMap::from([(
1545 "<html><body><h1>Transformation</h1></body></html>".into(),
1546 "".into(),
1547 )])];
1548 let response = SPIDER_CLIENT
1549 .transform(data, None, false, "application/json")
1550 .await;
1551 assert!(response.is_ok());
1552 }
1553
1554 #[tokio::test]
1555 async fn test_get_credits() {
1556 let response = SPIDER_CLIENT.get_credits().await;
1557 assert!(response.is_ok());
1558 }
1559}