Skip to main content

browserflare/
payloads.rs

1use serde::{Deserialize, Deserializer, Serialize};
2use serde_json::{Map, Value};
3
4// ── Shared sub-structs ──────────────────────────────────────────────────
5
6#[derive(Debug, Clone, Serialize, Deserialize, Default)]
7#[serde(rename_all = "camelCase")]
8pub struct Viewport {
9    pub width: u32,
10    pub height: u32,
11    #[serde(skip_serializing_if = "Option::is_none")]
12    pub device_scale_factor: Option<f64>,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize, Default)]
16#[serde(rename_all = "camelCase")]
17pub struct WaitForSelector {
18    pub selector: String,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub timeout: Option<u64>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, Default)]
24#[serde(rename_all = "camelCase")]
25pub struct GotoOptions {
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub wait_until: Option<String>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub timeout: Option<u64>,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub referer: Option<String>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize, Default)]
35pub struct JsonOptions {
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub prompt: Option<String>,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub schema: Option<Value>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, Default)]
43#[serde(rename_all = "camelCase")]
44pub struct PdfMargin {
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub top: Option<String>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub bottom: Option<String>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub left: Option<String>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub right: Option<String>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, Default)]
56#[serde(rename_all = "camelCase")]
57pub struct PdfOptions {
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub format: Option<String>,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub landscape: Option<bool>,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub print_background: Option<bool>,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub scale: Option<f64>,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub display_header_footer: Option<bool>,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub header_template: Option<String>,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub footer_template: Option<String>,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub margin: Option<PdfMargin>,
74}
75
76// ── Crawl payload ───────────────────────────────────────────────────────
77
78#[derive(Debug, Clone, Serialize, Deserialize, Default)]
79#[serde(rename_all = "camelCase")]
80pub struct CrawlPayload {
81    pub url: String,
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub limit: Option<u32>,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub formats: Option<Vec<String>>,
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub reject_resource_types: Option<Vec<String>>,
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub depth: Option<u32>,
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub source: Option<String>,
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub render: Option<bool>,
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub include_external_links: Option<bool>,
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub include_subdomains: Option<bool>,
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub include_patterns: Option<Vec<String>>,
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub exclude_patterns: Option<Vec<String>>,
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub max_age: Option<u64>,
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub modified_since: Option<i64>,
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub wait_for_selector: Option<WaitForSelector>,
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub json_options: Option<JsonOptions>,
110    #[serde(flatten, skip_serializing_if = "Option::is_none")]
111    pub extra: Option<Map<String, Value>>,
112}
113
114// ── Screenshot payload ──────────────────────────────────────────────────
115
116#[derive(Debug, Clone, Serialize, Default)]
117#[serde(rename_all = "camelCase")]
118pub struct ScreenshotOptions {
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub full_page: Option<bool>,
121    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
122    pub format: Option<String>,
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub quality: Option<u32>,
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub omit_background: Option<bool>,
127}
128
129#[derive(Debug, Clone, Serialize, Default)]
130#[serde(rename_all = "camelCase")]
131pub struct ScreenshotPayload {
132    pub url: String,
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub viewport: Option<Viewport>,
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub screenshot_options: Option<ScreenshotOptions>,
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub selector: Option<String>,
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub wait_for_selector: Option<WaitForSelector>,
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub goto_options: Option<GotoOptions>,
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub user_agent: Option<String>,
145}
146
147// ── PDF payload ─────────────────────────────────────────────────────────
148
149#[derive(Debug, Clone, Serialize, Default)]
150#[serde(rename_all = "camelCase")]
151pub struct PdfPayload {
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub url: Option<String>,
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub html: Option<String>,
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub viewport: Option<Viewport>,
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub pdf_options: Option<PdfOptions>,
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub wait_for_selector: Option<WaitForSelector>,
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub goto_options: Option<GotoOptions>,
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub user_agent: Option<String>,
166}
167
168// ── Content payload ────────────────────────────────────────────────────
169
170#[derive(Debug, Clone, Serialize, Default)]
171#[serde(rename_all = "camelCase")]
172pub struct ContentPayload {
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub url: Option<String>,
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub html: Option<String>,
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub viewport: Option<Viewport>,
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub goto_options: Option<GotoOptions>,
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub wait_for_selector: Option<WaitForSelector>,
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub user_agent: Option<String>,
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub reject_resource_types: Option<Vec<String>>,
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub reject_request_pattern: Option<Vec<String>>,
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub allow_resource_types: Option<Vec<String>>,
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub allow_request_pattern: Option<Vec<String>>,
193    #[serde(skip_serializing_if = "Option::is_none")]
194    #[serde(rename = "setExtraHTTPHeaders")]
195    pub set_extra_http_headers: Option<Map<String, Value>>,
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub cookies: Option<Vec<Value>>,
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub set_javascript_enabled: Option<bool>,
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub emulate_media_type: Option<String>,
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub best_attempt: Option<bool>,
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub action_timeout: Option<u64>,
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub wait_for_timeout: Option<u64>,
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub add_script_tag: Option<Vec<Value>>,
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub add_style_tag: Option<Vec<Value>>,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize, Default)]
215#[serde(rename_all = "camelCase")]
216pub struct ContentMeta {
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub status: Option<u16>,
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub title: Option<String>,
221}
222
223#[derive(Debug, Clone, Deserialize)]
224#[serde(rename_all = "camelCase")]
225pub struct ContentResult {
226    pub success: bool,
227    pub result: Option<String>,
228    pub errors: Option<Vec<Value>>,
229    pub meta: Option<ContentMeta>,
230}
231
232// ── Markdown payload ──────────────────────────────────────────────────
233
234#[derive(Debug, Clone, Serialize, Default)]
235#[serde(rename_all = "camelCase")]
236pub struct MarkdownPayload {
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub url: Option<String>,
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub html: Option<String>,
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub viewport: Option<Viewport>,
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub goto_options: Option<GotoOptions>,
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub wait_for_selector: Option<WaitForSelector>,
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub user_agent: Option<String>,
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub reject_resource_types: Option<Vec<String>>,
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub reject_request_pattern: Option<Vec<String>>,
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub allow_resource_types: Option<Vec<String>>,
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub allow_request_pattern: Option<Vec<String>>,
257    #[serde(skip_serializing_if = "Option::is_none")]
258    #[serde(rename = "setExtraHTTPHeaders")]
259    pub set_extra_http_headers: Option<Map<String, Value>>,
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub cookies: Option<Vec<Value>>,
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub set_javascript_enabled: Option<bool>,
264    #[serde(skip_serializing_if = "Option::is_none")]
265    pub emulate_media_type: Option<String>,
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub best_attempt: Option<bool>,
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub action_timeout: Option<u64>,
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub wait_for_timeout: Option<u64>,
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub add_script_tag: Option<Vec<Value>>,
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub add_style_tag: Option<Vec<Value>>,
276}
277
278#[derive(Debug, Clone, Deserialize)]
279#[serde(rename_all = "camelCase")]
280pub struct MarkdownResult {
281    pub success: bool,
282    pub result: Option<String>,
283    pub errors: Option<Vec<Value>>,
284    pub meta: Option<ContentMeta>,
285}
286
287// ── Snapshot payload ──────────────────────────────────────────────────
288
289#[derive(Debug, Clone, Serialize, Default)]
290#[serde(rename_all = "camelCase")]
291pub struct SnapshotPayload {
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub url: Option<String>,
294    #[serde(skip_serializing_if = "Option::is_none")]
295    pub html: Option<String>,
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub viewport: Option<Viewport>,
298    #[serde(skip_serializing_if = "Option::is_none")]
299    pub screenshot_options: Option<ScreenshotOptions>,
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub goto_options: Option<GotoOptions>,
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub wait_for_selector: Option<WaitForSelector>,
304    #[serde(skip_serializing_if = "Option::is_none")]
305    pub user_agent: Option<String>,
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub reject_resource_types: Option<Vec<String>>,
308    #[serde(skip_serializing_if = "Option::is_none")]
309    pub reject_request_pattern: Option<Vec<String>>,
310    #[serde(skip_serializing_if = "Option::is_none")]
311    pub allow_resource_types: Option<Vec<String>>,
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub allow_request_pattern: Option<Vec<String>>,
314    #[serde(skip_serializing_if = "Option::is_none")]
315    #[serde(rename = "setExtraHTTPHeaders")]
316    pub set_extra_http_headers: Option<Map<String, Value>>,
317    #[serde(skip_serializing_if = "Option::is_none")]
318    pub cookies: Option<Vec<Value>>,
319    #[serde(skip_serializing_if = "Option::is_none")]
320    pub set_javascript_enabled: Option<bool>,
321    #[serde(skip_serializing_if = "Option::is_none")]
322    pub emulate_media_type: Option<String>,
323    #[serde(skip_serializing_if = "Option::is_none")]
324    pub best_attempt: Option<bool>,
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub action_timeout: Option<u64>,
327    #[serde(skip_serializing_if = "Option::is_none")]
328    pub wait_for_timeout: Option<u64>,
329    #[serde(skip_serializing_if = "Option::is_none")]
330    pub add_script_tag: Option<Vec<Value>>,
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub add_style_tag: Option<Vec<Value>>,
333}
334
335#[derive(Debug, Clone, Deserialize)]
336#[serde(rename_all = "camelCase")]
337pub struct SnapshotResultData {
338    pub screenshot: String,
339    pub content: String,
340}
341
342#[derive(Debug, Clone, Deserialize)]
343#[serde(rename_all = "camelCase")]
344pub struct SnapshotResult {
345    pub success: bool,
346    pub result: Option<SnapshotResultData>,
347    pub errors: Option<Vec<Value>>,
348}
349
350// ── JSON endpoint payload ────────────────────────────────────────────
351
352#[derive(Debug, Clone, Serialize, Deserialize, Default)]
353pub struct ResponseFormat {
354    #[serde(rename = "type")]
355    pub format_type: String,
356    pub schema: Value,
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
360pub struct CustomAiModel {
361    pub model: String,
362    pub authorization: String,
363}
364
365#[derive(Debug, Clone, Serialize, Default)]
366#[serde(rename_all = "camelCase")]
367pub struct JsonPayload {
368    #[serde(skip_serializing_if = "Option::is_none")]
369    pub url: Option<String>,
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub html: Option<String>,
372    #[serde(skip_serializing_if = "Option::is_none")]
373    pub prompt: Option<String>,
374    #[serde(rename = "response_format", skip_serializing_if = "Option::is_none")]
375    pub response_format: Option<ResponseFormat>,
376    #[serde(rename = "custom_ai", skip_serializing_if = "Option::is_none")]
377    pub custom_ai: Option<Vec<CustomAiModel>>,
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub viewport: Option<Viewport>,
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub goto_options: Option<GotoOptions>,
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub wait_for_selector: Option<WaitForSelector>,
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub user_agent: Option<String>,
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub reject_resource_types: Option<Vec<String>>,
388    #[serde(skip_serializing_if = "Option::is_none")]
389    pub reject_request_pattern: Option<Vec<String>>,
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub allow_resource_types: Option<Vec<String>>,
392    #[serde(skip_serializing_if = "Option::is_none")]
393    pub allow_request_pattern: Option<Vec<String>>,
394    #[serde(skip_serializing_if = "Option::is_none")]
395    #[serde(rename = "setExtraHTTPHeaders")]
396    pub set_extra_http_headers: Option<Map<String, Value>>,
397    #[serde(skip_serializing_if = "Option::is_none")]
398    pub cookies: Option<Vec<Value>>,
399    #[serde(skip_serializing_if = "Option::is_none")]
400    pub set_javascript_enabled: Option<bool>,
401    #[serde(skip_serializing_if = "Option::is_none")]
402    pub emulate_media_type: Option<String>,
403    #[serde(skip_serializing_if = "Option::is_none")]
404    pub best_attempt: Option<bool>,
405    #[serde(skip_serializing_if = "Option::is_none")]
406    pub action_timeout: Option<u64>,
407    #[serde(skip_serializing_if = "Option::is_none")]
408    pub wait_for_timeout: Option<u64>,
409    #[serde(skip_serializing_if = "Option::is_none")]
410    pub add_script_tag: Option<Vec<Value>>,
411    #[serde(skip_serializing_if = "Option::is_none")]
412    pub add_style_tag: Option<Vec<Value>>,
413}
414
415#[derive(Debug, Clone, Deserialize)]
416#[serde(rename_all = "camelCase")]
417pub struct JsonExtractResult {
418    pub success: bool,
419    pub result: Option<Value>,
420    pub errors: Option<Vec<Value>>,
421}
422
423// ── Links payload ────────────────────────────────────────────────────
424
425#[derive(Debug, Clone, Serialize, Default)]
426#[serde(rename_all = "camelCase")]
427pub struct LinksPayload {
428    #[serde(skip_serializing_if = "Option::is_none")]
429    pub url: Option<String>,
430    #[serde(skip_serializing_if = "Option::is_none")]
431    pub html: Option<String>,
432    #[serde(skip_serializing_if = "Option::is_none")]
433    pub visible_links_only: Option<bool>,
434    #[serde(skip_serializing_if = "Option::is_none")]
435    pub exclude_external_links: Option<bool>,
436    #[serde(skip_serializing_if = "Option::is_none")]
437    pub viewport: Option<Viewport>,
438    #[serde(skip_serializing_if = "Option::is_none")]
439    pub goto_options: Option<GotoOptions>,
440    #[serde(skip_serializing_if = "Option::is_none")]
441    pub wait_for_selector: Option<WaitForSelector>,
442    #[serde(skip_serializing_if = "Option::is_none")]
443    pub user_agent: Option<String>,
444    #[serde(skip_serializing_if = "Option::is_none")]
445    pub reject_resource_types: Option<Vec<String>>,
446    #[serde(skip_serializing_if = "Option::is_none")]
447    pub reject_request_pattern: Option<Vec<String>>,
448    #[serde(skip_serializing_if = "Option::is_none")]
449    pub allow_resource_types: Option<Vec<String>>,
450    #[serde(skip_serializing_if = "Option::is_none")]
451    pub allow_request_pattern: Option<Vec<String>>,
452    #[serde(skip_serializing_if = "Option::is_none")]
453    #[serde(rename = "setExtraHTTPHeaders")]
454    pub set_extra_http_headers: Option<Map<String, Value>>,
455    #[serde(skip_serializing_if = "Option::is_none")]
456    pub cookies: Option<Vec<Value>>,
457    #[serde(skip_serializing_if = "Option::is_none")]
458    pub set_javascript_enabled: Option<bool>,
459    #[serde(skip_serializing_if = "Option::is_none")]
460    pub emulate_media_type: Option<String>,
461    #[serde(skip_serializing_if = "Option::is_none")]
462    pub best_attempt: Option<bool>,
463    #[serde(skip_serializing_if = "Option::is_none")]
464    pub action_timeout: Option<u64>,
465    #[serde(skip_serializing_if = "Option::is_none")]
466    pub wait_for_timeout: Option<u64>,
467    #[serde(skip_serializing_if = "Option::is_none")]
468    pub add_script_tag: Option<Vec<Value>>,
469    #[serde(skip_serializing_if = "Option::is_none")]
470    pub add_style_tag: Option<Vec<Value>>,
471}
472
473#[derive(Debug, Clone, Deserialize)]
474#[serde(rename_all = "camelCase")]
475pub struct LinksResult {
476    pub success: bool,
477    pub result: Option<Vec<String>>,
478    pub errors: Option<Vec<Value>>,
479}
480
481// ── Scrape payload ───────────────────────────────────────────────────
482
483#[derive(Debug, Clone, Serialize, Deserialize, Default)]
484#[serde(rename_all = "camelCase")]
485pub struct ScrapeElement {
486    pub selector: String,
487}
488
489#[derive(Debug, Clone, Serialize, Default)]
490#[serde(rename_all = "camelCase")]
491pub struct ScrapePayload {
492    #[serde(skip_serializing_if = "Option::is_none")]
493    pub url: Option<String>,
494    pub elements: Vec<ScrapeElement>,
495    #[serde(skip_serializing_if = "Option::is_none")]
496    pub viewport: Option<Viewport>,
497    #[serde(skip_serializing_if = "Option::is_none")]
498    pub goto_options: Option<GotoOptions>,
499    #[serde(skip_serializing_if = "Option::is_none")]
500    pub wait_for_selector: Option<WaitForSelector>,
501    #[serde(skip_serializing_if = "Option::is_none")]
502    pub user_agent: Option<String>,
503    #[serde(skip_serializing_if = "Option::is_none")]
504    pub reject_resource_types: Option<Vec<String>>,
505    #[serde(skip_serializing_if = "Option::is_none")]
506    pub reject_request_pattern: Option<Vec<String>>,
507    #[serde(skip_serializing_if = "Option::is_none")]
508    pub allow_resource_types: Option<Vec<String>>,
509    #[serde(skip_serializing_if = "Option::is_none")]
510    pub allow_request_pattern: Option<Vec<String>>,
511    #[serde(skip_serializing_if = "Option::is_none")]
512    #[serde(rename = "setExtraHTTPHeaders")]
513    pub set_extra_http_headers: Option<Map<String, Value>>,
514    #[serde(skip_serializing_if = "Option::is_none")]
515    pub cookies: Option<Vec<Value>>,
516    #[serde(skip_serializing_if = "Option::is_none")]
517    pub set_javascript_enabled: Option<bool>,
518    #[serde(skip_serializing_if = "Option::is_none")]
519    pub emulate_media_type: Option<String>,
520    #[serde(skip_serializing_if = "Option::is_none")]
521    pub best_attempt: Option<bool>,
522    #[serde(skip_serializing_if = "Option::is_none")]
523    pub action_timeout: Option<u64>,
524    #[serde(skip_serializing_if = "Option::is_none")]
525    pub wait_for_timeout: Option<u64>,
526    #[serde(skip_serializing_if = "Option::is_none")]
527    pub add_script_tag: Option<Vec<Value>>,
528    #[serde(skip_serializing_if = "Option::is_none")]
529    pub add_style_tag: Option<Vec<Value>>,
530}
531
532#[derive(Debug, Clone, Deserialize)]
533#[serde(rename_all = "camelCase")]
534pub struct ScrapeAttribute {
535    pub name: String,
536    pub value: String,
537}
538
539#[derive(Debug, Clone, Deserialize)]
540#[serde(rename_all = "camelCase")]
541pub struct ScrapeElementResult {
542    pub text: Option<String>,
543    pub html: Option<String>,
544    #[serde(default)]
545    pub attributes: Vec<ScrapeAttribute>,
546    pub height: Option<f64>,
547    pub width: Option<f64>,
548    pub top: Option<f64>,
549    pub left: Option<f64>,
550}
551
552#[derive(Debug, Clone, Deserialize)]
553#[serde(rename_all = "camelCase")]
554pub struct ScrapeSelectorResult {
555    pub selector: String,
556    pub results: Vec<ScrapeElementResult>,
557}
558
559#[derive(Debug, Clone, Deserialize)]
560#[serde(rename_all = "camelCase")]
561pub struct ScrapeResult {
562    pub success: bool,
563    pub result: Option<Vec<ScrapeSelectorResult>>,
564    pub errors: Option<Vec<Value>>,
565}
566
567// ── Helpers ─────────────────────────────────────────────────────────────
568
569fn deserialize_cursor<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
570where
571    D: Deserializer<'de>,
572{
573    let value: Option<Value> = Option::deserialize(deserializer)?;
574    Ok(value.and_then(|v| match v {
575        Value::String(s) if s.is_empty() => None,
576        Value::String(s) => Some(s),
577        Value::Number(n) => Some(n.to_string()),
578        Value::Null => None,
579        other => Some(other.to_string()),
580    }))
581}
582
583// ── Response types ──────────────────────────────────────────────────────
584
585#[derive(Debug, Clone, Serialize, Deserialize)]
586pub struct CfApiResponse<T> {
587    pub success: bool,
588    pub result: Option<T>,
589    pub errors: Option<Vec<Value>>,
590    pub messages: Option<Vec<Value>>,
591}
592
593#[derive(Debug, Clone, Serialize, Deserialize)]
594#[serde(rename_all = "camelCase")]
595pub struct CrawlResult {
596    #[serde(default)]
597    pub status: String,
598    #[serde(default)]
599    pub records: Vec<CrawlRecord>,
600    #[serde(skip_serializing_if = "Option::is_none")]
601    pub total: Option<u64>,
602    #[serde(default, deserialize_with = "deserialize_cursor", skip_serializing_if = "Option::is_none")]
603    pub cursor: Option<String>,
604    #[serde(skip_serializing_if = "Option::is_none")]
605    pub browser_seconds_used: Option<f64>,
606    #[serde(flatten)]
607    pub extra: Map<String, Value>,
608}
609
610#[derive(Debug, Clone, Serialize, Deserialize)]
611#[serde(rename_all = "camelCase")]
612pub struct CrawlRecord {
613    #[serde(default)]
614    pub url: String,
615    #[serde(skip_serializing_if = "Option::is_none")]
616    pub status: Option<String>,
617    #[serde(skip_serializing_if = "Option::is_none")]
618    pub html: Option<String>,
619    #[serde(skip_serializing_if = "Option::is_none")]
620    pub markdown: Option<String>,
621    #[serde(skip_serializing_if = "Option::is_none")]
622    pub json: Option<Value>,
623    #[serde(flatten)]
624    pub extra: Map<String, Value>,
625}
626
627#[derive(Debug)]
628pub struct ScreenshotResult {
629    pub bytes: Vec<u8>,
630    pub content_type: String,
631}
632
633#[derive(Debug)]
634pub struct PdfResult {
635    pub bytes: Vec<u8>,
636    pub content_type: String,
637}
638
639#[cfg(test)]
640mod tests {
641    use super::*;
642
643    #[test]
644    fn crawl_payload_serializes_with_defaults() {
645        let payload = CrawlPayload {
646            url: "https://example.com".into(),
647            limit: Some(100),
648            ..Default::default()
649        };
650        let json = serde_json::to_value(&payload).unwrap();
651        assert_eq!(json["url"], "https://example.com");
652        assert_eq!(json["limit"], 100);
653        assert!(json.get("formats").is_none());
654        assert!(json.get("depth").is_none());
655    }
656
657    #[test]
658    fn screenshot_payload_nests_options_under_screenshot_options() {
659        let payload = ScreenshotPayload {
660            url: "https://example.com".into(),
661            screenshot_options: Some(ScreenshotOptions {
662                format: Some("jpeg".into()),
663                full_page: Some(true),
664                quality: Some(80),
665                omit_background: None,
666            }),
667            ..Default::default()
668        };
669        let json = serde_json::to_value(&payload).unwrap();
670        assert_eq!(json["screenshotOptions"]["type"], "jpeg");
671        assert_eq!(json["screenshotOptions"]["fullPage"], true);
672        assert_eq!(json["screenshotOptions"]["quality"], 80);
673        assert!(json.get("fullPage").is_none());
674        assert!(json.get("type").is_none());
675        assert!(json.get("quality").is_none());
676    }
677
678    #[test]
679    fn pdf_payload_skips_none_fields() {
680        let payload = PdfPayload {
681            url: Some("https://example.com".into()),
682            ..Default::default()
683        };
684        let json = serde_json::to_value(&payload).unwrap();
685        assert_eq!(json["url"], "https://example.com");
686        assert!(json.get("html").is_none());
687        assert!(json.get("pdfOptions").is_none());
688    }
689
690    #[test]
691    fn crawl_payload_with_extra_fields() {
692        let mut extra = Map::new();
693        extra.insert("customField".into(), Value::Bool(true));
694        let payload = CrawlPayload {
695            url: "https://example.com".into(),
696            extra: Some(extra),
697            ..Default::default()
698        };
699        let json = serde_json::to_value(&payload).unwrap();
700        assert_eq!(json["customField"], true);
701    }
702}