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 #[serde(default)]
398 pub crawl_timeout: Option<Timeout>,
400 #[serde(default)]
401 pub evaluate_on_new_document: Option<Box<String>>,
403 #[serde(default)]
404 pub lite_mode: Option<bool>,
408}
409
410#[derive(Debug, Default, Deserialize, Serialize, Clone)]
412pub struct SearchRequestParams {
413 #[serde(default, flatten)]
415 pub base: RequestParams,
416 pub search: String,
418 pub search_limit: Option<u32>,
420 pub fetch_page_content: Option<bool>,
422 pub location: Option<String>,
424 pub country: Option<String>,
426 pub language: Option<String>,
428 pub num: Option<u32>,
430 pub page: Option<u32>,
432 #[serde(default)]
433 pub website_limit: Option<u32>,
435}
436
437#[derive(Debug, Default, Deserialize, Serialize, Clone)]
439pub struct TransformParams {
440 #[serde(default)]
441 pub return_format: Option<ReturnFormat>,
443 #[serde(default)]
444 pub readability: Option<bool>,
446 #[serde(default)]
447 pub clean: Option<bool>,
449 #[serde(default)]
450 pub clean_full: Option<bool>,
452 pub data: Vec<DataParam>,
454}
455
456#[derive(Serialize, Deserialize, Debug, Clone)]
457pub struct DataParam {
458 pub html: String,
460 pub url: Option<String>,
462}
463
464#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
466#[serde(rename_all = "lowercase")]
467pub enum RequestType {
468 Http,
470 Chrome,
472 #[default]
473 SmartMode,
475}
476
477#[derive(Default, Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Hash)]
479#[serde(rename_all = "lowercase")]
480pub enum ReturnFormat {
481 #[default]
482 Raw,
484 Markdown,
486 Commonmark,
488 Html2text,
490 Text,
492 Xml,
494 Bytes,
496}
497
498#[derive(Debug, Default)]
500pub struct Spider {
501 pub api_key: String,
503 pub client: Client,
505}
506
507impl Spider {
508 pub fn new(api_key: Option<String>) -> Result<Self, &'static str> {
518 let api_key = api_key.or_else(|| std::env::var("SPIDER_API_KEY").ok());
519
520 match api_key {
521 Some(key) => Ok(Self {
522 api_key: key,
523 client: Client::new(),
524 }),
525 None => Err("No API key provided"),
526 }
527 }
528
529 pub fn new_with_client(api_key: Option<String>, client: Client) -> Result<Self, &'static str> {
540 let api_key = api_key.or_else(|| std::env::var("SPIDER_API_KEY").ok());
541
542 match api_key {
543 Some(key) => Ok(Self {
544 api_key: key,
545 client,
546 }),
547 None => Err("No API key provided"),
548 }
549 }
550
551 async fn api_post_base(
564 &self,
565 endpoint: &str,
566 data: impl Serialize + Sized + std::fmt::Debug,
567 content_type: &str,
568 ) -> Result<Response, Error> {
569 let url: String = format!("{API_URL}/{}", endpoint);
570
571 self.client
572 .post(&url)
573 .header(
574 "User-Agent",
575 format!("Spider-Client/{}", env!("CARGO_PKG_VERSION")),
576 )
577 .header("Content-Type", content_type)
578 .header("Authorization", format!("Bearer {}", self.api_key))
579 .json(&data)
580 .send()
581 .await
582 }
583
584 async fn api_post(
597 &self,
598 endpoint: &str,
599 data: impl Serialize + std::fmt::Debug + Clone + Send + Sync,
600 content_type: &str,
601 ) -> Result<Response, Error> {
602 let fetch = || async {
603 self.api_post_base(endpoint, data.to_owned(), content_type)
604 .await
605 };
606
607 fetch
608 .retry(ExponentialBuilder::default().with_max_times(5))
609 .when(|err: &reqwest::Error| {
610 if let Some(status) = err.status() {
611 status.is_server_error()
612 } else {
613 err.is_timeout()
614 }
615 })
616 .await
617 }
618
619 async fn api_get_base<T: Serialize>(
629 &self,
630 endpoint: &str,
631 query_params: Option<&T>,
632 ) -> Result<serde_json::Value, reqwest::Error> {
633 let url = format!("{API_URL}/{}", endpoint);
634 let res = self
635 .client
636 .get(&url)
637 .query(&query_params)
638 .header(
639 "User-Agent",
640 format!("Spider-Client/{}", env!("CARGO_PKG_VERSION")),
641 )
642 .header("Content-Type", "application/json")
643 .header("Authorization", format!("Bearer {}", self.api_key))
644 .send()
645 .await?;
646 res.json().await
647 }
648
649 async fn api_get<T: Serialize>(
659 &self,
660 endpoint: &str,
661 query_params: Option<&T>,
662 ) -> Result<serde_json::Value, reqwest::Error> {
663 let fetch = || async { self.api_get_base(endpoint, query_params.to_owned()).await };
664
665 fetch
666 .retry(ExponentialBuilder::default().with_max_times(5))
667 .when(|err: &reqwest::Error| {
668 if let Some(status) = err.status() {
669 status.is_server_error()
670 } else {
671 err.is_timeout()
672 }
673 })
674 .await
675 }
676
677 async fn api_delete_base(
690 &self,
691 endpoint: &str,
692 params: Option<HashMap<String, serde_json::Value>>,
693 ) -> Result<Response, Error> {
694 let url = format!("{API_URL}/v1/{}", endpoint);
695 let request_builder = self
696 .client
697 .delete(&url)
698 .header(
699 "User-Agent",
700 format!("Spider-Client/{}", env!("CARGO_PKG_VERSION")),
701 )
702 .header("Content-Type", "application/json")
703 .header("Authorization", format!("Bearer {}", self.api_key));
704
705 let request_builder = if let Some(params) = params {
706 request_builder.json(¶ms)
707 } else {
708 request_builder
709 };
710
711 request_builder.send().await
712 }
713
714 async fn api_delete(
727 &self,
728 endpoint: &str,
729 params: Option<HashMap<String, serde_json::Value>>,
730 ) -> Result<Response, Error> {
731 let fetch = || async { self.api_delete_base(endpoint, params.to_owned()).await };
732
733 fetch
734 .retry(ExponentialBuilder::default().with_max_times(5))
735 .when(|err: &reqwest::Error| {
736 if let Some(status) = err.status() {
737 status.is_server_error()
738 } else {
739 err.is_timeout()
740 }
741 })
742 .await
743 }
744
745 pub async fn scrape_url(
758 &self,
759 url: &str,
760 params: Option<RequestParams>,
761 content_type: &str,
762 ) -> Result<serde_json::Value, reqwest::Error> {
763 let mut data = HashMap::new();
764
765 data.insert(
766 "url".to_string(),
767 serde_json::Value::String(url.to_string()),
768 );
769 data.insert("limit".to_string(), serde_json::Value::Number(1.into()));
770
771 if let Ok(params) = serde_json::to_value(params) {
772 if let Some(ref p) = params.as_object() {
773 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
774 }
775 }
776
777 let res = self.api_post("crawl", data, content_type).await?;
778 res.json().await
779 }
780
781 pub async fn crawl_url(
795 &self,
796 url: &str,
797 params: Option<RequestParams>,
798 stream: bool,
799 content_type: &str,
800 callback: Option<impl Fn(serde_json::Value) + Send>,
801 ) -> Result<serde_json::Value, reqwest::Error> {
802 use tokio_util::codec::{FramedRead, LinesCodec};
803
804 let mut data = HashMap::new();
805
806 if let Ok(params) = serde_json::to_value(params) {
807 if let Some(ref p) = params.as_object() {
808 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
809 }
810 }
811
812 data.insert("url".into(), serde_json::Value::String(url.to_string()));
813
814 let res = self.api_post("crawl", data, content_type).await?;
815
816 if stream {
817 if let Some(callback) = callback {
818 let stream = res.bytes_stream();
819
820 let stream_reader = tokio_util::io::StreamReader::new(
821 stream.map(|r| r.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))),
822 );
823
824 let mut lines = FramedRead::new(stream_reader, LinesCodec::new());
825
826 while let Some(line_result) = lines.next().await {
827 match line_result {
828 Ok(line) => {
829 match serde_json::from_str::<serde_json::Value>(&line) {
830 Ok(value) => {
831 callback(value);
832 }
833 Err(_e) => {
834 continue;
835 }
836 }
837 }
838 Err(_e) => {
839 return Ok(serde_json::Value::Null)
840 }
841 }
842 }
843
844 Ok(serde_json::Value::Null)
845 } else {
846 Ok(serde_json::Value::Null)
847 }
848 } else {
849 res.json().await
850 }
851 }
852
853 pub async fn links(
866 &self,
867 url: &str,
868 params: Option<RequestParams>,
869 _stream: bool,
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 data.insert("url".into(), serde_json::Value::String(url.to_string()));
881
882 let res = self.api_post("links", data, content_type).await?;
883 res.json().await
884 }
885
886 pub async fn screenshot(
899 &self,
900 url: &str,
901 params: Option<RequestParams>,
902 _stream: bool,
903 content_type: &str,
904 ) -> Result<serde_json::Value, reqwest::Error> {
905 let mut data = HashMap::new();
906
907 if let Ok(params) = serde_json::to_value(params) {
908 if let Some(ref p) = params.as_object() {
909 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
910 }
911 }
912
913 data.insert("url".into(), serde_json::Value::String(url.to_string()));
914
915 let res = self.api_post("screenshot", data, content_type).await?;
916 res.json().await
917 }
918
919 pub async fn search(
932 &self,
933 q: &str,
934 params: Option<SearchRequestParams>,
935 _stream: bool,
936 content_type: &str,
937 ) -> Result<serde_json::Value, reqwest::Error> {
938 let body = match params {
939 Some(mut params) => {
940 params.search = q.to_string();
941 params
942 }
943 _ => {
944 let mut params = SearchRequestParams::default();
945 params.search = q.to_string();
946 params
947 }
948 };
949
950 let res = self.api_post("search", body, content_type).await?;
951
952 res.json().await
953 }
954
955 pub async fn transform(
968 &self,
969 data: Vec<HashMap<&str, &str>>,
970 params: Option<TransformParams>,
971 _stream: bool,
972 content_type: &str,
973 ) -> Result<serde_json::Value, reqwest::Error> {
974 let mut payload = HashMap::new();
975
976 if let Ok(params) = serde_json::to_value(params) {
977 if let Some(ref p) = params.as_object() {
978 payload.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
979 }
980 }
981
982 if let Ok(d) = serde_json::to_value(data) {
983 payload.insert("data".into(), d);
984 }
985
986 let res = self.api_post("transform", payload, content_type).await?;
987
988 res.json().await
989 }
990
991 pub async fn extract_contacts(
1004 &self,
1005 url: &str,
1006 params: Option<RequestParams>,
1007 _stream: bool,
1008 content_type: &str,
1009 ) -> Result<serde_json::Value, reqwest::Error> {
1010 let mut data = HashMap::new();
1011
1012 if let Ok(params) = serde_json::to_value(params) {
1013 if let Ok(params) = serde_json::to_value(params) {
1014 if let Some(ref p) = params.as_object() {
1015 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1016 }
1017 }
1018 }
1019
1020 match serde_json::to_value(url) {
1021 Ok(u) => {
1022 data.insert("url".into(), u);
1023 }
1024 _ => (),
1025 }
1026
1027 let res = self
1028 .api_post("pipeline/extract-contacts", data, content_type)
1029 .await?;
1030 res.json().await
1031 }
1032
1033 pub async fn label(
1046 &self,
1047 url: &str,
1048 params: Option<RequestParams>,
1049 _stream: bool,
1050 content_type: &str,
1051 ) -> Result<serde_json::Value, reqwest::Error> {
1052 let mut data = HashMap::new();
1053
1054 if let Ok(params) = serde_json::to_value(params) {
1055 if let Ok(params) = serde_json::to_value(params) {
1056 if let Some(ref p) = params.as_object() {
1057 data.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1058 }
1059 }
1060 }
1061
1062 data.insert("url".into(), serde_json::Value::String(url.to_string()));
1063
1064 let res = self.api_post("pipeline/label", data, content_type).await?;
1065 res.json().await
1066 }
1067
1068 pub async fn download(
1080 &self,
1081 url: Option<&str>,
1082 options: Option<HashMap<&str, i32>>,
1083 ) -> Result<reqwest::Response, reqwest::Error> {
1084 let mut params = HashMap::new();
1085
1086 if let Some(url) = url {
1087 params.insert("url".to_string(), url.to_string());
1088 }
1089
1090 if let Some(options) = options {
1091 for (key, value) in options {
1092 params.insert(key.to_string(), value.to_string());
1093 }
1094 }
1095
1096 let url = format!("{API_URL}/v1/data/download");
1097 let request = self
1098 .client
1099 .get(&url)
1100 .header(
1101 "User-Agent",
1102 format!("Spider-Client/{}", env!("CARGO_PKG_VERSION")),
1103 )
1104 .header("Content-Type", "application/octet-stream")
1105 .header("Authorization", format!("Bearer {}", self.api_key))
1106 .query(¶ms);
1107
1108 let res = request.send().await?;
1109
1110 Ok(res)
1111 }
1112
1113 pub async fn create_signed_url(
1125 &self,
1126 url: Option<&str>,
1127 options: Option<HashMap<&str, i32>>,
1128 ) -> Result<serde_json::Value, reqwest::Error> {
1129 let mut params = HashMap::new();
1130
1131 if let Some(options) = options {
1132 for (key, value) in options {
1133 params.insert(key.to_string(), value.to_string());
1134 }
1135 }
1136
1137 if let Some(url) = url {
1138 params.insert("url".to_string(), url.to_string());
1139 }
1140
1141 let url = format!("{API_URL}/v1/data/sign-url");
1142 let request = self
1143 .client
1144 .get(&url)
1145 .header(
1146 "User-Agent",
1147 format!("Spider-Client/{}", env!("CARGO_PKG_VERSION")),
1148 )
1149 .header("Authorization", format!("Bearer {}", self.api_key))
1150 .query(¶ms);
1151
1152 let res = request.send().await?;
1153
1154 res.json().await
1155 }
1156
1157 pub async fn get_crawl_state(
1169 &self,
1170 url: &str,
1171 params: Option<RequestParams>,
1172 content_type: &str,
1173 ) -> Result<serde_json::Value, reqwest::Error> {
1174 let mut payload = HashMap::new();
1175 payload.insert("url".into(), serde_json::Value::String(url.to_string()));
1176 payload.insert(
1177 "contentType".into(),
1178 serde_json::Value::String(content_type.to_string()),
1179 );
1180
1181 if let Ok(params) = serde_json::to_value(params) {
1182 if let Ok(params) = serde_json::to_value(params) {
1183 if let Some(ref p) = params.as_object() {
1184 payload.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1185 }
1186 }
1187 }
1188
1189 let res = self
1190 .api_post("data/crawl_state", payload, content_type)
1191 .await?;
1192 res.json().await
1193 }
1194
1195 pub async fn get_credits(&self) -> Result<serde_json::Value, reqwest::Error> {
1197 self.api_get::<serde_json::Value>("data/credits", None)
1198 .await
1199 }
1200
1201 pub async fn data_post(
1203 &self,
1204 table: &str,
1205 data: Option<RequestParams>,
1206 ) -> Result<serde_json::Value, reqwest::Error> {
1207 let res = self
1208 .api_post(&format!("data/{}", table), data, "application/json")
1209 .await?;
1210 res.json().await
1211 }
1212
1213 pub async fn query(&self, params: &QueryRequest) -> Result<serde_json::Value, reqwest::Error> {
1215 let res = self
1216 .api_get::<QueryRequest>(&"data/query", Some(params))
1217 .await?;
1218
1219 Ok(res)
1220 }
1221
1222 pub async fn data_get(
1224 &self,
1225 table: &str,
1226 params: Option<RequestParams>,
1227 ) -> Result<serde_json::Value, reqwest::Error> {
1228 let mut payload = HashMap::new();
1229
1230 if let Some(params) = params {
1231 if let Ok(p) = serde_json::to_value(params) {
1232 if let Some(o) = p.as_object() {
1233 payload.extend(o.iter().map(|(k, v)| (k.as_str(), v.clone())));
1234 }
1235 }
1236 }
1237
1238 let res = self
1239 .api_get::<serde_json::Value>(&format!("data/{}", table), None)
1240 .await?;
1241 Ok(res)
1242 }
1243
1244 pub async fn data_delete(
1246 &self,
1247 table: &str,
1248 params: Option<RequestParams>,
1249 ) -> Result<serde_json::Value, reqwest::Error> {
1250 let mut payload = HashMap::new();
1251
1252 if let Ok(params) = serde_json::to_value(params) {
1253 if let Ok(params) = serde_json::to_value(params) {
1254 if let Some(ref p) = params.as_object() {
1255 payload.extend(p.iter().map(|(k, v)| (k.to_string(), v.clone())));
1256 }
1257 }
1258 }
1259
1260 let res = self
1261 .api_delete(&format!("data/{}", table), Some(payload))
1262 .await?;
1263 res.json().await
1264 }
1265}
1266
1267#[cfg(test)]
1268mod tests {
1269 use super::*;
1270 use dotenv::dotenv;
1271 use lazy_static::lazy_static;
1272 use reqwest::ClientBuilder;
1273
1274 lazy_static! {
1275 static ref SPIDER_CLIENT: Spider = {
1276 dotenv().ok();
1277 let client = ClientBuilder::new();
1278 let client = client.user_agent("SpiderBot").build().unwrap();
1279
1280 Spider::new_with_client(None, client).expect("client to build")
1281 };
1282 }
1283
1284 #[tokio::test]
1285 #[ignore]
1286 async fn test_scrape_url() {
1287 let response = SPIDER_CLIENT
1288 .scrape_url("https://example.com", None, "application/json")
1289 .await;
1290 assert!(response.is_ok());
1291 }
1292
1293 #[tokio::test]
1294 async fn test_crawl_url() {
1295 let response = SPIDER_CLIENT
1296 .crawl_url(
1297 "https://example.com",
1298 None,
1299 false,
1300 "application/json",
1301 None::<fn(serde_json::Value)>,
1302 )
1303 .await;
1304 assert!(response.is_ok());
1305 }
1306
1307 #[tokio::test]
1308 #[ignore]
1309 async fn test_links() {
1310 let response: Result<serde_json::Value, Error> = SPIDER_CLIENT
1311 .links("https://example.com", None, false, "application/json")
1312 .await;
1313 assert!(response.is_ok());
1314 }
1315
1316 #[tokio::test]
1317 #[ignore]
1318 async fn test_screenshot() {
1319 let mut params = RequestParams::default();
1320 params.limit = Some(1);
1321
1322 let response = SPIDER_CLIENT
1323 .screenshot(
1324 "https://example.com",
1325 Some(params),
1326 false,
1327 "application/json",
1328 )
1329 .await;
1330 assert!(response.is_ok());
1331 }
1332
1333 #[tokio::test]
1349 #[ignore]
1350 async fn test_transform() {
1351 let data = vec![HashMap::from([(
1352 "<html><body><h1>Transformation</h1></body></html>".into(),
1353 "".into(),
1354 )])];
1355 let response = SPIDER_CLIENT
1356 .transform(data, None, false, "application/json")
1357 .await;
1358 assert!(response.is_ok());
1359 }
1360
1361 #[tokio::test]
1362 #[ignore]
1363 async fn test_extract_contacts() {
1364 let response = SPIDER_CLIENT
1365 .extract_contacts("https://example.com", None, false, "application/json")
1366 .await;
1367 assert!(response.is_ok());
1368 }
1369
1370 #[tokio::test]
1371 #[ignore]
1372 async fn test_label() {
1373 let response = SPIDER_CLIENT
1374 .label("https://example.com", None, false, "application/json")
1375 .await;
1376 assert!(response.is_ok());
1377 }
1378
1379 #[tokio::test]
1380 async fn test_create_signed_url() {
1381 let response = SPIDER_CLIENT
1382 .create_signed_url(Some("example.com"), None)
1383 .await;
1384 assert!(response.is_ok());
1385 }
1386
1387 #[tokio::test]
1388 async fn test_get_crawl_state() {
1389 let response = SPIDER_CLIENT
1390 .get_crawl_state("https://example.com", None, "application/json")
1391 .await;
1392 assert!(response.is_ok());
1393 }
1394
1395 #[tokio::test]
1396 async fn test_query() {
1397 let mut query = QueryRequest::default();
1398
1399 query.domain = Some("spider.cloud".into());
1400
1401 let response = SPIDER_CLIENT.query(&query).await;
1402 assert!(response.is_ok());
1403 }
1404
1405 #[tokio::test]
1406 async fn test_get_credits() {
1407 let response = SPIDER_CLIENT.get_credits().await;
1408 assert!(response.is_ok());
1409 }
1410}