Skip to main content

crawlberg/types/
results.rs

1use std::borrow::Cow;
2use std::collections::HashMap;
3
4use ahash::AHashSet;
5use serde::{Deserialize, Serialize};
6
7use super::{
8    CookieInfo, DownloadedAsset, ExtractionMeta, FeedInfo, ImageInfo, JsonLdEntry, LinkInfo, PageMetadata, ResponseMeta,
9};
10
11/// Browser-specific extras populated when the native browser backend was used.
12///
13/// Available on `ScrapeResult.browser` when `BrowserBackend::Native` handled the request.
14#[derive(Debug, Clone, Default, Serialize, Deserialize)]
15pub struct BrowserExtras {
16    /// Return value of `BrowserConfig.eval_script`, if provided.
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub eval_result: Option<serde_json::Value>,
19    /// Network events captured during page navigation (only populated when
20    /// `BrowserConfig.capture_network_events` is true).
21    #[serde(default)]
22    pub network_events: Vec<ResponseMeta>,
23    /// All non-expired cookies present in the browser's cookie jar after
24    /// navigation completes (includes both prior cookies and server Set-Cookie).
25    #[serde(default)]
26    pub cookies: Vec<CookieInfo>,
27}
28
29/// A downloaded non-HTML document (PDF, DOCX, image, code file, etc.).
30///
31/// When the crawler encounters non-HTML content and `download_documents` is
32/// enabled, it downloads the raw bytes and populates this struct instead of
33/// skipping the resource.
34#[derive(Debug, Clone, Default, Serialize, Deserialize)]
35pub struct DownloadedDocument {
36    /// The URL the document was fetched from.
37    pub url: String,
38    /// The MIME type from the Content-Type header.
39    pub mime_type: Cow<'static, str>,
40    /// Raw document bytes. Skipped during JSON serialization.
41    #[serde(skip_serializing)]
42    #[cfg_attr(alef, alef(skip))]
43    pub content: Vec<u8>,
44    /// Size of the document in bytes.
45    pub size: usize,
46    /// Filename extracted from Content-Disposition or URL path.
47    pub filename: Option<Box<str>>,
48    /// SHA-256 hex digest of the content.
49    pub content_hash: Box<str>,
50    /// Selected response headers.
51    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
52    pub headers: HashMap<Box<str>, Box<str>>,
53}
54
55/// Result of executing a sequence of page interaction actions.
56#[derive(Debug, Clone, Default, Serialize, Deserialize)]
57pub struct InteractionResult {
58    /// Results from each executed action.
59    pub action_results: Vec<ActionResult>,
60    /// Final page HTML after all actions completed.
61    pub final_html: String,
62    /// Final page URL (may have changed due to navigation).
63    pub final_url: String,
64    /// Screenshot taken after all actions, if requested.
65    #[serde(skip)]
66    #[cfg_attr(alef, alef(skip))]
67    pub screenshot: Option<Vec<u8>>,
68}
69
70/// Result from a single page action execution.
71#[derive(Debug, Clone, Default, Serialize, Deserialize)]
72pub struct ActionResult {
73    /// Zero-based index of the action in the sequence.
74    pub action_index: usize,
75    /// The type of action that was executed.
76    pub action_type: Cow<'static, str>,
77    /// Whether the action completed successfully.
78    pub success: bool,
79    /// Action-specific return data (screenshot bytes, JS return value, scraped HTML).
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub data: Option<serde_json::Value>,
82    /// Error message if the action failed.
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub error: Option<String>,
85}
86
87/// The result of a single-page scrape operation.
88#[derive(Debug, Clone, Default, Serialize, Deserialize)]
89#[serde(deny_unknown_fields)]
90pub struct ScrapeResult {
91    /// The HTTP status code of the response.
92    pub status_code: u16,
93    /// The final URL after following all redirects.
94    pub final_url: String,
95    /// The Content-Type header value.
96    pub content_type: String,
97    /// The HTML body of the response.
98    pub html: String,
99    /// The size of the response body in bytes.
100    pub body_size: usize,
101    /// Extracted metadata from the page.
102    pub metadata: PageMetadata,
103    /// Links found on the page.
104    pub links: Vec<LinkInfo>,
105    /// Images found on the page.
106    pub images: Vec<ImageInfo>,
107    /// Feed links found on the page.
108    pub feeds: Vec<FeedInfo>,
109    /// JSON-LD entries found on the page.
110    pub json_ld: Vec<JsonLdEntry>,
111    /// Whether the URL is allowed by robots.txt.
112    pub is_allowed: bool,
113    /// The crawl delay from robots.txt, in seconds.
114    pub crawl_delay: Option<u64>,
115    /// Whether a noindex directive was detected.
116    pub noindex_detected: bool,
117    /// Whether a nofollow directive was detected.
118    pub nofollow_detected: bool,
119    /// The X-Robots-Tag header value, if present.
120    pub x_robots_tag: Option<String>,
121    /// Whether the content is a PDF.
122    pub is_pdf: bool,
123    /// Whether the page was skipped (binary or PDF content).
124    pub was_skipped: bool,
125    /// The detected character set encoding.
126    pub detected_charset: Option<String>,
127    /// Whether an authentication header was sent with the request.
128    pub auth_header_sent: bool,
129    /// Response metadata extracted from HTTP headers.
130    pub response_meta: Option<ResponseMeta>,
131    /// Downloaded assets from the page.
132    pub assets: Vec<DownloadedAsset>,
133    /// Whether the page content suggests JavaScript rendering is needed.
134    pub js_render_hint: bool,
135    /// Whether the browser fallback was used to fetch this page.
136    pub browser_used: bool,
137    /// Markdown conversion of the page content.
138    pub markdown: Option<MarkdownResult>,
139    /// Structured data extracted by LLM. Populated when extraction is configured.
140    pub extracted_data: Option<serde_json::Value>,
141    /// Metadata about the LLM extraction pass (cost, tokens, model).
142    pub extraction_meta: Option<ExtractionMeta>,
143    /// Screenshot of the page as PNG bytes. Populated when browser is used and capture_screenshot is enabled.
144    #[serde(skip)]
145    #[cfg_attr(alef, alef(skip))]
146    pub screenshot: Option<Vec<u8>>,
147    /// Downloaded non-HTML document (PDF, DOCX, image, code, etc.).
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub downloaded_document: Option<DownloadedDocument>,
150    /// Browser-specific extras (eval result, network events, cookies). Only
151    /// populated when `BrowserBackend::Native` was used for this request.
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub browser: Option<BrowserExtras>,
154}
155
156/// The result of crawling a single page during a crawl operation.
157#[derive(Debug, Clone, Default, Serialize, Deserialize)]
158#[serde(deny_unknown_fields)]
159pub struct CrawlPageResult {
160    /// The original URL of the page.
161    pub url: String,
162    /// The normalized URL of the page.
163    pub normalized_url: String,
164    /// The HTTP status code of the response.
165    pub status_code: u16,
166    /// The Content-Type header value.
167    pub content_type: String,
168    /// The HTML body of the response.
169    pub html: String,
170    /// The size of the response body in bytes.
171    pub body_size: usize,
172    /// Extracted metadata from the page.
173    pub metadata: PageMetadata,
174    /// Links found on the page.
175    pub links: Vec<LinkInfo>,
176    /// Images found on the page.
177    pub images: Vec<ImageInfo>,
178    /// Feed links found on the page.
179    pub feeds: Vec<FeedInfo>,
180    /// JSON-LD entries found on the page.
181    pub json_ld: Vec<JsonLdEntry>,
182    /// The depth of this page from the start URL.
183    pub depth: usize,
184    /// Whether this page is on the same domain as the start URL.
185    pub stayed_on_domain: bool,
186    /// Whether this page was skipped (binary or PDF content).
187    pub was_skipped: bool,
188    /// Whether the content is a PDF.
189    pub is_pdf: bool,
190    /// The detected character set encoding.
191    pub detected_charset: Option<String>,
192    /// Markdown conversion of the page content.
193    pub markdown: Option<MarkdownResult>,
194    /// Structured data extracted by LLM. Populated when extraction is configured.
195    pub extracted_data: Option<serde_json::Value>,
196    /// Metadata about the LLM extraction pass (cost, tokens, model).
197    pub extraction_meta: Option<ExtractionMeta>,
198    /// Downloaded non-HTML document (PDF, DOCX, image, code, etc.).
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub downloaded_document: Option<DownloadedDocument>,
201    /// Whether the browser fallback was used to fetch this page.
202    pub browser_used: bool,
203}
204
205/// The result of a multi-page crawl operation.
206#[derive(Debug, Clone, Default, Serialize, Deserialize)]
207#[serde(deny_unknown_fields)]
208pub struct CrawlResult {
209    /// The list of crawled pages.
210    pub pages: Vec<CrawlPageResult>,
211    /// The final URL after following redirects.
212    pub final_url: String,
213    /// The number of redirects followed.
214    pub redirect_count: usize,
215    /// Whether any page was skipped during crawling.
216    pub was_skipped: bool,
217    /// An error message, if the crawl encountered an issue.
218    pub error: Option<String>,
219    /// Cookies collected during the crawl.
220    pub cookies: Vec<CookieInfo>,
221    /// Whether all crawled pages stayed on the same domain as the start URL.
222    pub stayed_on_domain: bool,
223    /// Whether the browser fallback was used for any page in this crawl.
224    pub browser_used: bool,
225    /// Normalized URLs encountered during crawling (for deduplication counting).
226    #[serde(default, skip_serializing)]
227    #[cfg_attr(alef, alef(skip))]
228    pub normalized_urls: Vec<String>,
229}
230
231impl CrawlResult {
232    /// Create a new `CrawlResult` with the given fields.
233    #[allow(clippy::too_many_arguments)]
234    pub(crate) fn new(
235        pages: Vec<CrawlPageResult>,
236        final_url: String,
237        redirect_count: usize,
238        was_skipped: bool,
239        error: Option<String>,
240        cookies: Vec<CookieInfo>,
241        stayed_on_domain: bool,
242        normalized_urls: Vec<String>,
243    ) -> Self {
244        let browser_used = pages.iter().any(|p| p.browser_used);
245        Self {
246            pages,
247            final_url,
248            redirect_count,
249            was_skipped,
250            error,
251            cookies,
252            stayed_on_domain,
253            browser_used,
254            normalized_urls,
255        }
256    }
257
258    /// Returns the count of unique normalized URLs encountered during crawling.
259    pub fn unique_normalized_urls(&self) -> usize {
260        let mut unique: AHashSet<&str> = AHashSet::new();
261        for n in &self.normalized_urls {
262            unique.insert(n.as_str());
263        }
264        unique.len()
265    }
266}
267
268/// A URL entry from a sitemap.
269#[derive(Debug, Clone, Default, Serialize, Deserialize)]
270#[serde(deny_unknown_fields)]
271pub struct SitemapUrl {
272    /// The URL.
273    pub url: String,
274    /// The last modification date, if present.
275    pub lastmod: Option<String>,
276    /// The change frequency, if present.
277    pub changefreq: Option<String>,
278    /// The priority, if present.
279    pub priority: Option<String>,
280}
281
282/// The result of a map operation, containing discovered URLs.
283#[derive(Debug, Clone, Default, Serialize, Deserialize)]
284#[serde(deny_unknown_fields)]
285pub struct MapResult {
286    /// The list of discovered URLs.
287    pub urls: Vec<SitemapUrl>,
288}
289
290/// Rich markdown conversion result from HTML processing.
291#[derive(Debug, Clone, Default, Serialize, Deserialize)]
292#[serde(deny_unknown_fields)]
293pub struct MarkdownResult {
294    /// Converted markdown text.
295    pub content: String,
296    /// Structured document tree with semantic nodes.
297    pub document_structure: Option<serde_json::Value>,
298    /// Extracted tables with structured cell data.
299    pub tables: Vec<serde_json::Value>,
300    /// Non-fatal processing warnings.
301    pub warnings: Vec<String>,
302    /// Whether citation conversion was applied and produced at least one reference.
303    ///
304    /// `true` when the markdown contained inline links that were converted to
305    /// numbered citation references. The converted content (with `[N]` markers)
306    /// is available in `content`; the full reference list is accessible via
307    /// [`crate::citations::generate_citations`] if needed separately.
308    pub citations: bool,
309    /// Content-filtered markdown optimized for LLM consumption.
310    pub fit_content: Option<String>,
311}
312
313/// Cached page data for HTTP response caching.
314///
315/// Used only by the `CrawlCache` storage-backend trait, which is not part of
316/// the polyglot binding surface. Hidden from alef so bindings don't expose a
317/// type they can never receive.
318#[derive(Debug, Clone, Default, Serialize, Deserialize)]
319#[cfg_attr(alef, alef(skip))]
320pub struct CachedPage {
321    /// Absolute URL of the cached page.
322    pub url: String,
323    /// HTTP status code returned at the time the page was cached.
324    pub status_code: u16,
325    /// `Content-Type` header captured from the original response.
326    pub content_type: String,
327    /// Raw response body stored verbatim in the cache.
328    pub body: String,
329    /// `ETag` header value, if any — used for conditional revalidation.
330    pub etag: Option<String>,
331    /// `Last-Modified` header value, if any — used for conditional revalidation.
332    pub last_modified: Option<String>,
333    /// Unix timestamp (seconds) when the entry was written to the cache.
334    pub cached_at: u64,
335}