1use serde::de::DeserializeOwned;
4use serde::Serialize;
5use serde_json::Value;
6
7use crate::clients::base::{delete_resource, get_resource, update_resource, ResourceContext};
8use crate::common::{parse_data_envelope, sign_storage_content, PaginationList, QueryParams};
9use crate::error::ApifyClientResult;
10use crate::http_client::{HttpClient, HttpMethod, HttpRequest};
11use crate::models::Dataset;
12
13#[derive(Debug, Default, Clone)]
18pub struct DatasetListItemsOptions {
19 pub offset: Option<i64>,
21 pub limit: Option<i64>,
23 pub desc: Option<bool>,
25 pub fields: Option<Vec<String>>,
27 pub output_fields: Option<Vec<String>>,
30 pub omit: Option<Vec<String>>,
32 pub skip_empty: Option<bool>,
34 pub skip_hidden: Option<bool>,
36 pub clean: Option<bool>,
38 pub unwind: Option<Vec<String>>,
40 pub flatten: Option<Vec<String>>,
42 pub view: Option<String>,
44 pub simplified: Option<bool>,
46 pub skip_failed_pages: Option<bool>,
48 pub signature: Option<String>,
50}
51
52impl DatasetListItemsOptions {
53 fn apply(&self, params: &mut QueryParams) {
54 params
55 .add_int("offset", self.offset)
56 .add_int("limit", self.limit)
57 .add_bool("desc", self.desc)
58 .add_csv("fields", self.fields.as_deref())
59 .add_csv("outputFields", self.output_fields.as_deref())
60 .add_csv("omit", self.omit.as_deref())
61 .add_bool("skipEmpty", self.skip_empty)
62 .add_bool("skipHidden", self.skip_hidden)
63 .add_bool("clean", self.clean)
64 .add_csv("unwind", self.unwind.as_deref())
65 .add_csv("flatten", self.flatten.as_deref())
66 .add_str("view", self.view.clone())
67 .add_bool("simplified", self.simplified)
68 .add_bool("skipFailedPages", self.skip_failed_pages)
69 .add_str("signature", self.signature.clone());
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum DownloadItemsFormat {
76 Json,
78 Jsonl,
80 Csv,
82 Xlsx,
84 Xml,
86 Rss,
88 Html,
90}
91
92impl DownloadItemsFormat {
93 fn as_str(&self) -> &'static str {
94 match self {
95 DownloadItemsFormat::Json => "json",
96 DownloadItemsFormat::Jsonl => "jsonl",
97 DownloadItemsFormat::Csv => "csv",
98 DownloadItemsFormat::Xlsx => "xlsx",
99 DownloadItemsFormat::Xml => "xml",
100 DownloadItemsFormat::Rss => "rss",
101 DownloadItemsFormat::Html => "html",
102 }
103 }
104}
105
106#[derive(Debug, Default, Clone)]
109pub struct DatasetDownloadOptions {
110 pub items: DatasetListItemsOptions,
112 pub attachment: Option<bool>,
114 pub bom: Option<bool>,
116 pub delimiter: Option<String>,
118 pub skip_header_row: Option<bool>,
120 pub xml_root: Option<String>,
122 pub xml_row: Option<String>,
124 pub feed_title: Option<String>,
126 pub feed_description: Option<String>,
128}
129
130impl DatasetDownloadOptions {
131 fn apply(&self, params: &mut QueryParams) {
132 self.items.apply(params);
133 params
134 .add_bool("attachment", self.attachment)
135 .add_bool("bom", self.bom)
136 .add_str("delimiter", self.delimiter.clone())
137 .add_bool("skipHeaderRow", self.skip_header_row)
138 .add_str("xmlRoot", self.xml_root.clone())
139 .add_str("xmlRow", self.xml_row.clone())
140 .add_str("feedTitle", self.feed_title.clone())
141 .add_str("feedDescription", self.feed_description.clone());
142 }
143}
144
145#[derive(Debug, Clone)]
147pub struct DatasetClient {
148 ctx: ResourceContext,
149}
150
151impl DatasetClient {
152 pub(crate) fn new(http: HttpClient, base_url: &str, resource_path: &str, id: &str) -> Self {
153 Self {
154 ctx: ResourceContext::single(http, base_url, resource_path, id),
155 }
156 }
157
158 pub(crate) fn nested(http: HttpClient, base_url: &str, sub_path: &str) -> Self {
160 Self {
161 ctx: ResourceContext::collection(http, base_url, sub_path),
162 }
163 }
164
165 pub(crate) fn with_public_base(mut self, public_base_url: &str) -> Self {
167 self.ctx = self.ctx.with_public_origin(public_base_url);
168 self
169 }
170
171 pub async fn get(&self) -> ApifyClientResult<Option<Dataset>> {
173 get_resource(&self.ctx, None, &QueryParams::new()).await
174 }
175
176 pub async fn update<T: Serialize>(&self, new_fields: &T) -> ApifyClientResult<Dataset> {
178 update_resource(&self.ctx, None, new_fields).await
179 }
180
181 pub async fn delete(&self) -> ApifyClientResult<()> {
183 delete_resource(&self.ctx, None).await
184 }
185
186 pub async fn list_items<T: DeserializeOwned>(
192 &self,
193 options: DatasetListItemsOptions,
194 ) -> ApifyClientResult<PaginationList<T>> {
195 let mut params = QueryParams::new();
196 options.apply(&mut params);
197 let url = params.apply_to_url(&self.ctx.url(Some("items")));
198 let response = self
199 .ctx
200 .http
201 .call(HttpRequest {
202 method: HttpMethod::Get,
203 url,
204 headers: Default::default(),
205 body: None,
206 timeout: crate::clients::base::DEFAULT_REQUEST_TIMEOUT,
207 })
208 .await?;
209
210 let items: Vec<T> = serde_json::from_slice(&response.body)?;
211 let count = items.len() as i64;
212 let total = response
213 .header("x-apify-pagination-total")
214 .and_then(|v| v.parse().ok())
215 .unwrap_or(count);
216 let offset = response
217 .header("x-apify-pagination-offset")
218 .and_then(|v| v.parse().ok())
219 .unwrap_or(0);
220 let limit = response
221 .header("x-apify-pagination-limit")
222 .and_then(|v| v.parse().ok())
223 .unwrap_or(count);
224
225 Ok(PaginationList {
226 total,
227 offset,
228 limit,
229 count,
230 desc: options.desc.unwrap_or(false),
231 items,
232 })
233 }
234
235 pub async fn download_items(
242 &self,
243 format: DownloadItemsFormat,
244 options: DatasetDownloadOptions,
245 ) -> ApifyClientResult<Vec<u8>> {
246 let mut params = QueryParams::new();
247 params.add_str("format", Some(format.as_str()));
248 options.apply(&mut params);
249 let url = params.apply_to_url(&self.ctx.url(Some("items")));
250 let response = self
251 .ctx
252 .http
253 .call(HttpRequest {
254 method: HttpMethod::Get,
255 url,
256 headers: Default::default(),
257 body: None,
258 timeout: crate::clients::base::DEFAULT_REQUEST_TIMEOUT,
259 })
260 .await?;
261 Ok(response.body)
262 }
263
264 pub async fn push_items<T: Serialize>(&self, items: &T) -> ApifyClientResult<()> {
268 let body = serde_json::to_vec(items)?;
269 let url = self.ctx.url(Some("items"));
270 let mut headers = std::collections::HashMap::new();
271 headers.insert(
272 "Content-Type".to_string(),
273 "application/json; charset=utf-8".to_string(),
274 );
275 self.ctx
276 .http
277 .call(HttpRequest {
278 method: HttpMethod::Post,
279 url,
280 headers,
281 body: Some(body),
282 timeout: crate::clients::base::DEFAULT_REQUEST_TIMEOUT,
283 })
284 .await?;
285 Ok(())
286 }
287
288 pub async fn create_items_public_url(
296 &self,
297 options: DatasetListItemsOptions,
298 expires_in_secs: Option<i64>,
299 ) -> ApifyClientResult<String> {
300 let mut params = QueryParams::new();
301 options.apply(&mut params);
302
303 if let Some(dataset) = self.get().await? {
304 if let Some(secret) = dataset
305 .extra
306 .get("urlSigningSecretKey")
307 .and_then(|v| v.as_str())
308 {
309 let signature = sign_storage_content(secret, &dataset.id, expires_in_secs);
310 params.add_str("signature", Some(signature));
311 }
312 }
313 Ok(params.apply_to_url(&self.ctx.public_url(Some("items"))))
314 }
315
316 pub async fn get_statistics(&self) -> ApifyClientResult<Option<Value>> {
318 let result: ApifyClientResult<Value> = async {
319 let response = self
320 .ctx
321 .http
322 .call(HttpRequest {
323 method: HttpMethod::Get,
324 url: self.ctx.url(Some("statistics")),
325 headers: Default::default(),
326 body: None,
327 timeout: crate::clients::base::DEFAULT_REQUEST_TIMEOUT,
328 })
329 .await?;
330 parse_data_envelope(&response.body)
331 }
332 .await;
333 crate::common::catch_not_found(result)
334 }
335}