1pub mod agent;
4pub mod attachment;
5pub mod interact;
6pub mod locate;
7pub mod parse;
8pub mod request;
9pub mod response;
10
11pub use agent::{
12 AgentBudget, AgentEffort, AgentEvidence, AgentFailure, AgentOutcome, AgentRequest, AgentSource,
13 AgentStep, AgentUsage, FieldSource,
14};
15pub use attachment::Attachment;
16pub use interact::{
17 InteractRequest, InteractResponse, InteractStopped, LIVE_VIEW_UNAVAILABLE, SESSION_REPLAYED,
18 instance_tag, is_scrape_id, issued_elsewhere, scrape_id, session_gone,
19};
20pub use locate::{
21 ObserveRequest, ObserveResponse, Observed, SimilarData, SimilarItem, SimilarRequest,
22 SimilarResponse,
23};
24pub use parse::{PdfParser, Upload, UploadKind};
25
26pub use request::{
27 AdaptiveOptions, BatchScrapeRequest, CrawlRequest, CrawlScope, ExtractRequest, Format,
28 ImageAssetMode, Location, MapRequest, QueryFilter, RESPOND_TIMINGS, ScrapeOptions,
29 ScrapeRequest, SearchRequest,
30};
31pub use response::{
32 ChangeTracking, CrawlSummary, CrawlTrace, DocChunk, Document, ImageAssetResult,
33 ImageAssetStatus, JobError, JobErrors, JobStarted, JobStatus, MapLink, MapResponse, Metadata,
34 OcrMeta, PageMarkdown, PagesiftMeta, PlanStep, PlanTrace, QueryFilterStats, ScrapeResponse,
35};
36
37#[cfg(test)]
38mod tests {
39 use super::*;
40 use serde_json::json;
41
42 #[test]
43 fn formats_accept_strings_and_objects() {
44 let r: ScrapeRequest = serde_json::from_value(json!({
45 "url": "https://example.com",
46 "formats": ["markdown", {"type": "screenshot", "fullPage": true}],
47 }))
48 .unwrap();
49 assert_eq!(r.options.formats[0], Format::new("markdown"));
50 assert_eq!(r.options.formats[1].kind, "screenshot");
51 assert_eq!(r.options.formats[1].options["fullPage"], json!(true));
52 }
53
54 #[test]
55 fn defaults_follow_firecrawl_and_unknown_fields_are_ignored() {
56 let r: ScrapeRequest = serde_json::from_value(json!({
57 "url": "https://example.com",
58 "origin": "python-sdk",
59 "integration": null,
60 "zeroDataRetention": false,
61 }))
62 .unwrap();
63 let o = r.options;
64 assert_eq!(o.formats, vec![Format::new("markdown")]);
65 assert!(o.only_main_content && o.block_ads && o.store_in_cache && o.remove_base64_images);
66 assert_eq!(o.wait_for, 0);
67 assert!(!o.ignore_robots_txt);
68 }
69
70 #[test]
71 fn crawl_and_map_defaults() {
72 let c: CrawlRequest =
73 serde_json::from_value(json!({"url": "https://example.com"})).unwrap();
74 assert_eq!((c.limit, c.sitemap.as_str()), (10_000, "include"));
75 assert!(c.scrape_options.only_main_content);
76 let m: MapRequest = serde_json::from_value(json!({"url": "https://example.com"})).unwrap();
77 assert_eq!(m.limit, 5_000);
78 assert!(m.include_subdomains && m.ignore_query_parameters);
79 }
80
81 #[test]
82 fn query_filter_accepts_string_or_object() {
83 let r: ScrapeRequest = serde_json::from_value(json!({
84 "url": "https://example.com", "queryFilter": "refund policy",
85 }))
86 .unwrap();
87 let f = r.options.query_filter.unwrap();
88 assert_eq!(
89 (
90 f.query.as_str(),
91 f.threshold,
92 f.relative,
93 f.top_k,
94 f.headings
95 ),
96 ("refund policy", None, Some(0.5), None, true)
97 );
98 let r: ScrapeRequest = serde_json::from_value(json!({
99 "url": "https://example.com",
100 "queryFilter": {"query": "q", "threshold": 0.5, "topK": 3, "headings": false},
101 }))
102 .unwrap();
103 let f = r.options.query_filter.unwrap();
104 assert_eq!(
105 (f.threshold, f.relative, f.top_k, f.headings),
106 (Some(0.5), None, Some(3), false)
107 );
108 let r: ScrapeRequest = serde_json::from_value(json!({
109 "url": "https://example.com",
110 "queryFilter": {"query": "q", "maxTokens": 800},
111 }))
112 .unwrap();
113 let f = r.options.query_filter.unwrap();
114 assert_eq!((f.relative, f.max_tokens), (Some(0.5), Some(800)));
115 assert!(ScrapeOptions::default().query_filter.is_none());
116 }
117
118 #[test]
119 fn crawl_query_and_adaptive_options() {
120 let c: CrawlRequest =
121 serde_json::from_value(json!({"url": "https://example.com"})).unwrap();
122 assert_eq!((c.query, c.adaptive), (None, None));
123 let c: CrawlRequest = serde_json::from_value(json!({
124 "url": "https://example.com", "query": ["refund", "policy"], "adaptive": true,
125 }))
126 .unwrap();
127 assert_eq!(c.query.as_deref(), Some("refund policy"));
128 assert_eq!(c.adaptive, Some(AdaptiveOptions::default()));
129 let c: CrawlRequest = serde_json::from_value(json!({
130 "url": "https://example.com", "query": " ", "adaptive": {"patience": 2},
131 }))
132 .unwrap();
133 assert_eq!(c.query, None, "blank query is no query");
134 assert_eq!(c.adaptive.unwrap().patience, 2);
135 let c: CrawlRequest = serde_json::from_value(json!({
136 "url": "https://example.com", "query": null, "adaptive": false,
137 }))
138 .unwrap();
139 assert_eq!((c.query, c.adaptive), (None, None));
140 }
141
142 #[test]
143 fn change_tracking_serializes_firecrawl_fields() {
144 let doc = Document {
145 change_tracking: Some(ChangeTracking {
146 previous_scrape_at: None,
147 change_status: "new".into(),
148 visibility: "visible".into(),
149 diff: None,
150 json: None,
151 pagesift: None,
152 }),
153 ..Default::default()
154 };
155 let v = serde_json::to_value(&doc).unwrap();
156 assert_eq!(
157 v["changeTracking"],
158 json!({"previousScrapeAt": null, "changeStatus": "new", "visibility": "visible"})
159 );
160 assert!(
161 serde_json::to_value(Document::default())
162 .unwrap()
163 .get("changeTracking")
164 .is_none()
165 );
166 }
167
168 #[test]
169 fn metadata_serializes_firecrawl_names_and_extension() {
170 let mut meta = Metadata {
171 source_url: Some("https://example.com/a".into()),
172 status_code: 200,
173 proxy_used: "basic".into(),
174 pagesift: Some(PagesiftMeta {
175 content_hash: Some("ab".into()),
176 ..Default::default()
177 }),
178 ..Default::default()
179 };
180 meta.extra
181 .insert("ogImage".into(), json!("https://example.com/i.png"));
182 let v = serde_json::to_value(&meta).unwrap();
183 assert_eq!(v["sourceURL"], "https://example.com/a");
184 assert_eq!(v["statusCode"], 200);
185 assert_eq!(v["proxyUsed"], "basic");
186 assert_eq!(v["ogImage"], "https://example.com/i.png");
187 assert_eq!(v["pagesift"]["contentHash"], "ab");
188 assert!(v.get("title").is_none());
189 }
190}
191mod asset_cache;
192pub use asset_cache::AssetFetchCache;
193mod progress;
194pub use progress::{MarkdownPreview, MarkdownProgress};