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)]
240pub struct EventTracker {
241 responses: Option<bool>,
243 requests: Option<bool>
245}
246
247#[derive(Debug, Default, Deserialize, Serialize, Clone)]
249pub struct RequestParams {
250 #[serde(default)]
251 pub url: Option<String>,
253 #[serde(default)]
254 pub request: Option<RequestType>,
256 #[serde(default)]
257 pub limit: Option<u32>,
259 #[serde(default)]
260 pub return_format: Option<ReturnFormatHandling>,
262 #[serde(default)]
263 pub tld: Option<bool>,
265 #[serde(default)]
266 pub depth: Option<u32>,
268 #[serde(default)]
269 pub cache: Option<bool>,
271 #[serde(default)]
272 pub scroll: Option<u32>,
274 #[serde(default)]
275 pub budget: Option<HashMap<String, u32>>,
277 #[serde(default)]
278 pub blacklist: Option<Vec<String>>,
280 #[serde(default)]
281 pub whitelist: Option<Vec<String>>,
283 #[serde(default)]
284 pub locale: Option<String>,
286 #[serde(default)]
287 pub cookies: Option<String>,
289 #[serde(default)]
290 pub stealth: Option<bool>,
292 #[serde(default)]
293 pub headers: Option<HashMap<String, String>>,
295 #[serde(default)]
296 pub anti_bot: Option<bool>,
298 #[serde(default)]
299 pub webhooks: Option<WebhookSettings>,
301 #[serde(default)]
302 pub metadata: Option<bool>,
304 #[serde(default)]
305 pub viewport: Option<Viewport>,
307 #[serde(default)]
308 pub encoding: Option<String>,
310 #[serde(default)]
311 pub subdomains: Option<bool>,
313 #[serde(default)]
314 pub user_agent: Option<String>,
316 #[serde(default)]
317 pub store_data: Option<bool>,
319 #[serde(default)]
320 pub gpt_config: Option<HashMap<String, String>>,
322 #[serde(default)]
323 pub fingerprint: Option<bool>,
325 #[serde(default)]
326 pub storageless: Option<bool>,
328 #[serde(default)]
329 pub readability: Option<bool>,
331 #[serde(default)]
332 pub proxy_enabled: Option<bool>,
334 #[serde(default)]
335 pub respect_robots: Option<bool>,
337 #[serde(default)]
338 pub root_selector: Option<String>,
340 #[serde(default)]
341 pub full_resources: Option<bool>,
343 #[serde(default)]
344 pub text: Option<String>,
346 #[serde(default)]
347 pub sitemap: Option<bool>,
349 #[serde(default)]
350 pub external_domains: Option<Vec<String>>,
352 #[serde(default)]
353 pub return_embeddings: Option<bool>,
355 #[serde(default)]
356 pub return_headers: Option<bool>,
358 #[serde(default)]
359 pub return_page_links: Option<bool>,
361 #[serde(default)]
362 pub return_cookies: Option<bool>,
364 #[serde(default)]
365 pub request_timeout: Option<u8>,
367 #[serde(default)]
368 pub run_in_background: Option<bool>,
370 #[serde(default)]
371 pub skip_config_checks: Option<bool>,
373 #[serde(default)]
374 pub css_extraction_map: Option<CSSExtractionMap>,
376 #[serde(default)]
377 pub chunking_alg: Option<ChunkingAlgDict>,
379 #[serde(default)]
380 pub disable_intercept: Option<bool>,
382 #[serde(default)]
383 pub wait_for: Option<WaitFor>,
385 #[serde(default)]
386 pub execution_scripts: Option<ExecutionScriptsMap>,
388 #[serde(default)]
389 pub automation_scripts: Option<WebAutomationMap>,
391 #[serde(default)]
392 pub redirect_policy: Option<RedirectPolicy>,
394 #[serde(default)]
395 pub event_tracker: Option<EventTracker>
397}
398
399#[derive(Debug, Default, Deserialize, Serialize, Clone)]
401pub struct SearchRequestParams {
402 #[serde(default, flatten)]
404 pub base: RequestParams,
405 pub search: String,
407 pub search_limit: Option<u32>,
409 pub fetch_page_content: Option<bool>,
411 pub location: Option<String>,
413 pub country: Option<String>,
415 pub language: Option<String>,
417 pub num: Option<u32>,
419 pub page: Option<u32>,
421 #[serde(default)]
422 pub website_limit: Option<u32>,
424}
425
426#[derive(Debug, Default, Deserialize, Serialize, Clone)]
428pub struct TransformParams {
429 #[serde(default)]
430 pub return_format: Option<ReturnFormat>,
432 #[serde(default)]
433 pub readability: Option<bool>,
435 #[serde(default)]
436 pub clean: Option<bool>,
438 #[serde(default)]
439 pub clean_full: Option<bool>,
441 pub data: Vec<DataParam>,
443}
444
445#[derive(Serialize, Deserialize, Debug, Clone)]
446pub struct DataParam {
447 pub html: String,
449 pub url: Option<String>,
451}
452
453#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
455#[serde(rename_all = "lowercase")]
456pub enum RequestType {
457 Http,
459 Chrome,
461 #[default]
462 SmartMode,
464}
465
466#[derive(Default, Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Hash)]
468#[serde(rename_all = "lowercase")]
469pub enum ReturnFormat {
470 #[default]
471 Raw,
473 Markdown,
475 Commonmark,
477 Html2text,
479 Text,
481 Xml,
483 Bytes,
485}
486
487#[derive(Debug, Default)]
489pub struct Spider {
490 pub api_key: String,
492 pub client: Client,
494}
495
496impl Spider {
497 pub fn new(api_key: Option<String>) -> Result<Self, &'static str> {
507 let api_key = api_key.or_else(|| std::env::var("SPIDER_API_KEY").ok());
508
509 match api_key {
510 Some(key) => Ok(Self {
511 api_key: key,
512 client: Client::new(),
513 }),
514 None => Err("No API key provided"),
515 }
516 }
517
518 pub fn new_with_client(api_key: Option<String>, client: Client) -> Result<Self, &'static str> {
529 let api_key = api_key.or_else(|| std::env::var("SPIDER_API_KEY").ok());
530
531 match api_key {
532 Some(key) => Ok(Self {
533 api_key: key,
534 client,
535 }),
536 None => Err("No API key provided"),
537 }
538 }
539
540 async fn api_post_base(
553 &self,
554 endpoint: &str,
555 data: impl Serialize + Sized + std::fmt::Debug,
556 content_type: &str,
557 ) -> Result<Response, Error> {
558 let url: String = format!("{API_URL}/{}", endpoint);
559
560 self.client
561 .post(&url)
562 .header(
563 "User-Agent",
564 format!("Spider-Client/{}", env!("CARGO_PKG_VERSION")),
565 )
566 .header("Content-Type", content_type)
567 .header("Authorization", format!("Bearer {}", self.api_key))
568 .json(&data)
569 .send()
570 .await
571 }
572
573 async fn api_post(
586 &self,
587 endpoint: &str,
588 data: impl Serialize + std::fmt::Debug + Clone + Send + Sync,
589 content_type: &str,
590 ) -> Result<Response, Error> {
591 let fetch = || async {
592 self.api_post_base(endpoint, data.to_owned(), content_type)
593 .await
594 };
595
596 fetch
597 .retry(ExponentialBuilder::default().with_max_times(5))
598 .when(|err: &reqwest::Error| {
599 if let Some(status) = err.status() {
600 status.is_server_error()
601 } else {
602 err.is_timeout()
603 }
604 })
605 .await
606 }
607
608 async fn api_get_base<T: Serialize>(
618 &self,
619 endpoint: &str,
620 query_params: Option<&T>,
621 ) -> Result<serde_json::Value, reqwest::Error> {
622 let url = format!("{API_URL}/{}", endpoint);
623 let res = self
624 .client
625 .get(&url)
626 .query(&query_params)
627 .header(
628 "User-Agent",
629 format!("Spider-Client/{}", env!("CARGO_PKG_VERSION")),
630 )
631 .header("Content-Type", "application/json")
632 .header("Authorization", format!("Bearer {}", self.api_key))
633 .send()
634 .await?;
635 res.json().await
636 }
637
638 async fn api_get<T: Serialize>(
648 &self,
649 endpoint: &str,
650 query_params: Option<&T>,
651 ) -> Result<serde_json::Value, reqwest::Error> {
652 let fetch = || async { self.api_get_base(endpoint, query_params.to_owned()).await };
653
654 fetch
655 .retry(ExponentialBuilder::default().with_max_times(5))
656 .when(|err: &reqwest::Error| {
657 if let Some(status) = err.status() {
658 status.is_server_error()
659 } else {
660 err.is_timeout()
661 }
662 })
663 .await
664 }
665
666 async fn api_delete_base(
679 &self,
680 endpoint: &str,
681 params: Option<HashMap<String, serde_json::Value>>,
682 ) -> Result<Response, Error> {
683 let url = format!("{API_URL}/v1/{}", endpoint);
684 let request_builder = self
685 .client
686 .delete(&url)
687 .header(
688 "User-Agent",
689 format!("Spider-Client/{}", env!("CARGO_PKG_VERSION")),
690 )
691 .header("Content-Type", "application/json")
692 .header("Authorization", format!("Bearer {}", self.api_key));
693
694 let request_builder = if let Some(params) = params {
695 request_builder.json(¶ms)
696 } else {
697 request_builder
698 };
699
700 request_builder.send().await
701 }
702
703 async fn api_delete(
716 &self,
717 endpoint: &str,
718 params: Option<HashMap<String, serde_json::Value>>,
719 ) -> Result<Response, Error> {
720 let fetch = || async { self.api_delete_base(endpoint, params.to_owned()).await };
721
722 fetch
723 .retry(ExponentialBuilder::default().with_max_times(5))
724 .when(|err: &reqwest::Error| {
725 if let Some(status) = err.status() {
726 status.is_server_error()
727 } else {
728 err.is_timeout()
729 }
730 })
731 .await
732 }
733
734 pub async fn scrape_url(
747 &self,
748 url: &str,
749 params: Option<RequestParams>,
750 content_type: &str,
751 ) -> Result<serde_json::Value, reqwest::Error> {
752 let mut data = HashMap::new();
753
754 data.insert(
755 "url".to_string(),
756 serde_json::Value::String(url.to_string()),
757 );
758 data.insert("limit".to_string(), serde_json::Value::Number(1.into()));
759
760 if let Ok(params) = serde_json::to_value(params) {
761 if let Some(ref p) = params.as_object() {
762 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
763 }
764 }
765
766 let res = self.api_post("crawl", data, content_type).await?;
767 res.json().await
768 }
769
770 pub async fn crawl_url(
784 &self,
785 url: &str,
786 params: Option<RequestParams>,
787 stream: bool,
788 content_type: &str,
789 callback: Option<impl Fn(serde_json::Value) + Send>,
790 ) -> Result<serde_json::Value, reqwest::Error> {
791 use tokio_util::codec::{FramedRead, LinesCodec};
792
793 let mut data = HashMap::new();
794
795 if let Ok(params) = serde_json::to_value(params) {
796 if let Some(ref p) = params.as_object() {
797 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
798 }
799 }
800
801 data.insert("url".into(), serde_json::Value::String(url.to_string()));
802
803 let res = self.api_post("crawl", data, content_type).await?;
804
805 if stream {
806 if let Some(callback) = callback {
807 let stream = res.bytes_stream();
808
809 let stream_reader = tokio_util::io::StreamReader::new(
810 stream.map(|r| r.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))),
811 );
812
813 let mut lines = FramedRead::new(stream_reader, LinesCodec::new());
814
815 while let Some(line_result) = lines.next().await {
816 match line_result {
817 Ok(line) => {
818 match serde_json::from_str::<serde_json::Value>(&line) {
819 Ok(value) => {
820 callback(value);
821 }
822 Err(_e) => {
823 continue;
824 }
825 }
826 }
827 Err(_e) => {
828 return Ok(serde_json::Value::Null)
829 }
830 }
831 }
832
833 Ok(serde_json::Value::Null)
834 } else {
835 Ok(serde_json::Value::Null)
836 }
837 } else {
838 res.json().await
839 }
840 }
841
842 pub async fn links(
855 &self,
856 url: &str,
857 params: Option<RequestParams>,
858 _stream: bool,
859 content_type: &str,
860 ) -> Result<serde_json::Value, reqwest::Error> {
861 let mut data = HashMap::new();
862
863 if let Ok(params) = serde_json::to_value(params) {
864 if let Some(ref p) = params.as_object() {
865 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
866 }
867 }
868
869 data.insert("url".into(), serde_json::Value::String(url.to_string()));
870
871 let res = self.api_post("links", data, content_type).await?;
872 res.json().await
873 }
874
875 pub async fn screenshot(
888 &self,
889 url: &str,
890 params: Option<RequestParams>,
891 _stream: bool,
892 content_type: &str,
893 ) -> Result<serde_json::Value, reqwest::Error> {
894 let mut data = HashMap::new();
895
896 if let Ok(params) = serde_json::to_value(params) {
897 if let Some(ref p) = params.as_object() {
898 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
899 }
900 }
901
902 data.insert("url".into(), serde_json::Value::String(url.to_string()));
903
904 let res = self.api_post("screenshot", data, content_type).await?;
905 res.json().await
906 }
907
908 pub async fn search(
921 &self,
922 q: &str,
923 params: Option<SearchRequestParams>,
924 _stream: bool,
925 content_type: &str,
926 ) -> Result<serde_json::Value, reqwest::Error> {
927 let body = match params {
928 Some(mut params) => {
929 params.search = q.to_string();
930 params
931 }
932 _ => {
933 let mut params = SearchRequestParams::default();
934 params.search = q.to_string();
935 params
936 }
937 };
938
939 let res = self.api_post("search", body, content_type).await?;
940
941 res.json().await
942 }
943
944 pub async fn transform(
957 &self,
958 data: Vec<HashMap<&str, &str>>,
959 params: Option<TransformParams>,
960 _stream: bool,
961 content_type: &str,
962 ) -> Result<serde_json::Value, reqwest::Error> {
963 let mut payload = HashMap::new();
964
965 if let Ok(params) = serde_json::to_value(params) {
966 if let Some(ref p) = params.as_object() {
967 payload.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
968 }
969 }
970
971 if let Ok(d) = serde_json::to_value(data) {
972 payload.insert("data".into(), d);
973 }
974
975 let res = self.api_post("transform", payload, content_type).await?;
976
977 res.json().await
978 }
979
980 pub async fn extract_contacts(
993 &self,
994 url: &str,
995 params: Option<RequestParams>,
996 _stream: bool,
997 content_type: &str,
998 ) -> Result<serde_json::Value, reqwest::Error> {
999 let mut data = HashMap::new();
1000
1001 if let Ok(params) = serde_json::to_value(params) {
1002 if let Ok(params) = serde_json::to_value(params) {
1003 if let Some(ref p) = params.as_object() {
1004 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1005 }
1006 }
1007 }
1008
1009 match serde_json::to_value(url) {
1010 Ok(u) => {
1011 data.insert("url".into(), u);
1012 }
1013 _ => (),
1014 }
1015
1016 let res = self
1017 .api_post("pipeline/extract-contacts", data, content_type)
1018 .await?;
1019 res.json().await
1020 }
1021
1022 pub async fn label(
1035 &self,
1036 url: &str,
1037 params: Option<RequestParams>,
1038 _stream: bool,
1039 content_type: &str,
1040 ) -> Result<serde_json::Value, reqwest::Error> {
1041 let mut data = HashMap::new();
1042
1043 if let Ok(params) = serde_json::to_value(params) {
1044 if let Ok(params) = serde_json::to_value(params) {
1045 if let Some(ref p) = params.as_object() {
1046 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1047 }
1048 }
1049 }
1050
1051 data.insert("url".into(), serde_json::Value::String(url.to_string()));
1052
1053 let res = self.api_post("pipeline/label", data, content_type).await?;
1054 res.json().await
1055 }
1056
1057 pub async fn download(
1069 &self,
1070 url: Option<&str>,
1071 options: Option<HashMap<&str, i32>>,
1072 ) -> Result<reqwest::Response, reqwest::Error> {
1073 let mut params = HashMap::new();
1074
1075 if let Some(url) = url {
1076 params.insert("url".to_string(), url.to_string());
1077 }
1078
1079 if let Some(options) = options {
1080 for (key, value) in options {
1081 params.insert(key.to_string(), value.to_string());
1082 }
1083 }
1084
1085 let url = format!("{API_URL}/v1/data/download");
1086 let request = self
1087 .client
1088 .get(&url)
1089 .header(
1090 "User-Agent",
1091 format!("Spider-Client/{}", env!("CARGO_PKG_VERSION")),
1092 )
1093 .header("Content-Type", "application/octet-stream")
1094 .header("Authorization", format!("Bearer {}", self.api_key))
1095 .query(¶ms);
1096
1097 let res = request.send().await?;
1098
1099 Ok(res)
1100 }
1101
1102 pub async fn create_signed_url(
1114 &self,
1115 url: Option<&str>,
1116 options: Option<HashMap<&str, i32>>,
1117 ) -> Result<serde_json::Value, reqwest::Error> {
1118 let mut params = HashMap::new();
1119
1120 if let Some(options) = options {
1121 for (key, value) in options {
1122 params.insert(key.to_string(), value.to_string());
1123 }
1124 }
1125
1126 if let Some(url) = url {
1127 params.insert("url".to_string(), url.to_string());
1128 }
1129
1130 let url = format!("{API_URL}/v1/data/sign-url");
1131 let request = self
1132 .client
1133 .get(&url)
1134 .header(
1135 "User-Agent",
1136 format!("Spider-Client/{}", env!("CARGO_PKG_VERSION")),
1137 )
1138 .header("Authorization", format!("Bearer {}", self.api_key))
1139 .query(¶ms);
1140
1141 let res = request.send().await?;
1142
1143 res.json().await
1144 }
1145
1146 pub async fn get_crawl_state(
1158 &self,
1159 url: &str,
1160 params: Option<RequestParams>,
1161 content_type: &str,
1162 ) -> Result<serde_json::Value, reqwest::Error> {
1163 let mut payload = HashMap::new();
1164 payload.insert("url".into(), serde_json::Value::String(url.to_string()));
1165 payload.insert(
1166 "contentType".into(),
1167 serde_json::Value::String(content_type.to_string()),
1168 );
1169
1170 if let Ok(params) = serde_json::to_value(params) {
1171 if let Ok(params) = serde_json::to_value(params) {
1172 if let Some(ref p) = params.as_object() {
1173 payload.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1174 }
1175 }
1176 }
1177
1178 let res = self
1179 .api_post("data/crawl_state", payload, content_type)
1180 .await?;
1181 res.json().await
1182 }
1183
1184 pub async fn get_credits(&self) -> Result<serde_json::Value, reqwest::Error> {
1186 self.api_get::<serde_json::Value>("data/credits", None)
1187 .await
1188 }
1189
1190 pub async fn data_post(
1192 &self,
1193 table: &str,
1194 data: Option<RequestParams>,
1195 ) -> Result<serde_json::Value, reqwest::Error> {
1196 let res = self
1197 .api_post(&format!("data/{}", table), data, "application/json")
1198 .await?;
1199 res.json().await
1200 }
1201
1202 pub async fn query(&self, params: &QueryRequest) -> Result<serde_json::Value, reqwest::Error> {
1204 let res = self
1205 .api_get::<QueryRequest>(&"data/query", Some(params))
1206 .await?;
1207
1208 Ok(res)
1209 }
1210
1211 pub async fn data_get(
1213 &self,
1214 table: &str,
1215 params: Option<RequestParams>,
1216 ) -> Result<serde_json::Value, reqwest::Error> {
1217 let mut payload = HashMap::new();
1218
1219 if let Some(params) = params {
1220 if let Ok(p) = serde_json::to_value(params) {
1221 if let Some(o) = p.as_object() {
1222 payload.extend(o.iter().map(|(k, v)| (k.as_str(), v.clone())));
1223 }
1224 }
1225 }
1226
1227 let res = self
1228 .api_get::<serde_json::Value>(&format!("data/{}", table), None)
1229 .await?;
1230 Ok(res)
1231 }
1232
1233 pub async fn data_delete(
1235 &self,
1236 table: &str,
1237 params: Option<RequestParams>,
1238 ) -> Result<serde_json::Value, reqwest::Error> {
1239 let mut payload = HashMap::new();
1240
1241 if let Ok(params) = serde_json::to_value(params) {
1242 if let Ok(params) = serde_json::to_value(params) {
1243 if let Some(ref p) = params.as_object() {
1244 payload.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1245 }
1246 }
1247 }
1248
1249 let res = self
1250 .api_delete(&format!("data/{}", table), Some(payload))
1251 .await?;
1252 res.json().await
1253 }
1254}
1255
1256#[cfg(test)]
1257mod tests {
1258 use super::*;
1259 use dotenv::dotenv;
1260 use lazy_static::lazy_static;
1261 use reqwest::ClientBuilder;
1262
1263 lazy_static! {
1264 static ref SPIDER_CLIENT: Spider = {
1265 dotenv().ok();
1266 let client = ClientBuilder::new();
1267 let client = client.user_agent("SpiderBot").build().unwrap();
1268
1269 Spider::new_with_client(None, client).expect("client to build")
1270 };
1271 }
1272
1273 #[tokio::test]
1274 #[ignore]
1275 async fn test_scrape_url() {
1276 let response = SPIDER_CLIENT
1277 .scrape_url("https://example.com", None, "application/json")
1278 .await;
1279 assert!(response.is_ok());
1280 }
1281
1282 #[tokio::test]
1283 async fn test_crawl_url() {
1284 let response = SPIDER_CLIENT
1285 .crawl_url(
1286 "https://example.com",
1287 None,
1288 false,
1289 "application/json",
1290 None::<fn(serde_json::Value)>,
1291 )
1292 .await;
1293 assert!(response.is_ok());
1294 }
1295
1296 #[tokio::test]
1297 #[ignore]
1298 async fn test_links() {
1299 let response: Result<serde_json::Value, Error> = SPIDER_CLIENT
1300 .links("https://example.com", None, false, "application/json")
1301 .await;
1302 assert!(response.is_ok());
1303 }
1304
1305 #[tokio::test]
1306 #[ignore]
1307 async fn test_screenshot() {
1308 let mut params = RequestParams::default();
1309 params.limit = Some(1);
1310
1311 let response = SPIDER_CLIENT
1312 .screenshot(
1313 "https://example.com",
1314 Some(params),
1315 false,
1316 "application/json",
1317 )
1318 .await;
1319 assert!(response.is_ok());
1320 }
1321
1322 #[tokio::test]
1338 #[ignore]
1339 async fn test_transform() {
1340 let data = vec![HashMap::from([(
1341 "<html><body><h1>Transformation</h1></body></html>".into(),
1342 "".into(),
1343 )])];
1344 let response = SPIDER_CLIENT
1345 .transform(data, None, false, "application/json")
1346 .await;
1347 assert!(response.is_ok());
1348 }
1349
1350 #[tokio::test]
1351 #[ignore]
1352 async fn test_extract_contacts() {
1353 let response = SPIDER_CLIENT
1354 .extract_contacts("https://example.com", None, false, "application/json")
1355 .await;
1356 assert!(response.is_ok());
1357 }
1358
1359 #[tokio::test]
1360 #[ignore]
1361 async fn test_label() {
1362 let response = SPIDER_CLIENT
1363 .label("https://example.com", None, false, "application/json")
1364 .await;
1365 assert!(response.is_ok());
1366 }
1367
1368 #[tokio::test]
1369 async fn test_create_signed_url() {
1370 let response = SPIDER_CLIENT
1371 .create_signed_url(Some("example.com"), None)
1372 .await;
1373 assert!(response.is_ok());
1374 }
1375
1376 #[tokio::test]
1377 async fn test_get_crawl_state() {
1378 let response = SPIDER_CLIENT
1379 .get_crawl_state("https://example.com", None, "application/json")
1380 .await;
1381 assert!(response.is_ok());
1382 }
1383
1384 #[tokio::test]
1385 async fn test_query() {
1386 let mut query = QueryRequest::default();
1387
1388 query.domain = Some("spider.cloud".into());
1389
1390 let response = SPIDER_CLIENT.query(&query).await;
1391 assert!(response.is_ok());
1392 }
1393
1394 #[tokio::test]
1395 async fn test_get_credits() {
1396 let response = SPIDER_CLIENT.get_credits().await;
1397 assert!(response.is_ok());
1398 }
1399}