1use backon::ExponentialBuilder;
65use backon::Retryable;
66use reqwest::Client;
67use reqwest::{Error, Response};
68use serde::{Deserialize, Serialize};
69use std::collections::HashMap;
70use tokio_stream::StreamExt;
71
72#[derive(Debug, Deserialize, Serialize, Clone)]
74pub struct ChunkingAlgDict {
75 r#type: ChunkingType,
77 value: i32,
79}
80
81#[derive(Serialize, Deserialize, Debug, Clone)]
83pub struct Timeout {
84 pub secs: u64,
86 pub nanos: u32,
88}
89
90#[derive(Serialize, Deserialize, Debug, Clone)]
91pub struct IdleNetwork {
92 pub timeout: Timeout,
94}
95
96#[derive(Serialize, Deserialize, Debug, Clone)]
97#[serde(tag = "type", rename_all = "PascalCase")]
98pub enum WebAutomation {
99 Evaluate { code: String },
100 Click { selector: String },
101 Wait { duration: u64 },
102 WaitForNavigation,
103 WaitFor { selector: String },
104 WaitForAndClick { selector: String },
105 ScrollX { pixels: i32 },
106 ScrollY { pixels: i32 },
107 Fill { selector: String, value: String },
108 InfiniteScroll { times: u32 },
109}
110
111#[derive(Default, Serialize, Deserialize, Debug, Clone)]
112#[serde(tag = "type", rename_all = "PascalCase")]
113pub enum RedirectPolicy {
114 Loose,
115 #[default]
116 Strict,
117}
118
119pub type WebAutomationMap = std::collections::HashMap<String, Vec<WebAutomation>>;
120pub type ExecutionScriptsMap = std::collections::HashMap<String, String>;
121
122#[derive(Serialize, Deserialize, Debug, Clone)]
123pub struct Selector {
124 pub timeout: Timeout,
126 pub selector: String,
128}
129
130#[derive(Serialize, Deserialize, Debug, Clone)]
131pub struct Delay {
132 pub timeout: Timeout,
134}
135
136#[derive(Serialize, Deserialize, Debug, Clone)]
137pub struct WaitFor {
138 pub idle_network: Option<IdleNetwork>,
140 pub selector: Option<Selector>,
142 pub delay: Option<Delay>,
144 pub page_navigations: Option<bool>,
146}
147
148#[derive(Serialize, Deserialize, Debug, Clone, Default)]
150pub struct QueryRequest {
151 pub url: Option<String>,
153 pub domain: Option<String>,
155 pub pathname: Option<String>,
157}
158
159#[derive(Default, Debug, Deserialize, Serialize, Clone)]
161#[serde(rename_all = "lowercase")]
162pub enum ChunkingType {
163 #[default]
164 ByWords,
166 ByLines,
168 ByCharacterLength,
170 BySentence,
172}
173
174#[derive(Default, Debug, Deserialize, Serialize, Clone)]
175pub struct Viewport {
177 pub width: u32,
179 pub height: u32,
181 pub device_scale_factor: Option<f64>,
183 pub emulating_mobile: bool,
185 pub is_landscape: bool,
187 pub has_touch: bool,
189}
190
191const API_URL: &'static str = "https://api.spider.cloud";
193
194#[derive(Debug, Clone, Default, Deserialize, Serialize)]
196pub struct CSSSelector {
197 pub name: String,
199 pub selectors: Vec<String>,
201}
202
203pub type CSSExtractionMap = HashMap<String, Vec<CSSSelector>>;
205
206#[derive(Debug, Default, Deserialize, Serialize, Clone)]
208pub struct WebhookSettings {
209 destination: String,
211 on_credits_depleted: bool,
213 on_credits_half_depleted: bool,
215 on_website_status: bool,
217 on_find: bool,
219 on_find_metadata: bool,
221}
222
223#[derive(Debug, Deserialize, Serialize, Clone)]
225#[serde(untagged)]
226pub enum ReturnFormatHandling {
227 Single(ReturnFormat),
229 Multi(std::collections::HashSet<ReturnFormat>),
231}
232
233impl Default for ReturnFormatHandling {
234 fn default() -> ReturnFormatHandling {
235 ReturnFormatHandling::Single(ReturnFormat::Raw)
236 }
237}
238
239#[derive(Debug, Default, Deserialize, Serialize, Clone)]
241pub struct RequestParams {
242 #[serde(default)]
243 pub url: Option<String>,
245 #[serde(default)]
246 pub request: Option<RequestType>,
248 #[serde(default)]
249 pub limit: Option<u32>,
251 #[serde(default)]
252 pub return_format: Option<ReturnFormatHandling>,
254 #[serde(default)]
255 pub tld: Option<bool>,
257 #[serde(default)]
258 pub depth: Option<u32>,
260 #[serde(default)]
261 pub cache: Option<bool>,
263 #[serde(default)]
264 pub scroll: Option<u32>,
266 #[serde(default)]
267 pub budget: Option<HashMap<String, u32>>,
269 #[serde(default)]
270 pub blacklist: Option<Vec<String>>,
272 #[serde(default)]
273 pub whitelist: Option<Vec<String>>,
275 #[serde(default)]
276 pub locale: Option<String>,
278 #[serde(default)]
279 pub cookies: Option<String>,
281 #[serde(default)]
282 pub stealth: Option<bool>,
284 #[serde(default)]
285 pub headers: Option<HashMap<String, String>>,
287 #[serde(default)]
288 pub anti_bot: Option<bool>,
290 #[serde(default)]
291 pub webhooks: Option<WebhookSettings>,
293 #[serde(default)]
294 pub metadata: Option<bool>,
296 #[serde(default)]
297 pub viewport: Option<Viewport>,
299 #[serde(default)]
300 pub encoding: Option<String>,
302 #[serde(default)]
303 pub subdomains: Option<bool>,
305 #[serde(default)]
306 pub user_agent: Option<String>,
308 #[serde(default)]
309 pub store_data: Option<bool>,
311 #[serde(default)]
312 pub gpt_config: Option<HashMap<String, String>>,
314 #[serde(default)]
315 pub fingerprint: Option<bool>,
317 #[serde(default)]
318 pub storageless: Option<bool>,
320 #[serde(default)]
321 pub readability: Option<bool>,
323 #[serde(default)]
324 pub proxy_enabled: Option<bool>,
326 #[serde(default)]
327 pub respect_robots: Option<bool>,
329 #[serde(default)]
330 pub root_selector: Option<String>,
332 #[serde(default)]
333 pub full_resources: Option<bool>,
335 #[serde(default)]
336 pub text: Option<String>,
338 #[serde(default)]
339 pub sitemap: Option<bool>,
341 #[serde(default)]
342 pub external_domains: Option<Vec<String>>,
344 #[serde(default)]
345 pub return_embeddings: Option<bool>,
347 #[serde(default)]
348 pub return_headers: Option<bool>,
350 #[serde(default)]
351 pub return_page_links: Option<bool>,
353 #[serde(default)]
354 pub return_cookies: Option<bool>,
356 #[serde(default)]
357 pub request_timeout: Option<u8>,
359 #[serde(default)]
360 pub run_in_background: Option<bool>,
362 #[serde(default)]
363 pub skip_config_checks: Option<bool>,
365 #[serde(default)]
366 pub css_extraction_map: Option<CSSExtractionMap>,
368 #[serde(default)]
369 pub chunking_alg: Option<ChunkingAlgDict>,
371 pub disable_intercept: Option<bool>,
373 pub wait_for: Option<WaitFor>,
375 pub execution_scripts: Option<ExecutionScriptsMap>,
377 pub automation_scripts: Option<WebAutomationMap>,
379 pub redirect_policy: Option<RedirectPolicy>,
381}
382
383#[derive(Debug, Default, Deserialize, Serialize, Clone)]
385pub struct SearchRequestParams {
386 #[serde(default, flatten)]
388 pub base: RequestParams,
389 pub search: String,
391 pub search_limit: Option<u32>,
393 pub fetch_page_content: Option<bool>,
395 pub location: Option<String>,
397 pub country: Option<String>,
399 pub language: Option<String>,
401 pub num: Option<u32>,
403 pub page: Option<u32>,
405 #[serde(default)]
406 pub website_limit: Option<u32>,
408}
409
410#[derive(Debug, Default, Deserialize, Serialize, Clone)]
412pub struct TransformParams {
413 #[serde(default)]
414 pub return_format: Option<ReturnFormat>,
416 #[serde(default)]
417 pub readability: Option<bool>,
419 #[serde(default)]
420 pub clean: Option<bool>,
422 #[serde(default)]
423 pub clean_full: Option<bool>,
425 pub data: Vec<DataParam>,
427}
428
429#[derive(Serialize, Deserialize, Debug, Clone)]
430pub struct DataParam {
431 pub html: String,
433 pub url: Option<String>,
435}
436
437#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
439#[serde(rename_all = "lowercase")]
440pub enum RequestType {
441 Http,
443 Chrome,
445 #[default]
446 SmartMode,
448}
449
450#[derive(Default, Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Hash)]
452#[serde(rename_all = "lowercase")]
453pub enum ReturnFormat {
454 #[default]
455 Raw,
457 Markdown,
459 Commonmark,
461 Html2text,
463 Text,
465 Xml,
467 Bytes,
469}
470
471#[derive(Debug, Default)]
473pub struct Spider {
474 pub api_key: String,
476 pub client: Client,
478}
479
480impl Spider {
481 pub fn new(api_key: Option<String>) -> Result<Self, &'static str> {
491 let api_key = api_key.or_else(|| std::env::var("SPIDER_API_KEY").ok());
492
493 match api_key {
494 Some(key) => Ok(Self {
495 api_key: key,
496 client: Client::new(),
497 }),
498 None => Err("No API key provided"),
499 }
500 }
501
502 pub fn new_with_client(api_key: Option<String>, client: Client) -> Result<Self, &'static str> {
513 let api_key = api_key.or_else(|| std::env::var("SPIDER_API_KEY").ok());
514
515 match api_key {
516 Some(key) => Ok(Self {
517 api_key: key,
518 client,
519 }),
520 None => Err("No API key provided"),
521 }
522 }
523
524 async fn api_post_base(
537 &self,
538 endpoint: &str,
539 data: impl Serialize + Sized + std::fmt::Debug,
540 content_type: &str,
541 ) -> Result<Response, Error> {
542 let url: String = format!("{API_URL}/{}", endpoint);
543
544 self.client
545 .post(&url)
546 .header(
547 "User-Agent",
548 format!("Spider-Client/{}", env!("CARGO_PKG_VERSION")),
549 )
550 .header("Content-Type", content_type)
551 .header("Authorization", format!("Bearer {}", self.api_key))
552 .json(&data)
553 .send()
554 .await
555 }
556
557 async fn api_post(
570 &self,
571 endpoint: &str,
572 data: impl Serialize + std::fmt::Debug + Clone + Send + Sync,
573 content_type: &str,
574 ) -> Result<Response, Error> {
575 let fetch = || async {
576 self.api_post_base(endpoint, data.to_owned(), content_type)
577 .await
578 };
579
580 fetch
581 .retry(ExponentialBuilder::default().with_max_times(5))
582 .when(|err: &reqwest::Error| {
583 if let Some(status) = err.status() {
584 status.is_server_error()
585 } else {
586 err.is_timeout()
587 }
588 })
589 .await
590 }
591
592 async fn api_get_base<T: Serialize>(
602 &self,
603 endpoint: &str,
604 query_params: Option<&T>,
605 ) -> Result<serde_json::Value, reqwest::Error> {
606 let url = format!("{API_URL}/{}", endpoint);
607 let res = self
608 .client
609 .get(&url)
610 .query(&query_params)
611 .header(
612 "User-Agent",
613 format!("Spider-Client/{}", env!("CARGO_PKG_VERSION")),
614 )
615 .header("Content-Type", "application/json")
616 .header("Authorization", format!("Bearer {}", self.api_key))
617 .send()
618 .await?;
619 res.json().await
620 }
621
622 async fn api_get<T: Serialize>(
632 &self,
633 endpoint: &str,
634 query_params: Option<&T>,
635 ) -> Result<serde_json::Value, reqwest::Error> {
636 let fetch = || async { self.api_get_base(endpoint, query_params.to_owned()).await };
637
638 fetch
639 .retry(ExponentialBuilder::default().with_max_times(5))
640 .when(|err: &reqwest::Error| {
641 if let Some(status) = err.status() {
642 status.is_server_error()
643 } else {
644 err.is_timeout()
645 }
646 })
647 .await
648 }
649
650 async fn api_delete_base(
663 &self,
664 endpoint: &str,
665 params: Option<HashMap<String, serde_json::Value>>,
666 ) -> Result<Response, Error> {
667 let url = format!("{API_URL}/v1/{}", endpoint);
668 let request_builder = self
669 .client
670 .delete(&url)
671 .header(
672 "User-Agent",
673 format!("Spider-Client/{}", env!("CARGO_PKG_VERSION")),
674 )
675 .header("Content-Type", "application/json")
676 .header("Authorization", format!("Bearer {}", self.api_key));
677
678 let request_builder = if let Some(params) = params {
679 request_builder.json(¶ms)
680 } else {
681 request_builder
682 };
683
684 request_builder.send().await
685 }
686
687 async fn api_delete(
700 &self,
701 endpoint: &str,
702 params: Option<HashMap<String, serde_json::Value>>,
703 ) -> Result<Response, Error> {
704 let fetch = || async { self.api_delete_base(endpoint, params.to_owned()).await };
705
706 fetch
707 .retry(ExponentialBuilder::default().with_max_times(5))
708 .when(|err: &reqwest::Error| {
709 if let Some(status) = err.status() {
710 status.is_server_error()
711 } else {
712 err.is_timeout()
713 }
714 })
715 .await
716 }
717
718 pub async fn scrape_url(
731 &self,
732 url: &str,
733 params: Option<RequestParams>,
734 content_type: &str,
735 ) -> Result<serde_json::Value, reqwest::Error> {
736 let mut data = HashMap::new();
737
738 data.insert(
739 "url".to_string(),
740 serde_json::Value::String(url.to_string()),
741 );
742 data.insert("limit".to_string(), serde_json::Value::Number(1.into()));
743
744 if let Ok(params) = serde_json::to_value(params) {
745 if let Some(ref p) = params.as_object() {
746 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
747 }
748 }
749
750 let res = self.api_post("crawl", data, content_type).await?;
751 res.json().await
752 }
753
754 pub async fn crawl_url(
768 &self,
769 url: &str,
770 params: Option<RequestParams>,
771 stream: bool,
772 content_type: &str,
773 callback: Option<impl Fn(serde_json::Value) + Send>,
774 ) -> Result<serde_json::Value, reqwest::Error> {
775 use tokio_util::codec::{FramedRead, LinesCodec};
776
777 let mut data = HashMap::new();
778
779 if let Ok(params) = serde_json::to_value(params) {
780 if let Some(ref p) = params.as_object() {
781 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
782 }
783 }
784
785 data.insert("url".into(), serde_json::Value::String(url.to_string()));
786
787 let res = self.api_post("crawl", data, content_type).await?;
788
789 if stream {
790 if let Some(callback) = callback {
791 let stream = res.bytes_stream();
792
793 let stream_reader = tokio_util::io::StreamReader::new(
794 stream.map(|r| r.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))),
795 );
796
797 let mut lines = FramedRead::new(stream_reader, LinesCodec::new());
798
799 while let Some(line_result) = lines.next().await {
800 match line_result {
801 Ok(line) => {
802 match serde_json::from_str::<serde_json::Value>(&line) {
803 Ok(value) => {
804 callback(value);
805 }
806 Err(_e) => {
807 continue;
808 }
809 }
810 }
811 Err(_e) => {
812 return Ok(serde_json::Value::Null)
813 }
814 }
815 }
816
817 Ok(serde_json::Value::Null)
818 } else {
819 Ok(serde_json::Value::Null)
820 }
821 } else {
822 res.json().await
823 }
824 }
825
826 pub async fn links(
839 &self,
840 url: &str,
841 params: Option<RequestParams>,
842 _stream: bool,
843 content_type: &str,
844 ) -> Result<serde_json::Value, reqwest::Error> {
845 let mut data = HashMap::new();
846
847 if let Ok(params) = serde_json::to_value(params) {
848 if let Some(ref p) = params.as_object() {
849 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
850 }
851 }
852
853 data.insert("url".into(), serde_json::Value::String(url.to_string()));
854
855 let res = self.api_post("links", data, content_type).await?;
856 res.json().await
857 }
858
859 pub async fn screenshot(
872 &self,
873 url: &str,
874 params: Option<RequestParams>,
875 _stream: bool,
876 content_type: &str,
877 ) -> Result<serde_json::Value, reqwest::Error> {
878 let mut data = HashMap::new();
879
880 if let Ok(params) = serde_json::to_value(params) {
881 if let Some(ref p) = params.as_object() {
882 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
883 }
884 }
885
886 data.insert("url".into(), serde_json::Value::String(url.to_string()));
887
888 let res = self.api_post("screenshot", data, content_type).await?;
889 res.json().await
890 }
891
892 pub async fn search(
905 &self,
906 q: &str,
907 params: Option<SearchRequestParams>,
908 _stream: bool,
909 content_type: &str,
910 ) -> Result<serde_json::Value, reqwest::Error> {
911 let body = match params {
912 Some(mut params) => {
913 params.search = q.to_string();
914 params
915 }
916 _ => {
917 let mut params = SearchRequestParams::default();
918 params.search = q.to_string();
919 params
920 }
921 };
922
923 let res = self.api_post("search", body, content_type).await?;
924
925 res.json().await
926 }
927
928 pub async fn transform(
941 &self,
942 data: Vec<HashMap<&str, &str>>,
943 params: Option<TransformParams>,
944 _stream: bool,
945 content_type: &str,
946 ) -> Result<serde_json::Value, reqwest::Error> {
947 let mut payload = HashMap::new();
948
949 if let Ok(params) = serde_json::to_value(params) {
950 if let Some(ref p) = params.as_object() {
951 payload.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
952 }
953 }
954
955 if let Ok(d) = serde_json::to_value(data) {
956 payload.insert("data".into(), d);
957 }
958
959 let res = self.api_post("transform", payload, content_type).await?;
960
961 res.json().await
962 }
963
964 pub async fn extract_contacts(
977 &self,
978 url: &str,
979 params: Option<RequestParams>,
980 _stream: bool,
981 content_type: &str,
982 ) -> Result<serde_json::Value, reqwest::Error> {
983 let mut data = HashMap::new();
984
985 if let Ok(params) = serde_json::to_value(params) {
986 if let Ok(params) = serde_json::to_value(params) {
987 if let Some(ref p) = params.as_object() {
988 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
989 }
990 }
991 }
992
993 match serde_json::to_value(url) {
994 Ok(u) => {
995 data.insert("url".into(), u);
996 }
997 _ => (),
998 }
999
1000 let res = self
1001 .api_post("pipeline/extract-contacts", data, content_type)
1002 .await?;
1003 res.json().await
1004 }
1005
1006 pub async fn label(
1019 &self,
1020 url: &str,
1021 params: Option<RequestParams>,
1022 _stream: bool,
1023 content_type: &str,
1024 ) -> Result<serde_json::Value, reqwest::Error> {
1025 let mut data = HashMap::new();
1026
1027 if let Ok(params) = serde_json::to_value(params) {
1028 if let Ok(params) = serde_json::to_value(params) {
1029 if let Some(ref p) = params.as_object() {
1030 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1031 }
1032 }
1033 }
1034
1035 data.insert("url".into(), serde_json::Value::String(url.to_string()));
1036
1037 let res = self.api_post("pipeline/label", data, content_type).await?;
1038 res.json().await
1039 }
1040
1041 pub async fn download(
1053 &self,
1054 url: Option<&str>,
1055 options: Option<HashMap<&str, i32>>,
1056 ) -> Result<reqwest::Response, reqwest::Error> {
1057 let mut params = HashMap::new();
1058
1059 if let Some(url) = url {
1060 params.insert("url".to_string(), url.to_string());
1061 }
1062
1063 if let Some(options) = options {
1064 for (key, value) in options {
1065 params.insert(key.to_string(), value.to_string());
1066 }
1067 }
1068
1069 let url = format!("{API_URL}/v1/data/download");
1070 let request = self
1071 .client
1072 .get(&url)
1073 .header(
1074 "User-Agent",
1075 format!("Spider-Client/{}", env!("CARGO_PKG_VERSION")),
1076 )
1077 .header("Content-Type", "application/octet-stream")
1078 .header("Authorization", format!("Bearer {}", self.api_key))
1079 .query(¶ms);
1080
1081 let res = request.send().await?;
1082
1083 Ok(res)
1084 }
1085
1086 pub async fn create_signed_url(
1098 &self,
1099 url: Option<&str>,
1100 options: Option<HashMap<&str, i32>>,
1101 ) -> Result<serde_json::Value, reqwest::Error> {
1102 let mut params = HashMap::new();
1103
1104 if let Some(options) = options {
1105 for (key, value) in options {
1106 params.insert(key.to_string(), value.to_string());
1107 }
1108 }
1109
1110 if let Some(url) = url {
1111 params.insert("url".to_string(), url.to_string());
1112 }
1113
1114 let url = format!("{API_URL}/v1/data/sign-url");
1115 let request = self
1116 .client
1117 .get(&url)
1118 .header(
1119 "User-Agent",
1120 format!("Spider-Client/{}", env!("CARGO_PKG_VERSION")),
1121 )
1122 .header("Authorization", format!("Bearer {}", self.api_key))
1123 .query(¶ms);
1124
1125 let res = request.send().await?;
1126
1127 res.json().await
1128 }
1129
1130 pub async fn get_crawl_state(
1142 &self,
1143 url: &str,
1144 params: Option<RequestParams>,
1145 content_type: &str,
1146 ) -> Result<serde_json::Value, reqwest::Error> {
1147 let mut payload = HashMap::new();
1148 payload.insert("url".into(), serde_json::Value::String(url.to_string()));
1149 payload.insert(
1150 "contentType".into(),
1151 serde_json::Value::String(content_type.to_string()),
1152 );
1153
1154 if let Ok(params) = serde_json::to_value(params) {
1155 if let Ok(params) = serde_json::to_value(params) {
1156 if let Some(ref p) = params.as_object() {
1157 payload.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1158 }
1159 }
1160 }
1161
1162 let res = self
1163 .api_post("data/crawl_state", payload, content_type)
1164 .await?;
1165 res.json().await
1166 }
1167
1168 pub async fn get_credits(&self) -> Result<serde_json::Value, reqwest::Error> {
1170 self.api_get::<serde_json::Value>("data/credits", None)
1171 .await
1172 }
1173
1174 pub async fn data_post(
1176 &self,
1177 table: &str,
1178 data: Option<RequestParams>,
1179 ) -> Result<serde_json::Value, reqwest::Error> {
1180 let res = self
1181 .api_post(&format!("data/{}", table), data, "application/json")
1182 .await?;
1183 res.json().await
1184 }
1185
1186 pub async fn query(&self, params: &QueryRequest) -> Result<serde_json::Value, reqwest::Error> {
1188 let res = self
1189 .api_get::<QueryRequest>(&"data/query", Some(params))
1190 .await?;
1191
1192 Ok(res)
1193 }
1194
1195 pub async fn data_get(
1197 &self,
1198 table: &str,
1199 params: Option<RequestParams>,
1200 ) -> Result<serde_json::Value, reqwest::Error> {
1201 let mut payload = HashMap::new();
1202
1203 if let Some(params) = params {
1204 if let Ok(p) = serde_json::to_value(params) {
1205 if let Some(o) = p.as_object() {
1206 payload.extend(o.iter().map(|(k, v)| (k.as_str(), v.clone())));
1207 }
1208 }
1209 }
1210
1211 let res = self
1212 .api_get::<serde_json::Value>(&format!("data/{}", table), None)
1213 .await?;
1214 Ok(res)
1215 }
1216
1217 pub async fn data_delete(
1219 &self,
1220 table: &str,
1221 params: Option<RequestParams>,
1222 ) -> Result<serde_json::Value, reqwest::Error> {
1223 let mut payload = HashMap::new();
1224
1225 if let Ok(params) = serde_json::to_value(params) {
1226 if let Ok(params) = serde_json::to_value(params) {
1227 if let Some(ref p) = params.as_object() {
1228 payload.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1229 }
1230 }
1231 }
1232
1233 let res = self
1234 .api_delete(&format!("data/{}", table), Some(payload))
1235 .await?;
1236 res.json().await
1237 }
1238}
1239
1240#[cfg(test)]
1241mod tests {
1242 use super::*;
1243 use dotenv::dotenv;
1244 use lazy_static::lazy_static;
1245 use reqwest::ClientBuilder;
1246
1247 lazy_static! {
1248 static ref SPIDER_CLIENT: Spider = {
1249 dotenv().ok();
1250 let client = ClientBuilder::new();
1251 let client = client.user_agent("SpiderBot").build().unwrap();
1252
1253 Spider::new_with_client(None, client).expect("client to build")
1254 };
1255 }
1256
1257 #[tokio::test]
1258 #[ignore]
1259 async fn test_scrape_url() {
1260 let response = SPIDER_CLIENT
1261 .scrape_url("https://example.com", None, "application/json")
1262 .await;
1263 assert!(response.is_ok());
1264 }
1265
1266 #[tokio::test]
1267 async fn test_crawl_url() {
1268 let response = SPIDER_CLIENT
1269 .crawl_url(
1270 "https://example.com",
1271 None,
1272 false,
1273 "application/json",
1274 None::<fn(serde_json::Value)>,
1275 )
1276 .await;
1277 assert!(response.is_ok());
1278 }
1279
1280 #[tokio::test]
1281 #[ignore]
1282 async fn test_links() {
1283 let response: Result<serde_json::Value, Error> = SPIDER_CLIENT
1284 .links("https://example.com", None, false, "application/json")
1285 .await;
1286 assert!(response.is_ok());
1287 }
1288
1289 #[tokio::test]
1290 #[ignore]
1291 async fn test_screenshot() {
1292 let mut params = RequestParams::default();
1293 params.limit = Some(1);
1294
1295 let response = SPIDER_CLIENT
1296 .screenshot(
1297 "https://example.com",
1298 Some(params),
1299 false,
1300 "application/json",
1301 )
1302 .await;
1303 assert!(response.is_ok());
1304 }
1305
1306 #[tokio::test]
1322 #[ignore]
1323 async fn test_transform() {
1324 let data = vec![HashMap::from([(
1325 "<html><body><h1>Transformation</h1></body></html>".into(),
1326 "".into(),
1327 )])];
1328 let response = SPIDER_CLIENT
1329 .transform(data, None, false, "application/json")
1330 .await;
1331 assert!(response.is_ok());
1332 }
1333
1334 #[tokio::test]
1335 #[ignore]
1336 async fn test_extract_contacts() {
1337 let response = SPIDER_CLIENT
1338 .extract_contacts("https://example.com", None, false, "application/json")
1339 .await;
1340 assert!(response.is_ok());
1341 }
1342
1343 #[tokio::test]
1344 #[ignore]
1345 async fn test_label() {
1346 let response = SPIDER_CLIENT
1347 .label("https://example.com", None, false, "application/json")
1348 .await;
1349 assert!(response.is_ok());
1350 }
1351
1352 #[tokio::test]
1353 async fn test_create_signed_url() {
1354 let response = SPIDER_CLIENT
1355 .create_signed_url(Some("example.com"), None)
1356 .await;
1357 assert!(response.is_ok());
1358 }
1359
1360 #[tokio::test]
1361 async fn test_get_crawl_state() {
1362 let response = SPIDER_CLIENT
1363 .get_crawl_state("https://example.com", None, "application/json")
1364 .await;
1365 assert!(response.is_ok());
1366 }
1367
1368 #[tokio::test]
1369 async fn test_query() {
1370 let mut query = QueryRequest::default();
1371
1372 query.domain = Some("spider.cloud".into());
1373
1374 let response = SPIDER_CLIENT.query(&query).await;
1375 assert!(response.is_ok());
1376 }
1377
1378 #[tokio::test]
1379 async fn test_get_credits() {
1380 let response = SPIDER_CLIENT.get_credits().await;
1381 assert!(response.is_ok());
1382 }
1383}