1pub 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#[derive(Debug, Default)]
79pub struct RateLimitInfo {
80 pub limit: AtomicU32,
82 pub remaining: AtomicU32,
84 pub reset_seconds: AtomicU32,
86}
87
88impl RateLimitInfo {
89 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
101pub 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#[derive(Debug, Default)]
110pub struct Spider {
111 pub api_key: String,
113 pub client: Client,
115 pub rate_limit: RateLimitInfo,
117}
118
119pub async fn handle_json(res: reqwest::Response) -> Result<serde_json::Value, reqwest::Error> {
121 res.json().await
122}
123
124pub 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#[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
154pub 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#[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"))]
172pub 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 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 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 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 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 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 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 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 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(¶ms)
452 } else {
453 request_builder
454 };
455
456 request_builder.send().await
457 }
458
459 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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]
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}