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}