Skip to main content

firecrawl/
types.rs

1//! Type definitions for Firecrawl API v2.
2
3use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
4use serde_json::Value;
5use std::collections::HashMap;
6
7use crate::serde_helpers::deserialize_string_or_array;
8
9/// Available output formats for scraping operations.
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub enum Format {
12    /// Markdown content of the page.
13    Markdown,
14    /// Filtered, content-only HTML.
15    Html,
16    /// Original, untouched HTML.
17    RawHtml,
18    /// List of URLs found on the page.
19    Links,
20    /// List of image URLs found on the page.
21    Images,
22    /// Screenshot of the visible viewport.
23    Screenshot,
24    /// AI-generated summary of the page content.
25    Summary,
26    /// Change tracking information.
27    ChangeTracking,
28    /// Structured JSON extraction via LLM.
29    Json,
30    /// Custom attribute extraction.
31    Attributes,
32    /// Brand analysis of the page.
33    Branding,
34    /// Product extraction from the page.
35    Product,
36    /// Menu extraction from the page.
37    Menu,
38    /// Audio extraction (MP3) from YouTube videos.
39    Audio,
40    /// Video extraction from supported video URLs.
41    Video,
42    /// Question answer generated from the page content.
43    Question(QuestionFormat),
44    /// Direct highlights selected from the page content.
45    Highlights(HighlightsFormat),
46    /// Deprecated query answer generated from the page content.
47    Query(QueryFormat),
48}
49
50impl Serialize for Format {
51    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
52    where
53        S: Serializer,
54    {
55        match self {
56            Format::Markdown => serializer.serialize_str("markdown"),
57            Format::Html => serializer.serialize_str("html"),
58            Format::RawHtml => serializer.serialize_str("rawHtml"),
59            Format::Links => serializer.serialize_str("links"),
60            Format::Images => serializer.serialize_str("images"),
61            Format::Screenshot => serializer.serialize_str("screenshot"),
62            Format::Summary => serializer.serialize_str("summary"),
63            Format::ChangeTracking => serializer.serialize_str("changeTracking"),
64            Format::Json => serializer.serialize_str("json"),
65            Format::Attributes => serializer.serialize_str("attributes"),
66            Format::Branding => serializer.serialize_str("branding"),
67            Format::Product => serializer.serialize_str("product"),
68            Format::Menu => serializer.serialize_str("menu"),
69            Format::Audio => serializer.serialize_str("audio"),
70            Format::Video => serializer.serialize_str("video"),
71            Format::Question(question) => question.serialize(serializer),
72            Format::Highlights(highlights) => highlights.serialize(serializer),
73            Format::Query(query) => query.serialize(serializer),
74        }
75    }
76}
77
78impl<'de> Deserialize<'de> for Format {
79    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
80    where
81        D: Deserializer<'de>,
82    {
83        let value = Value::deserialize(deserializer)?;
84        match value {
85            Value::String(format) => match format.as_str() {
86                "markdown" => Ok(Format::Markdown),
87                "html" => Ok(Format::Html),
88                "rawHtml" => Ok(Format::RawHtml),
89                "links" => Ok(Format::Links),
90                "images" => Ok(Format::Images),
91                "screenshot" => Ok(Format::Screenshot),
92                "summary" => Ok(Format::Summary),
93                "changeTracking" => Ok(Format::ChangeTracking),
94                "json" => Ok(Format::Json),
95                "attributes" => Ok(Format::Attributes),
96                "branding" => Ok(Format::Branding),
97                "product" => Ok(Format::Product),
98                "menu" => Ok(Format::Menu),
99                "audio" => Ok(Format::Audio),
100                "video" => Ok(Format::Video),
101                _ => Err(de::Error::custom(format!("unknown format: {}", format))),
102            },
103            Value::Object(_) => match value.get("type").and_then(Value::as_str) {
104                Some("question") => QuestionFormat::deserialize(value)
105                    .map(Format::Question)
106                    .map_err(de::Error::custom),
107                Some("highlights") => HighlightsFormat::deserialize(value)
108                    .map(Format::Highlights)
109                    .map_err(de::Error::custom),
110                Some("query") => QueryFormat::deserialize(value)
111                    .map(Format::Query)
112                    .map_err(de::Error::custom),
113                Some(format_type) => Err(de::Error::custom(format!(
114                    "unknown object format: {}",
115                    format_type
116                ))),
117                None => Err(de::Error::custom("object format must have a type")),
118            },
119            _ => Err(de::Error::custom("format must be a string or object")),
120        }
121    }
122}
123
124/// Question format for asking a question about page content.
125#[derive(Clone, Debug, PartialEq, Eq)]
126pub struct QuestionFormat {
127    pub question: String,
128}
129
130#[derive(Deserialize, Serialize)]
131#[serde(rename_all = "camelCase")]
132struct QuestionFormatWire {
133    #[serde(rename = "type")]
134    format_type: String,
135    question: String,
136}
137
138impl Serialize for QuestionFormat {
139    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
140    where
141        S: Serializer,
142    {
143        QuestionFormatWire {
144            format_type: "question".to_string(),
145            question: self.question.clone(),
146        }
147        .serialize(serializer)
148    }
149}
150
151impl<'de> Deserialize<'de> for QuestionFormat {
152    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
153    where
154        D: Deserializer<'de>,
155    {
156        let wire = QuestionFormatWire::deserialize(deserializer)?;
157        if wire.format_type != "question" {
158            return Err(de::Error::custom(
159                "question format object must have type question",
160            ));
161        }
162
163        Ok(Self {
164            question: wire.question,
165        })
166    }
167}
168
169/// Highlights format for selecting direct highlights from page content.
170#[derive(Clone, Debug, PartialEq, Eq)]
171pub struct HighlightsFormat {
172    pub query: String,
173}
174
175#[derive(Deserialize, Serialize)]
176#[serde(rename_all = "camelCase")]
177struct HighlightsFormatWire {
178    #[serde(rename = "type")]
179    format_type: String,
180    query: String,
181}
182
183impl Serialize for HighlightsFormat {
184    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
185    where
186        S: Serializer,
187    {
188        HighlightsFormatWire {
189            format_type: "highlights".to_string(),
190            query: self.query.clone(),
191        }
192        .serialize(serializer)
193    }
194}
195
196impl<'de> Deserialize<'de> for HighlightsFormat {
197    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
198    where
199        D: Deserializer<'de>,
200    {
201        let wire = HighlightsFormatWire::deserialize(deserializer)?;
202        if wire.format_type != "highlights" {
203            return Err(de::Error::custom(
204                "highlights format object must have type highlights",
205            ));
206        }
207
208        Ok(Self { query: wire.query })
209    }
210}
211
212/// Deprecated query format for asking a question about page content.
213#[derive(Clone, Debug, PartialEq, Eq)]
214pub struct QueryFormat {
215    pub prompt: String,
216    pub mode: Option<QueryFormatMode>,
217}
218
219#[derive(Deserialize, Serialize)]
220#[serde(rename_all = "camelCase")]
221struct QueryFormatWire {
222    #[serde(rename = "type")]
223    format_type: String,
224    prompt: String,
225    #[serde(skip_serializing_if = "Option::is_none")]
226    mode: Option<QueryFormatMode>,
227}
228
229impl Serialize for QueryFormat {
230    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
231    where
232        S: Serializer,
233    {
234        QueryFormatWire {
235            format_type: "query".to_string(),
236            prompt: self.prompt.clone(),
237            mode: self.mode,
238        }
239        .serialize(serializer)
240    }
241}
242
243impl<'de> Deserialize<'de> for QueryFormat {
244    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
245    where
246        D: Deserializer<'de>,
247    {
248        let wire = QueryFormatWire::deserialize(deserializer)?;
249        if wire.format_type != "query" {
250            return Err(de::Error::custom(
251                "query format object must have type query",
252            ));
253        }
254
255        Ok(Self {
256            prompt: wire.prompt,
257            mode: wire.mode,
258        })
259    }
260}
261
262/// Query answer mode.
263#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq)]
264pub enum QueryFormatMode {
265    #[serde(rename = "freeform")]
266    Freeform,
267    #[serde(rename = "directQuote")]
268    DirectQuote,
269}
270
271/// Viewport dimensions for screenshots.
272#[serde_with::skip_serializing_none]
273#[derive(Deserialize, Serialize, Debug, Default, Clone)]
274pub struct Viewport {
275    pub width: u32,
276    pub height: u32,
277}
278
279/// Screenshot format options.
280#[serde_with::skip_serializing_none]
281#[derive(Deserialize, Serialize, Debug, Default, Clone)]
282#[serde(rename_all = "camelCase")]
283pub struct ScreenshotOptions {
284    /// Take a full-page screenshot instead of just the visible viewport.
285    pub full_page: Option<bool>,
286    /// Quality of the screenshot (1-100).
287    pub quality: Option<u8>,
288    /// Custom viewport dimensions.
289    pub viewport: Option<Viewport>,
290}
291
292/// Change tracking format options.
293#[serde_with::skip_serializing_none]
294#[derive(Deserialize, Serialize, Debug, Default, Clone)]
295#[serde(rename_all = "camelCase")]
296pub struct ChangeTrackingOptions {
297    /// Modes for change tracking output.
298    pub modes: Option<Vec<ChangeTrackingMode>>,
299    /// JSON schema for structured change output.
300    pub schema: Option<Value>,
301    /// Prompt for LLM-based change analysis.
302    pub prompt: Option<String>,
303    /// Tag to identify this tracking session.
304    pub tag: Option<String>,
305}
306
307/// Available change tracking modes.
308#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq)]
309#[serde(rename_all = "kebab-case")]
310pub enum ChangeTrackingMode {
311    GitDiff,
312    Json,
313}
314
315/// Attribute extraction selector.
316#[serde_with::skip_serializing_none]
317#[derive(Deserialize, Serialize, Debug, Default, Clone)]
318pub struct AttributeSelector {
319    /// CSS selector for the element.
320    pub selector: String,
321    /// Attribute name to extract.
322    pub attribute: String,
323}
324
325/// JSON extraction options.
326#[serde_with::skip_serializing_none]
327#[derive(Deserialize, Serialize, Debug, Default, Clone)]
328#[serde(rename_all = "camelCase")]
329pub struct JsonOptions {
330    /// JSON schema the output should adhere to.
331    pub schema: Option<Value>,
332    /// System prompt for the LLM agent.
333    pub system_prompt: Option<String>,
334    /// Extraction prompt for the LLM agent.
335    pub prompt: Option<String>,
336}
337
338/// Location configuration for proxy routing.
339#[serde_with::skip_serializing_none]
340#[derive(Deserialize, Serialize, Debug, Default, Clone)]
341#[serde(rename_all = "camelCase")]
342pub struct LocationConfig {
343    /// Country code (ISO 3166-1 alpha-2).
344    pub country: Option<String>,
345    /// List of preferred language codes.
346    pub languages: Option<Vec<String>>,
347}
348
349/// Persistent browser profile for maintaining state across scrapes.
350#[serde_with::skip_serializing_none]
351#[derive(Deserialize, Serialize, Debug, Default, Clone)]
352#[serde(rename_all = "camelCase")]
353pub struct ProfileConfig {
354    /// Profile name (1–128 characters).
355    pub name: String,
356    /// Whether to persist changes made during the session (defaults to true).
357    pub save_changes: Option<bool>,
358}
359
360/// Proxy type for scraping.
361#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq)]
362#[serde(rename_all = "lowercase")]
363pub enum ProxyType {
364    Basic,
365    Stealth,
366    Enhanced,
367    Auto,
368}
369
370/// Browser action types for automation.
371#[derive(Deserialize, Serialize, Debug, Clone)]
372#[serde(tag = "type", rename_all = "camelCase")]
373pub enum Action {
374    /// Wait for a specified time or element.
375    Wait {
376        /// Milliseconds to wait.
377        #[serde(skip_serializing_if = "Option::is_none")]
378        milliseconds: Option<u32>,
379        /// CSS selector to wait for.
380        #[serde(skip_serializing_if = "Option::is_none")]
381        selector: Option<String>,
382    },
383    /// Take a screenshot.
384    Screenshot {
385        #[serde(skip_serializing_if = "Option::is_none")]
386        full_page: Option<bool>,
387        #[serde(skip_serializing_if = "Option::is_none")]
388        quality: Option<u8>,
389        #[serde(skip_serializing_if = "Option::is_none")]
390        viewport: Option<Viewport>,
391    },
392    /// Click an element.
393    Click {
394        /// CSS selector of the element to click.
395        selector: String,
396    },
397    /// Write text to the focused input.
398    Write {
399        /// Text to write.
400        text: String,
401    },
402    /// Press a keyboard key.
403    Press {
404        /// Key name to press.
405        key: String,
406    },
407    /// Scroll the page.
408    Scroll {
409        /// Direction to scroll.
410        direction: ScrollDirection,
411        /// Optional selector to scroll within.
412        #[serde(skip_serializing_if = "Option::is_none")]
413        selector: Option<String>,
414    },
415    /// Trigger a scrape action.
416    Scrape,
417    /// Execute custom JavaScript.
418    #[serde(rename = "executeJavascript")]
419    ExecuteJavascript {
420        /// JavaScript code to execute.
421        script: String,
422    },
423    /// Generate a PDF.
424    Pdf {
425        #[serde(skip_serializing_if = "Option::is_none")]
426        format: Option<PdfFormat>,
427        #[serde(skip_serializing_if = "Option::is_none")]
428        landscape: Option<bool>,
429        #[serde(skip_serializing_if = "Option::is_none")]
430        scale: Option<f32>,
431    },
432}
433
434/// Scroll direction for scroll actions.
435#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq)]
436#[serde(rename_all = "lowercase")]
437pub enum ScrollDirection {
438    Up,
439    Down,
440}
441
442/// PDF format options.
443#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq)]
444pub enum PdfFormat {
445    A0,
446    A1,
447    A2,
448    A3,
449    A4,
450    A5,
451    A6,
452    Letter,
453    Legal,
454    Tabloid,
455    Ledger,
456}
457
458/// Webhook configuration for async operations.
459#[serde_with::skip_serializing_none]
460#[derive(Deserialize, Serialize, Debug, Default, Clone)]
461#[serde(rename_all = "camelCase")]
462pub struct WebhookConfig {
463    /// URL to send webhook notifications to.
464    pub url: String,
465    /// Custom headers to include in webhook requests.
466    pub headers: Option<HashMap<String, String>>,
467    /// Custom metadata to include in webhook payloads.
468    pub metadata: Option<HashMap<String, String>>,
469    /// Event types to receive notifications for.
470    pub events: Option<Vec<WebhookEvent>>,
471}
472
473impl From<String> for WebhookConfig {
474    fn from(url: String) -> Self {
475        Self {
476            url,
477            ..Default::default()
478        }
479    }
480}
481
482impl From<&str> for WebhookConfig {
483    fn from(url: &str) -> Self {
484        Self {
485            url: url.to_string(),
486            ..Default::default()
487        }
488    }
489}
490
491/// Webhook event types for crawl/batch operations.
492#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq)]
493#[serde(rename_all = "camelCase")]
494pub enum WebhookEvent {
495    Completed,
496    Failed,
497    Page,
498    Started,
499}
500
501/// Agent-specific webhook event types.
502#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq)]
503#[serde(rename_all = "camelCase")]
504pub enum AgentWebhookEvent {
505    Started,
506    Action,
507    Completed,
508    Failed,
509    Cancelled,
510}
511
512/// Agent webhook configuration.
513#[serde_with::skip_serializing_none]
514#[derive(Deserialize, Serialize, Debug, Default, Clone)]
515#[serde(rename_all = "camelCase")]
516pub struct AgentWebhookConfig {
517    /// URL to send webhook notifications to.
518    pub url: String,
519    /// Custom headers to include in webhook requests.
520    pub headers: Option<HashMap<String, String>>,
521    /// Custom metadata to include in webhook payloads.
522    pub metadata: Option<HashMap<String, String>>,
523    /// Event types to receive notifications for.
524    pub events: Option<Vec<AgentWebhookEvent>>,
525}
526
527impl From<String> for AgentWebhookConfig {
528    fn from(url: String) -> Self {
529        Self {
530            url,
531            ..Default::default()
532        }
533    }
534}
535
536impl From<&str> for AgentWebhookConfig {
537    fn from(url: &str) -> Self {
538        Self {
539            url: url.to_string(),
540            ..Default::default()
541        }
542    }
543}
544
545/// Document metadata returned from scrape operations.
546#[serde_with::skip_serializing_none]
547#[derive(Deserialize, Serialize, Debug, Default, Clone)]
548#[serde(rename_all = "camelCase")]
549pub struct DocumentMetadata {
550    // Firecrawl specific
551    #[serde(rename = "sourceURL")]
552    pub source_url: Option<String>,
553    pub status_code: Option<u16>,
554    pub error: Option<String>,
555
556    // Basic meta tags
557    #[serde(default, deserialize_with = "deserialize_string_or_array")]
558    pub title: Option<String>,
559    #[serde(default, deserialize_with = "deserialize_string_or_array")]
560    pub description: Option<String>,
561    #[serde(default, deserialize_with = "deserialize_string_or_array")]
562    pub language: Option<String>,
563    #[serde(default, deserialize_with = "deserialize_string_or_array")]
564    pub keywords: Option<String>,
565    #[serde(default, deserialize_with = "deserialize_string_or_array")]
566    pub robots: Option<String>,
567
568    // OpenGraph namespace
569    #[serde(default, deserialize_with = "deserialize_string_or_array")]
570    pub og_title: Option<String>,
571    #[serde(default, deserialize_with = "deserialize_string_or_array")]
572    pub og_description: Option<String>,
573    #[serde(default, deserialize_with = "deserialize_string_or_array")]
574    pub og_url: Option<String>,
575    #[serde(default, deserialize_with = "deserialize_string_or_array")]
576    pub og_image: Option<String>,
577    #[serde(default, deserialize_with = "deserialize_string_or_array")]
578    pub og_audio: Option<String>,
579    #[serde(default, deserialize_with = "deserialize_string_or_array")]
580    pub og_determiner: Option<String>,
581    #[serde(default, deserialize_with = "deserialize_string_or_array")]
582    pub og_locale: Option<String>,
583    pub og_locale_alternate: Option<Vec<String>>,
584    #[serde(default, deserialize_with = "deserialize_string_or_array")]
585    pub og_site_name: Option<String>,
586    #[serde(default, deserialize_with = "deserialize_string_or_array")]
587    pub og_video: Option<String>,
588
589    // Article namespace
590    #[serde(default, deserialize_with = "deserialize_string_or_array")]
591    pub article_section: Option<String>,
592    #[serde(default, deserialize_with = "deserialize_string_or_array")]
593    pub article_tag: Option<String>,
594    #[serde(default, deserialize_with = "deserialize_string_or_array")]
595    pub published_time: Option<String>,
596    #[serde(default, deserialize_with = "deserialize_string_or_array")]
597    pub modified_time: Option<String>,
598
599    // Dublin Core namespace
600    #[serde(default, deserialize_with = "deserialize_string_or_array")]
601    pub dcterms_keywords: Option<String>,
602    #[serde(default, deserialize_with = "deserialize_string_or_array")]
603    pub dc_description: Option<String>,
604    #[serde(default, deserialize_with = "deserialize_string_or_array")]
605    pub dc_subject: Option<String>,
606    #[serde(default, deserialize_with = "deserialize_string_or_array")]
607    pub dcterms_subject: Option<String>,
608    #[serde(default, deserialize_with = "deserialize_string_or_array")]
609    pub dcterms_audience: Option<String>,
610    #[serde(default, deserialize_with = "deserialize_string_or_array")]
611    pub dc_type: Option<String>,
612    #[serde(default, deserialize_with = "deserialize_string_or_array")]
613    pub dcterms_type: Option<String>,
614    #[serde(default, deserialize_with = "deserialize_string_or_array")]
615    pub dc_date: Option<String>,
616    #[serde(default, deserialize_with = "deserialize_string_or_array")]
617    pub dc_date_created: Option<String>,
618    #[serde(default, deserialize_with = "deserialize_string_or_array")]
619    pub dcterms_created: Option<String>,
620
621    // Response metadata
622    #[serde(default, deserialize_with = "deserialize_string_or_array")]
623    pub scrape_id: Option<String>,
624    pub num_pages: Option<u32>,
625    #[serde(default, deserialize_with = "deserialize_string_or_array")]
626    pub content_type: Option<String>,
627    #[serde(default, deserialize_with = "deserialize_string_or_array")]
628    pub timezone: Option<String>,
629    #[serde(default, deserialize_with = "deserialize_string_or_array")]
630    pub proxy_used: Option<String>,
631    #[serde(default, deserialize_with = "deserialize_string_or_array")]
632    pub cache_state: Option<String>,
633    #[serde(default, deserialize_with = "deserialize_string_or_array")]
634    pub cached_at: Option<String>,
635    pub credits_used: Option<u32>,
636    pub concurrency_limited: Option<bool>,
637}
638
639/// Extracted attribute result.
640#[serde_with::skip_serializing_none]
641#[derive(Deserialize, Serialize, Debug, Default, Clone)]
642pub struct AttributeResult {
643    pub selector: String,
644    pub attribute: String,
645    pub values: Vec<String>,
646}
647
648/// Document returned from scrape operations.
649#[serde_with::skip_serializing_none]
650#[derive(Deserialize, Serialize, Debug, Default, Clone)]
651#[serde(rename_all = "camelCase")]
652pub struct Document {
653    /// Markdown content of the page.
654    pub markdown: Option<String>,
655    /// Filtered HTML content.
656    pub html: Option<String>,
657    /// Raw HTML content.
658    pub raw_html: Option<String>,
659    /// Structured JSON extraction result.
660    pub json: Option<Value>,
661    /// AI-generated summary.
662    pub summary: Option<String>,
663    /// Document metadata.
664    pub metadata: Option<DocumentMetadata>,
665    /// Links found on the page.
666    pub links: Option<Vec<String>>,
667    /// Images found on the page.
668    pub images: Option<Vec<String>>,
669    /// Screenshot URL or base64 data.
670    pub screenshot: Option<String>,
671    /// Audio download URL (signed GCS link for MP3).
672    pub audio: Option<String>,
673    /// Video download URL (signed GCS link).
674    pub video: Option<String>,
675    /// Extracted attributes.
676    pub attributes: Option<Vec<AttributeResult>>,
677    /// Action results.
678    pub actions: Option<HashMap<String, Value>>,
679    /// Answer generated by the question or deprecated query format.
680    pub answer: Option<String>,
681    /// Highlights generated by the highlights format.
682    pub highlights: Option<String>,
683    /// Warning message.
684    pub warning: Option<String>,
685    /// Change tracking data.
686    pub change_tracking: Option<Value>,
687    /// Branding analysis.
688    pub branding: Option<Value>,
689    /// Product extraction result.
690    pub product: Option<Product>,
691    /// Menu extraction result.
692    pub menu: Option<Menu>,
693    /// Physical PDF page markdown, present only when `parsers[].pages` is true.
694    pub pages: Option<Vec<PdfPage>>,
695    /// Typed PDF layout blocks, present only when `parsers[].blocks` is true.
696    pub blocks: Option<Vec<PdfPageBlocks>>,
697}
698
699/// Physical markdown for a single PDF page.
700#[serde_with::skip_serializing_none]
701#[derive(Deserialize, Serialize, Debug, Default, Clone)]
702#[serde(rename_all = "camelCase")]
703pub struct PdfPage {
704    pub page_number: u32,
705    pub markdown: String,
706}
707
708/// Layout and OCR confidence scores for a PDF block.
709#[serde_with::skip_serializing_none]
710#[derive(Deserialize, Serialize, Debug, Default, Clone)]
711#[serde(rename_all = "camelCase")]
712pub struct PdfBlockConfidence {
713    pub layout: Option<f64>,
714    pub ocr: Option<f64>,
715}
716
717/// A typed PDF layout block (bounding box, type, reading order).
718#[serde_with::skip_serializing_none]
719#[derive(Deserialize, Serialize, Debug, Default, Clone)]
720#[serde(rename_all = "camelCase")]
721pub struct PdfBlockItem {
722    pub id: String,
723    #[serde(rename = "type")]
724    pub block_type: String,
725    pub label: Option<String>,
726    pub bbox: Option<[f64; 4]>,
727    pub content: String,
728    pub markdown_span: Option<[i64; 2]>,
729    pub reading_order: i64,
730    pub source: Option<String>,
731    pub confidence: PdfBlockConfidence,
732}
733
734/// Typed layout blocks for a single PDF page.
735#[serde_with::skip_serializing_none]
736#[derive(Deserialize, Serialize, Debug, Default, Clone)]
737#[serde(rename_all = "camelCase")]
738pub struct PdfPageBlocks {
739    pub page_number: u32,
740    pub width: Option<f64>,
741    pub height: Option<f64>,
742    pub status: String,
743    pub items: Vec<PdfBlockItem>,
744}
745
746/// Product extraction result for a page.
747#[serde_with::skip_serializing_none]
748#[derive(Deserialize, Serialize, Debug, Default, Clone)]
749#[serde(rename_all = "camelCase")]
750pub struct Product {
751    /// Product title.
752    pub title: String,
753    /// Brand name.
754    pub brand: Option<String>,
755    /// Product category.
756    pub category: Option<String>,
757    /// Product URL.
758    pub url: String,
759    /// Product description.
760    pub description: Option<String>,
761    /// Product variants.
762    #[serde(default)]
763    pub variants: Vec<ProductVariant>,
764}
765
766/// An image associated with a product.
767#[serde_with::skip_serializing_none]
768#[derive(Deserialize, Serialize, Debug, Default, Clone)]
769#[serde(rename_all = "camelCase")]
770pub struct ProductImage {
771    /// Image URL.
772    pub url: String,
773    /// Alternative text for the image.
774    pub alt: Option<String>,
775}
776
777/// Price information for a product.
778#[serde_with::skip_serializing_none]
779#[derive(Deserialize, Serialize, Debug, Default, Clone)]
780#[serde(rename_all = "camelCase")]
781pub struct ProductPrice {
782    /// Numeric price amount.
783    pub amount: f64,
784    /// Currency code.
785    pub currency: Option<String>,
786    /// Human-readable formatted price.
787    pub formatted: Option<String>,
788}
789
790/// Availability information for a product.
791#[serde_with::skip_serializing_none]
792#[derive(Deserialize, Serialize, Debug, Default, Clone)]
793#[serde(rename_all = "camelCase")]
794pub struct ProductAvailability {
795    /// Whether the product is in stock.
796    #[serde(rename = "inStock")]
797    pub in_stock: bool,
798    /// Human-readable availability text.
799    pub text: Option<String>,
800}
801
802/// A variant of a product.
803#[serde_with::skip_serializing_none]
804#[derive(Deserialize, Serialize, Debug, Default, Clone)]
805#[serde(rename_all = "camelCase")]
806pub struct ProductVariant {
807    /// Variant identifier.
808    pub id: Option<String>,
809    /// Stock keeping unit.
810    pub sku: Option<String>,
811    /// Variant title.
812    pub title: Option<String>,
813    /// Variant option values (e.g. size, color).
814    pub values: Option<HashMap<String, serde_json::Value>>,
815    /// Variant price.
816    pub price: Option<ProductPrice>,
817    /// Sale information, present when the variant is discounted.
818    pub sale: Option<ProductSale>,
819    /// Variant availability information (always present).
820    pub availability: ProductAvailability,
821    /// Variant images.
822    pub images: Option<Vec<ProductImage>>,
823}
824
825/// Sale information for a product variant.
826#[serde_with::skip_serializing_none]
827#[derive(Deserialize, Serialize, Debug, Default, Clone)]
828#[serde(rename_all = "camelCase")]
829pub struct ProductSale {
830    /// Original price before the discount.
831    pub original_price: ProductPrice,
832}
833
834/// Menu extraction result for a page.
835#[serde_with::skip_serializing_none]
836#[derive(Deserialize, Serialize, Debug, Default, Clone)]
837#[serde(rename_all = "camelCase")]
838pub struct Menu {
839    /// Whether the page was identified as a menu.
840    pub is_menu: bool,
841    /// Confidence score for the menu classification.
842    pub confidence: f64,
843    /// Currency code for the menu prices.
844    pub currency: Option<String>,
845    /// Source URL of the menu.
846    pub source_url: String,
847    /// Merchant information.
848    pub merchant: MenuMerchant,
849    /// Menu sections.
850    #[serde(default)]
851    pub sections: Vec<MenuSection>,
852}
853
854/// Merchant information for a menu.
855#[serde_with::skip_serializing_none]
856#[derive(Deserialize, Serialize, Debug, Default, Clone)]
857#[serde(rename_all = "camelCase")]
858pub struct MenuMerchant {
859    /// Merchant name.
860    pub name: String,
861    /// Merchant type.
862    #[serde(rename = "type")]
863    pub merchant_type: Option<String>,
864    /// Merchant location (arbitrary shape).
865    pub location: Option<Value>,
866}
867
868/// A section of a menu.
869#[serde_with::skip_serializing_none]
870#[derive(Deserialize, Serialize, Debug, Default, Clone)]
871#[serde(rename_all = "camelCase")]
872pub struct MenuSection {
873    /// Section identifier.
874    pub id: String,
875    /// Section name.
876    pub name: String,
877    /// Section description.
878    pub description: Option<String>,
879    /// Items in the section.
880    #[serde(default)]
881    pub items: Vec<MenuItem>,
882}
883
884/// An item on a menu.
885#[serde_with::skip_serializing_none]
886#[derive(Deserialize, Serialize, Debug, Default, Clone)]
887#[serde(rename_all = "camelCase")]
888pub struct MenuItem {
889    /// Item identifier.
890    pub id: String,
891    /// Item name.
892    pub name: String,
893    /// Item description.
894    pub description: Option<String>,
895    /// Item images.
896    #[serde(default)]
897    pub images: Vec<MenuImage>,
898    /// Item price.
899    pub price: Option<MenuPrice>,
900    /// Item availability information.
901    pub availability: MenuAvailability,
902    /// Dietary tags.
903    #[serde(default)]
904    pub dietary: Vec<String>,
905    /// Calorie count.
906    pub calories: Option<f64>,
907    /// Option groups (arbitrary shape).
908    #[serde(default)]
909    pub option_groups: Vec<Value>,
910    /// Item identifiers.
911    #[serde(default)]
912    pub identifiers: MenuItemIdentifiers,
913    /// Item URL.
914    pub url: Option<String>,
915    /// Source URL of the item.
916    pub source_url: String,
917}
918
919/// An image associated with a menu item.
920#[serde_with::skip_serializing_none]
921#[derive(Deserialize, Serialize, Debug, Default, Clone)]
922#[serde(rename_all = "camelCase")]
923pub struct MenuImage {
924    /// Image URL.
925    pub url: String,
926    /// Alternative text for the image.
927    pub alt: Option<String>,
928}
929
930/// Price information for a menu item.
931#[serde_with::skip_serializing_none]
932#[derive(Deserialize, Serialize, Debug, Default, Clone)]
933#[serde(rename_all = "camelCase")]
934pub struct MenuPrice {
935    /// Numeric price amount.
936    pub amount: f64,
937    /// Currency code.
938    pub currency: Option<String>,
939    /// Human-readable formatted price.
940    pub formatted: Option<String>,
941}
942
943/// Availability information for a menu item.
944#[serde_with::skip_serializing_none]
945#[derive(Deserialize, Serialize, Debug, Default, Clone)]
946#[serde(rename_all = "camelCase")]
947pub struct MenuAvailability {
948    /// Whether the item is in stock.
949    #[serde(rename = "inStock")]
950    pub in_stock: bool,
951    /// Human-readable availability text.
952    pub text: Option<String>,
953}
954
955/// Identifiers for a menu item.
956#[serde_with::skip_serializing_none]
957#[derive(Deserialize, Serialize, Debug, Default, Clone)]
958#[serde(rename_all = "camelCase")]
959pub struct MenuItemIdentifiers {
960    /// Merchant-specific item identifier.
961    pub merchant_item_id: Option<String>,
962}
963
964/// Job status types for crawl and batch operations.
965#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq)]
966#[serde(rename_all = "camelCase")]
967pub enum JobStatus {
968    Scraping,
969    Completed,
970    Failed,
971    Cancelled,
972}
973
974/// Sitemap handling mode.
975#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq)]
976#[serde(rename_all = "lowercase")]
977pub enum SitemapMode {
978    /// Skip sitemap entirely.
979    Skip,
980    /// Include sitemap links alongside discovered links.
981    Include,
982    /// Only use links from the sitemap.
983    Only,
984}
985
986/// Agent model types.
987#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq)]
988#[serde(rename_all = "kebab-case")]
989pub enum AgentModel {
990    #[serde(rename = "spark-1-pro")]
991    Spark1Pro,
992    #[serde(rename = "spark-1-mini")]
993    Spark1Mini,
994}
995
996/// Search source types.
997#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq)]
998#[serde(rename_all = "lowercase")]
999pub enum SearchSource {
1000    Web,
1001    News,
1002    Images,
1003}
1004
1005/// Search category types.
1006#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq)]
1007#[serde(rename_all = "lowercase")]
1008pub enum SearchCategory {
1009    Github,
1010    Research,
1011    Pdf,
1012}
1013
1014/// Web search result.
1015#[serde_with::skip_serializing_none]
1016#[derive(Deserialize, Serialize, Debug, Default, Clone)]
1017#[serde(rename_all = "camelCase")]
1018pub struct SearchResultWeb {
1019    pub url: String,
1020    pub title: Option<String>,
1021    pub description: Option<String>,
1022    pub category: Option<String>,
1023}
1024
1025/// News search result.
1026#[serde_with::skip_serializing_none]
1027#[derive(Deserialize, Serialize, Debug, Default, Clone)]
1028#[serde(rename_all = "camelCase")]
1029pub struct SearchResultNews {
1030    pub title: Option<String>,
1031    pub url: Option<String>,
1032    pub snippet: Option<String>,
1033    pub date: Option<String>,
1034    pub image_url: Option<String>,
1035    pub position: Option<u32>,
1036    pub category: Option<String>,
1037}
1038
1039/// Image search result.
1040#[serde_with::skip_serializing_none]
1041#[derive(Deserialize, Serialize, Debug, Default, Clone)]
1042#[serde(rename_all = "camelCase")]
1043pub struct SearchResultImage {
1044    pub title: Option<String>,
1045    pub image_url: Option<String>,
1046    pub image_width: Option<u32>,
1047    pub image_height: Option<u32>,
1048    pub url: Option<String>,
1049    pub position: Option<u32>,
1050}
1051
1052/// Crawl error information.
1053#[serde_with::skip_serializing_none]
1054#[derive(Deserialize, Serialize, Debug, Clone)]
1055#[serde(rename_all = "camelCase")]
1056pub struct CrawlError {
1057    pub id: String,
1058    pub timestamp: Option<String>,
1059    pub url: String,
1060    pub code: Option<String>,
1061    pub error: String,
1062}
1063
1064/// Crawl errors response.
1065#[serde_with::skip_serializing_none]
1066#[derive(Deserialize, Serialize, Debug, Clone)]
1067#[serde(rename_all = "camelCase")]
1068pub struct CrawlErrorsResponse {
1069    pub errors: Vec<CrawlError>,
1070    #[serde(rename = "robotsBlocked")]
1071    pub robots_blocked: Vec<String>,
1072}
1073
1074#[cfg(test)]
1075mod tests {
1076    use super::*;
1077    use serde_json::json;
1078
1079    #[test]
1080    fn test_full_document_with_array_metadata() {
1081        let json = json!({
1082            "markdown": "# Hello",
1083            "video": "https://storage.googleapis.com/firecrawl/video.mp4",
1084            "metadata": {
1085                "sourceURL": "https://example.com",
1086                "statusCode": 200,
1087                "title": "Example Page",
1088                "description": ["A great page", "with multiple descriptions"],
1089                "robots": ["index", "follow"],
1090                "ogImage": ["https://img.jpg"],
1091                "language": "en",
1092                "keywords": ["rust", "sdk", "firecrawl"]
1093            }
1094        });
1095        let doc: Document = serde_json::from_value(json).unwrap();
1096        assert_eq!(doc.markdown, Some("# Hello".to_string()));
1097        assert_eq!(
1098            doc.video,
1099            Some("https://storage.googleapis.com/firecrawl/video.mp4".to_string())
1100        );
1101        let meta = doc.metadata.unwrap();
1102        assert_eq!(meta.title, Some("Example Page".to_string()));
1103        assert_eq!(
1104            meta.description,
1105            Some("A great page, with multiple descriptions".to_string())
1106        );
1107        assert_eq!(meta.robots, Some("index, follow".to_string()));
1108        assert_eq!(meta.og_image, Some("https://img.jpg".to_string()));
1109        assert_eq!(meta.language, Some("en".to_string()));
1110        assert_eq!(meta.keywords, Some("rust, sdk, firecrawl".to_string()));
1111    }
1112
1113    #[test]
1114    fn test_format_menu_round_trip() {
1115        let format = Format::Menu;
1116        let serialized = serde_json::to_value(&format).unwrap();
1117        assert_eq!(serialized, json!("menu"));
1118        let deserialized: Format = serde_json::from_value(json!("menu")).unwrap();
1119        assert_eq!(deserialized, Format::Menu);
1120    }
1121
1122    #[test]
1123    fn test_document_with_menu() {
1124        let json = json!({
1125            "menu": {
1126                "isMenu": true,
1127                "confidence": 0.95,
1128                "currency": "USD",
1129                "sourceUrl": "https://example.com/menu",
1130                "merchant": {
1131                    "name": "Test Diner",
1132                    "type": "restaurant",
1133                    "location": { "city": "Springfield" }
1134                },
1135                "sections": [
1136                    {
1137                        "id": "s1",
1138                        "name": "Mains",
1139                        "items": [
1140                            {
1141                                "id": "i1",
1142                                "name": "Burger",
1143                                "images": [{ "url": "https://example.com/burger.jpg" }],
1144                                "price": { "amount": 12.5, "currency": "USD", "formatted": "$12.50" },
1145                                "availability": { "inStock": true },
1146                                "dietary": ["vegetarian"],
1147                                "optionGroups": [],
1148                                "identifiers": { "merchantItemId": "abc123" },
1149                                "sourceUrl": "https://example.com/menu#i1"
1150                            }
1151                        ]
1152                    }
1153                ]
1154            }
1155        });
1156        let doc: Document = serde_json::from_value(json).unwrap();
1157        let menu = doc.menu.as_ref().expect("menu should be present");
1158        assert!(menu.is_menu);
1159        assert_eq!(menu.confidence, 0.95);
1160        assert_eq!(menu.currency, Some("USD".to_string()));
1161        assert_eq!(menu.source_url, "https://example.com/menu");
1162        assert_eq!(menu.merchant.name, "Test Diner");
1163        assert_eq!(menu.merchant.merchant_type, Some("restaurant".to_string()));
1164        assert_eq!(menu.sections.len(), 1);
1165        let section = &menu.sections[0];
1166        assert_eq!(section.name, "Mains");
1167        assert_eq!(section.items.len(), 1);
1168        let item = &section.items[0];
1169        assert_eq!(item.name, "Burger");
1170        assert!(item.availability.in_stock);
1171        assert_eq!(item.dietary, vec!["vegetarian".to_string()]);
1172        assert_eq!(
1173            item.identifiers.merchant_item_id,
1174            Some("abc123".to_string())
1175        );
1176        let price = item.price.as_ref().unwrap();
1177        assert_eq!(price.amount, 12.5);
1178
1179        // Round-trip back to JSON and ensure camelCase field names are preserved.
1180        let reserialized = serde_json::to_value(&doc).unwrap();
1181        let item_json = &reserialized["menu"]["sections"][0]["items"][0];
1182        assert_eq!(item_json["sourceUrl"], "https://example.com/menu#i1");
1183        assert_eq!(item_json["availability"]["inStock"], true);
1184        assert_eq!(item_json["identifiers"]["merchantItemId"], "abc123");
1185    }
1186
1187    #[test]
1188    fn test_document_with_blocks() {
1189        let json = json!({
1190            "markdown": "# Annual Report 2025",
1191            "blocks": [{
1192                "pageNumber": 1,
1193                "width": 1700.0,
1194                "height": 2200.0,
1195                "status": "ok",
1196                "items": [{
1197                    "id": "p1.b0",
1198                    "type": "title",
1199                    "label": "doc_title",
1200                    "bbox": [0.118, 0.054, 0.882, 0.092],
1201                    "content": "# Annual Report 2025",
1202                    "markdownSpan": [0, 21],
1203                    "readingOrder": 0,
1204                    "source": "native_text",
1205                    "confidence": { "layout": 0.97, "ocr": null }
1206                }]
1207            }]
1208        });
1209        let doc: Document = serde_json::from_value(json).unwrap();
1210        let pages = doc.blocks.expect("blocks should be present");
1211        assert_eq!(pages.len(), 1);
1212        assert_eq!(pages[0].page_number, 1);
1213        assert_eq!(pages[0].status, "ok");
1214        assert_eq!(pages[0].items[0].id, "p1.b0");
1215        assert_eq!(pages[0].items[0].block_type, "title");
1216        assert_eq!(pages[0].items[0].reading_order, 0);
1217        assert_eq!(pages[0].items[0].confidence.layout, Some(0.97));
1218        assert_eq!(pages[0].items[0].confidence.ocr, None);
1219    }
1220
1221    #[test]
1222    fn test_document_with_pages() {
1223        let json = json!({
1224            "markdown": "# Annual Report 2025",
1225            "pages": [
1226                { "pageNumber": 1, "markdown": "# Cover" },
1227                { "pageNumber": 2, "markdown": "## Intro" }
1228            ]
1229        });
1230        let doc: Document = serde_json::from_value(json).unwrap();
1231        let pages = doc.pages.expect("pages should be present");
1232        assert_eq!(pages.len(), 2);
1233        assert_eq!(pages[0].page_number, 1);
1234        assert_eq!(pages[0].markdown, "# Cover");
1235        assert_eq!(pages[1].page_number, 2);
1236        assert_eq!(pages[1].markdown, "## Intro");
1237    }
1238}