1use 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#[derive(Clone, Debug, PartialEq, Eq)]
11pub enum Format {
12 Markdown,
14 Html,
16 RawHtml,
18 Links,
20 Images,
22 Screenshot,
24 Summary,
26 ChangeTracking,
28 Json,
30 Attributes,
32 Branding,
34 Product,
36 Menu,
38 Audio,
40 Video,
42 Question(QuestionFormat),
44 Highlights(HighlightsFormat),
46 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#[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#[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#[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#[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#[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#[serde_with::skip_serializing_none]
281#[derive(Deserialize, Serialize, Debug, Default, Clone)]
282#[serde(rename_all = "camelCase")]
283pub struct ScreenshotOptions {
284 pub full_page: Option<bool>,
286 pub quality: Option<u8>,
288 pub viewport: Option<Viewport>,
290}
291
292#[serde_with::skip_serializing_none]
294#[derive(Deserialize, Serialize, Debug, Default, Clone)]
295#[serde(rename_all = "camelCase")]
296pub struct ChangeTrackingOptions {
297 pub modes: Option<Vec<ChangeTrackingMode>>,
299 pub schema: Option<Value>,
301 pub prompt: Option<String>,
303 pub tag: Option<String>,
305}
306
307#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq)]
309#[serde(rename_all = "kebab-case")]
310pub enum ChangeTrackingMode {
311 GitDiff,
312 Json,
313}
314
315#[serde_with::skip_serializing_none]
317#[derive(Deserialize, Serialize, Debug, Default, Clone)]
318pub struct AttributeSelector {
319 pub selector: String,
321 pub attribute: String,
323}
324
325#[serde_with::skip_serializing_none]
327#[derive(Deserialize, Serialize, Debug, Default, Clone)]
328#[serde(rename_all = "camelCase")]
329pub struct JsonOptions {
330 pub schema: Option<Value>,
332 pub system_prompt: Option<String>,
334 pub prompt: Option<String>,
336}
337
338#[serde_with::skip_serializing_none]
340#[derive(Deserialize, Serialize, Debug, Default, Clone)]
341#[serde(rename_all = "camelCase")]
342pub struct LocationConfig {
343 pub country: Option<String>,
345 pub languages: Option<Vec<String>>,
347}
348
349#[serde_with::skip_serializing_none]
351#[derive(Deserialize, Serialize, Debug, Default, Clone)]
352#[serde(rename_all = "camelCase")]
353pub struct ProfileConfig {
354 pub name: String,
356 pub save_changes: Option<bool>,
358}
359
360#[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#[derive(Deserialize, Serialize, Debug, Clone)]
372#[serde(tag = "type", rename_all = "camelCase")]
373pub enum Action {
374 Wait {
376 #[serde(skip_serializing_if = "Option::is_none")]
378 milliseconds: Option<u32>,
379 #[serde(skip_serializing_if = "Option::is_none")]
381 selector: Option<String>,
382 },
383 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 {
394 selector: String,
396 },
397 Write {
399 text: String,
401 },
402 Press {
404 key: String,
406 },
407 Scroll {
409 direction: ScrollDirection,
411 #[serde(skip_serializing_if = "Option::is_none")]
413 selector: Option<String>,
414 },
415 Scrape,
417 #[serde(rename = "executeJavascript")]
419 ExecuteJavascript {
420 script: String,
422 },
423 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#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq)]
436#[serde(rename_all = "lowercase")]
437pub enum ScrollDirection {
438 Up,
439 Down,
440}
441
442#[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#[serde_with::skip_serializing_none]
460#[derive(Deserialize, Serialize, Debug, Default, Clone)]
461#[serde(rename_all = "camelCase")]
462pub struct WebhookConfig {
463 pub url: String,
465 pub headers: Option<HashMap<String, String>>,
467 pub metadata: Option<HashMap<String, String>>,
469 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#[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#[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#[serde_with::skip_serializing_none]
514#[derive(Deserialize, Serialize, Debug, Default, Clone)]
515#[serde(rename_all = "camelCase")]
516pub struct AgentWebhookConfig {
517 pub url: String,
519 pub headers: Option<HashMap<String, String>>,
521 pub metadata: Option<HashMap<String, String>>,
523 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#[serde_with::skip_serializing_none]
547#[derive(Deserialize, Serialize, Debug, Default, Clone)]
548#[serde(rename_all = "camelCase")]
549pub struct DocumentMetadata {
550 #[serde(rename = "sourceURL")]
552 pub source_url: Option<String>,
553 pub status_code: Option<u16>,
554 pub error: Option<String>,
555
556 #[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 #[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 #[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 #[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 #[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#[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#[serde_with::skip_serializing_none]
650#[derive(Deserialize, Serialize, Debug, Default, Clone)]
651#[serde(rename_all = "camelCase")]
652pub struct Document {
653 pub markdown: Option<String>,
655 pub html: Option<String>,
657 pub raw_html: Option<String>,
659 pub json: Option<Value>,
661 pub summary: Option<String>,
663 pub metadata: Option<DocumentMetadata>,
665 pub links: Option<Vec<String>>,
667 pub images: Option<Vec<String>>,
669 pub screenshot: Option<String>,
671 pub audio: Option<String>,
673 pub video: Option<String>,
675 pub attributes: Option<Vec<AttributeResult>>,
677 pub actions: Option<HashMap<String, Value>>,
679 pub answer: Option<String>,
681 pub highlights: Option<String>,
683 pub warning: Option<String>,
685 pub change_tracking: Option<Value>,
687 pub branding: Option<Value>,
689 pub product: Option<Product>,
691 pub menu: Option<Menu>,
693 pub pages: Option<Vec<PdfPage>>,
695 pub blocks: Option<Vec<PdfPageBlocks>>,
697}
698
699#[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#[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#[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#[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#[serde_with::skip_serializing_none]
748#[derive(Deserialize, Serialize, Debug, Default, Clone)]
749#[serde(rename_all = "camelCase")]
750pub struct Product {
751 pub title: String,
753 pub brand: Option<String>,
755 pub category: Option<String>,
757 pub url: String,
759 pub description: Option<String>,
761 #[serde(default)]
763 pub variants: Vec<ProductVariant>,
764}
765
766#[serde_with::skip_serializing_none]
768#[derive(Deserialize, Serialize, Debug, Default, Clone)]
769#[serde(rename_all = "camelCase")]
770pub struct ProductImage {
771 pub url: String,
773 pub alt: Option<String>,
775}
776
777#[serde_with::skip_serializing_none]
779#[derive(Deserialize, Serialize, Debug, Default, Clone)]
780#[serde(rename_all = "camelCase")]
781pub struct ProductPrice {
782 pub amount: f64,
784 pub currency: Option<String>,
786 pub formatted: Option<String>,
788}
789
790#[serde_with::skip_serializing_none]
792#[derive(Deserialize, Serialize, Debug, Default, Clone)]
793#[serde(rename_all = "camelCase")]
794pub struct ProductAvailability {
795 #[serde(rename = "inStock")]
797 pub in_stock: bool,
798 pub text: Option<String>,
800}
801
802#[serde_with::skip_serializing_none]
804#[derive(Deserialize, Serialize, Debug, Default, Clone)]
805#[serde(rename_all = "camelCase")]
806pub struct ProductVariant {
807 pub id: Option<String>,
809 pub sku: Option<String>,
811 pub title: Option<String>,
813 pub values: Option<HashMap<String, serde_json::Value>>,
815 pub price: Option<ProductPrice>,
817 pub sale: Option<ProductSale>,
819 pub availability: ProductAvailability,
821 pub images: Option<Vec<ProductImage>>,
823}
824
825#[serde_with::skip_serializing_none]
827#[derive(Deserialize, Serialize, Debug, Default, Clone)]
828#[serde(rename_all = "camelCase")]
829pub struct ProductSale {
830 pub original_price: ProductPrice,
832}
833
834#[serde_with::skip_serializing_none]
836#[derive(Deserialize, Serialize, Debug, Default, Clone)]
837#[serde(rename_all = "camelCase")]
838pub struct Menu {
839 pub is_menu: bool,
841 pub confidence: f64,
843 pub currency: Option<String>,
845 pub source_url: String,
847 pub merchant: MenuMerchant,
849 #[serde(default)]
851 pub sections: Vec<MenuSection>,
852}
853
854#[serde_with::skip_serializing_none]
856#[derive(Deserialize, Serialize, Debug, Default, Clone)]
857#[serde(rename_all = "camelCase")]
858pub struct MenuMerchant {
859 pub name: String,
861 #[serde(rename = "type")]
863 pub merchant_type: Option<String>,
864 pub location: Option<Value>,
866}
867
868#[serde_with::skip_serializing_none]
870#[derive(Deserialize, Serialize, Debug, Default, Clone)]
871#[serde(rename_all = "camelCase")]
872pub struct MenuSection {
873 pub id: String,
875 pub name: String,
877 pub description: Option<String>,
879 #[serde(default)]
881 pub items: Vec<MenuItem>,
882}
883
884#[serde_with::skip_serializing_none]
886#[derive(Deserialize, Serialize, Debug, Default, Clone)]
887#[serde(rename_all = "camelCase")]
888pub struct MenuItem {
889 pub id: String,
891 pub name: String,
893 pub description: Option<String>,
895 #[serde(default)]
897 pub images: Vec<MenuImage>,
898 pub price: Option<MenuPrice>,
900 pub availability: MenuAvailability,
902 #[serde(default)]
904 pub dietary: Vec<String>,
905 pub calories: Option<f64>,
907 #[serde(default)]
909 pub option_groups: Vec<Value>,
910 #[serde(default)]
912 pub identifiers: MenuItemIdentifiers,
913 pub url: Option<String>,
915 pub source_url: String,
917}
918
919#[serde_with::skip_serializing_none]
921#[derive(Deserialize, Serialize, Debug, Default, Clone)]
922#[serde(rename_all = "camelCase")]
923pub struct MenuImage {
924 pub url: String,
926 pub alt: Option<String>,
928}
929
930#[serde_with::skip_serializing_none]
932#[derive(Deserialize, Serialize, Debug, Default, Clone)]
933#[serde(rename_all = "camelCase")]
934pub struct MenuPrice {
935 pub amount: f64,
937 pub currency: Option<String>,
939 pub formatted: Option<String>,
941}
942
943#[serde_with::skip_serializing_none]
945#[derive(Deserialize, Serialize, Debug, Default, Clone)]
946#[serde(rename_all = "camelCase")]
947pub struct MenuAvailability {
948 #[serde(rename = "inStock")]
950 pub in_stock: bool,
951 pub text: Option<String>,
953}
954
955#[serde_with::skip_serializing_none]
957#[derive(Deserialize, Serialize, Debug, Default, Clone)]
958#[serde(rename_all = "camelCase")]
959pub struct MenuItemIdentifiers {
960 pub merchant_item_id: Option<String>,
962}
963
964#[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#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq)]
976#[serde(rename_all = "lowercase")]
977pub enum SitemapMode {
978 Skip,
980 Include,
982 Only,
984}
985
986#[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#[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#[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#[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#[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#[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#[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#[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 = §ion.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 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}