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 let mut data = HashMap::new();
776
777 if let Ok(params) = serde_json::to_value(params) {
778 if let Some(ref p) = params.as_object() {
779 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
780 }
781 }
782
783 data.insert("url".into(), serde_json::Value::String(url.to_string()));
784
785 let res = self.api_post("crawl", data, content_type).await?;
786
787 if stream {
788 if let Some(callback) = callback {
789 use bytes::Buf;
790 let mut stream = res.bytes_stream();
791 let mut buffer = bytes::BytesMut::new();
792
793 while let Some(chunk_result) = stream.next().await {
794 if let Ok(chunk) = chunk_result {
795 buffer.extend_from_slice(&chunk);
796
797 loop {
798 let mut stream_de = serde_json::Deserializer::from_slice(&buffer)
799 .into_iter::<serde_json::Value>();
800
801 match stream_de.next() {
802 Some(Ok(value)) => {
803 let offset = stream_de.byte_offset();
804 buffer.advance(offset);
805 callback(value);
806 }
807 Some(Err(e)) if e.is_eof() => break, Some(Err(_e)) => return Ok(Default::default()),
809 None => break,
810 }
811 }
812 }
813 }
814
815 Ok(serde_json::Value::Null)
816 } else {
817 Ok(serde_json::Value::Null)
818 }
819 } else {
820 res.json().await
821 }
822 }
823
824 pub async fn links(
837 &self,
838 url: &str,
839 params: Option<RequestParams>,
840 _stream: bool,
841 content_type: &str,
842 ) -> Result<serde_json::Value, reqwest::Error> {
843 let mut data = HashMap::new();
844
845 if let Ok(params) = serde_json::to_value(params) {
846 if let Some(ref p) = params.as_object() {
847 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
848 }
849 }
850
851 data.insert("url".into(), serde_json::Value::String(url.to_string()));
852
853 let res = self.api_post("links", data, content_type).await?;
854 res.json().await
855 }
856
857 pub async fn screenshot(
870 &self,
871 url: &str,
872 params: Option<RequestParams>,
873 _stream: bool,
874 content_type: &str,
875 ) -> Result<serde_json::Value, reqwest::Error> {
876 let mut data = HashMap::new();
877
878 if let Ok(params) = serde_json::to_value(params) {
879 if let Some(ref p) = params.as_object() {
880 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
881 }
882 }
883
884 data.insert("url".into(), serde_json::Value::String(url.to_string()));
885
886 let res = self.api_post("screenshot", data, content_type).await?;
887 res.json().await
888 }
889
890 pub async fn search(
903 &self,
904 q: &str,
905 params: Option<SearchRequestParams>,
906 _stream: bool,
907 content_type: &str,
908 ) -> Result<serde_json::Value, reqwest::Error> {
909 let body = match params {
910 Some(mut params) => {
911 params.search = q.to_string();
912 params
913 }
914 _ => {
915 let mut params = SearchRequestParams::default();
916 params.search = q.to_string();
917 params
918 }
919 };
920
921 let res = self.api_post("search", body, content_type).await?;
922
923 res.json().await
924 }
925
926 pub async fn transform(
939 &self,
940 data: Vec<HashMap<&str, &str>>,
941 params: Option<TransformParams>,
942 _stream: bool,
943 content_type: &str,
944 ) -> Result<serde_json::Value, reqwest::Error> {
945 let mut payload = HashMap::new();
946
947 if let Ok(params) = serde_json::to_value(params) {
948 if let Some(ref p) = params.as_object() {
949 payload.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
950 }
951 }
952
953 if let Ok(d) = serde_json::to_value(data) {
954 payload.insert("data".into(), d);
955 }
956
957 let res = self.api_post("transform", payload, content_type).await?;
958
959 res.json().await
960 }
961
962 pub async fn extract_contacts(
975 &self,
976 url: &str,
977 params: Option<RequestParams>,
978 _stream: bool,
979 content_type: &str,
980 ) -> Result<serde_json::Value, reqwest::Error> {
981 let mut data = HashMap::new();
982
983 if let Ok(params) = serde_json::to_value(params) {
984 if let Ok(params) = serde_json::to_value(params) {
985 if let Some(ref p) = params.as_object() {
986 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
987 }
988 }
989 }
990
991 match serde_json::to_value(url) {
992 Ok(u) => {
993 data.insert("url".into(), u);
994 }
995 _ => (),
996 }
997
998 let res = self
999 .api_post("pipeline/extract-contacts", data, content_type)
1000 .await?;
1001 res.json().await
1002 }
1003
1004 pub async fn label(
1017 &self,
1018 url: &str,
1019 params: Option<RequestParams>,
1020 _stream: bool,
1021 content_type: &str,
1022 ) -> Result<serde_json::Value, reqwest::Error> {
1023 let mut data = HashMap::new();
1024
1025 if let Ok(params) = serde_json::to_value(params) {
1026 if let Ok(params) = serde_json::to_value(params) {
1027 if let Some(ref p) = params.as_object() {
1028 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1029 }
1030 }
1031 }
1032
1033 data.insert("url".into(), serde_json::Value::String(url.to_string()));
1034
1035 let res = self.api_post("pipeline/label", data, content_type).await?;
1036 res.json().await
1037 }
1038
1039 pub async fn download(
1051 &self,
1052 url: Option<&str>,
1053 options: Option<HashMap<&str, i32>>,
1054 ) -> Result<reqwest::Response, reqwest::Error> {
1055 let mut params = HashMap::new();
1056
1057 if let Some(url) = url {
1058 params.insert("url".to_string(), url.to_string());
1059 }
1060
1061 if let Some(options) = options {
1062 for (key, value) in options {
1063 params.insert(key.to_string(), value.to_string());
1064 }
1065 }
1066
1067 let url = format!("{API_URL}/v1/data/download");
1068 let request = self
1069 .client
1070 .get(&url)
1071 .header(
1072 "User-Agent",
1073 format!("Spider-Client/{}", env!("CARGO_PKG_VERSION")),
1074 )
1075 .header("Content-Type", "application/octet-stream")
1076 .header("Authorization", format!("Bearer {}", self.api_key))
1077 .query(¶ms);
1078
1079 let res = request.send().await?;
1080
1081 Ok(res)
1082 }
1083
1084 pub async fn create_signed_url(
1096 &self,
1097 url: Option<&str>,
1098 options: Option<HashMap<&str, i32>>,
1099 ) -> Result<serde_json::Value, reqwest::Error> {
1100 let mut params = HashMap::new();
1101
1102 if let Some(options) = options {
1103 for (key, value) in options {
1104 params.insert(key.to_string(), value.to_string());
1105 }
1106 }
1107
1108 if let Some(url) = url {
1109 params.insert("url".to_string(), url.to_string());
1110 }
1111
1112 let url = format!("{API_URL}/v1/data/sign-url");
1113 let request = self
1114 .client
1115 .get(&url)
1116 .header(
1117 "User-Agent",
1118 format!("Spider-Client/{}", env!("CARGO_PKG_VERSION")),
1119 )
1120 .header("Authorization", format!("Bearer {}", self.api_key))
1121 .query(¶ms);
1122
1123 let res = request.send().await?;
1124
1125 res.json().await
1126 }
1127
1128 pub async fn get_crawl_state(
1140 &self,
1141 url: &str,
1142 params: Option<RequestParams>,
1143 content_type: &str,
1144 ) -> Result<serde_json::Value, reqwest::Error> {
1145 let mut payload = HashMap::new();
1146 payload.insert("url".into(), serde_json::Value::String(url.to_string()));
1147 payload.insert(
1148 "contentType".into(),
1149 serde_json::Value::String(content_type.to_string()),
1150 );
1151
1152 if let Ok(params) = serde_json::to_value(params) {
1153 if let Ok(params) = serde_json::to_value(params) {
1154 if let Some(ref p) = params.as_object() {
1155 payload.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1156 }
1157 }
1158 }
1159
1160 let res = self
1161 .api_post("data/crawl_state", payload, content_type)
1162 .await?;
1163 res.json().await
1164 }
1165
1166 pub async fn get_credits(&self) -> Result<serde_json::Value, reqwest::Error> {
1168 self.api_get::<serde_json::Value>("data/credits", None)
1169 .await
1170 }
1171
1172 pub async fn data_post(
1174 &self,
1175 table: &str,
1176 data: Option<RequestParams>,
1177 ) -> Result<serde_json::Value, reqwest::Error> {
1178 let res = self
1179 .api_post(&format!("data/{}", table), data, "application/json")
1180 .await?;
1181 res.json().await
1182 }
1183
1184 pub async fn query(&self, params: &QueryRequest) -> Result<serde_json::Value, reqwest::Error> {
1186 let res = self
1187 .api_get::<QueryRequest>(&"data/query", Some(params))
1188 .await?;
1189
1190 Ok(res)
1191 }
1192
1193 pub async fn data_get(
1195 &self,
1196 table: &str,
1197 params: Option<RequestParams>,
1198 ) -> Result<serde_json::Value, reqwest::Error> {
1199 let mut payload = HashMap::new();
1200
1201 if let Some(params) = params {
1202 if let Ok(p) = serde_json::to_value(params) {
1203 if let Some(o) = p.as_object() {
1204 payload.extend(o.iter().map(|(k, v)| (k.as_str(), v.clone())));
1205 }
1206 }
1207 }
1208
1209 let res = self
1210 .api_get::<serde_json::Value>(&format!("data/{}", table), None)
1211 .await?;
1212 Ok(res)
1213 }
1214
1215 pub async fn data_delete(
1217 &self,
1218 table: &str,
1219 params: Option<RequestParams>,
1220 ) -> Result<serde_json::Value, reqwest::Error> {
1221 let mut payload = HashMap::new();
1222
1223 if let Ok(params) = serde_json::to_value(params) {
1224 if let Ok(params) = serde_json::to_value(params) {
1225 if let Some(ref p) = params.as_object() {
1226 payload.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1227 }
1228 }
1229 }
1230
1231 let res = self
1232 .api_delete(&format!("data/{}", table), Some(payload))
1233 .await?;
1234 res.json().await
1235 }
1236}
1237
1238#[cfg(test)]
1239mod tests {
1240 use super::*;
1241 use dotenv::dotenv;
1242 use lazy_static::lazy_static;
1243 use reqwest::ClientBuilder;
1244
1245 lazy_static! {
1246 static ref SPIDER_CLIENT: Spider = {
1247 dotenv().ok();
1248 let client = ClientBuilder::new();
1249 let client = client.user_agent("SpiderBot").build().unwrap();
1250
1251 Spider::new_with_client(None, client).expect("client to build")
1252 };
1253 }
1254
1255 #[tokio::test]
1256 #[ignore]
1257 async fn test_scrape_url() {
1258 let response = SPIDER_CLIENT
1259 .scrape_url("https://example.com", None, "application/json")
1260 .await;
1261 assert!(response.is_ok());
1262 }
1263
1264 #[tokio::test]
1265 async fn test_crawl_url() {
1266 let response = SPIDER_CLIENT
1267 .crawl_url(
1268 "https://example.com",
1269 None,
1270 false,
1271 "application/json",
1272 None::<fn(serde_json::Value)>,
1273 )
1274 .await;
1275 assert!(response.is_ok());
1276 }
1277
1278 #[tokio::test]
1279 #[ignore]
1280 async fn test_links() {
1281 let response: Result<serde_json::Value, Error> = SPIDER_CLIENT
1282 .links("https://example.com", None, false, "application/json")
1283 .await;
1284 assert!(response.is_ok());
1285 }
1286
1287 #[tokio::test]
1288 #[ignore]
1289 async fn test_screenshot() {
1290 let mut params = RequestParams::default();
1291 params.limit = Some(1);
1292
1293 let response = SPIDER_CLIENT
1294 .screenshot(
1295 "https://example.com",
1296 Some(params),
1297 false,
1298 "application/json",
1299 )
1300 .await;
1301 assert!(response.is_ok());
1302 }
1303
1304 #[tokio::test]
1320 #[ignore]
1321 async fn test_transform() {
1322 let data = vec![HashMap::from([(
1323 "<html><body><h1>Transformation</h1></body></html>".into(),
1324 "".into(),
1325 )])];
1326 let response = SPIDER_CLIENT
1327 .transform(data, None, false, "application/json")
1328 .await;
1329 assert!(response.is_ok());
1330 }
1331
1332 #[tokio::test]
1333 #[ignore]
1334 async fn test_extract_contacts() {
1335 let response = SPIDER_CLIENT
1336 .extract_contacts("https://example.com", None, false, "application/json")
1337 .await;
1338 assert!(response.is_ok());
1339 }
1340
1341 #[tokio::test]
1342 #[ignore]
1343 async fn test_label() {
1344 let response = SPIDER_CLIENT
1345 .label("https://example.com", None, false, "application/json")
1346 .await;
1347 assert!(response.is_ok());
1348 }
1349
1350 #[tokio::test]
1351 async fn test_create_signed_url() {
1352 let response = SPIDER_CLIENT
1353 .create_signed_url(Some("example.com"), None)
1354 .await;
1355 assert!(response.is_ok());
1356 }
1357
1358 #[tokio::test]
1359 async fn test_get_crawl_state() {
1360 let response = SPIDER_CLIENT
1361 .get_crawl_state("https://example.com", None, "application/json")
1362 .await;
1363 assert!(response.is_ok());
1364 }
1365
1366 #[tokio::test]
1367 async fn test_query() {
1368 let mut query = QueryRequest::default();
1369
1370 query.domain = Some("spider.cloud".into());
1371
1372 let response = SPIDER_CLIENT.query(&query).await;
1373 assert!(response.is_ok());
1374 }
1375
1376 #[tokio::test]
1377 async fn test_get_credits() {
1378 let response = SPIDER_CLIENT.get_credits().await;
1379 assert!(response.is_ok());
1380 }
1381}