Skip to main content

webscrape_ai/
response.rs

1//! Response models. Every response field is treated as optional/nullable —
2//! responses may carry an explicit `null` for any absent field.
3
4use std::time::Duration;
5
6use serde::de::DeserializeOwned;
7use serde::Deserialize;
8use serde_json::Value;
9
10use crate::error::{Error, Result};
11
12/// A successful (`completed`/`queued`) response envelope with typed `data`.
13///
14/// The envelope fields are preserved rather than flattened so callers can watch
15/// credit drain and keep the two request ids distinct.
16#[derive(Debug, Clone)]
17#[non_exhaustive]
18pub struct Response<T> {
19    /// Support-facing request id (`req_…`), from the envelope or `X-Request-ID`.
20    pub request_id: Option<String>,
21    /// Credits charged for this call. Absent on SmartBrowse dispatch, and `0`
22    /// for free polling endpoints.
23    pub credits_used: Option<i64>,
24    /// Wallet balance after this call. Absent on SmartBrowse dispatch.
25    pub credits_remaining: Option<i64>,
26    /// The typed `data` payload.
27    pub data: T,
28}
29
30/// An outbound link from [`ScrapeData::links`].
31#[derive(Debug, Clone, Deserialize)]
32pub struct LinkInfo {
33    /// Absolute link URL.
34    pub url: Option<String>,
35    /// Visible anchor text.
36    pub text: Option<String>,
37}
38
39/// Page metadata from [`ScrapeData::metadata`] (HTML only; null for PDFs).
40#[derive(Debug, Clone, Deserialize)]
41pub struct PageMetadata {
42    /// Document title.
43    pub title: Option<String>,
44    /// Meta description.
45    pub description: Option<String>,
46    /// Detected language.
47    pub language: Option<String>,
48}
49
50/// `data` for a successful [`Client::scrape`](crate::Client::scrape) call.
51#[derive(Debug, Clone, Deserialize)]
52pub struct ScrapeData {
53    /// Extraction id (distinct from the top-level `request_id`).
54    pub request_id: Option<String>,
55    /// Raw HTML, or markdown when `clean: true` / for PDFs.
56    pub html: Option<String>,
57    /// `"html"` or `"pdf"`.
58    pub content_type: Option<String>,
59    /// True when the cleaner pass actually ran.
60    pub cleaned: Option<bool>,
61    /// Present only with `extract_links: true`.
62    pub links: Option<Vec<LinkInfo>>,
63    /// `{title, description, language}`; null for PDFs.
64    pub metadata: Option<PageMetadata>,
65    /// Structured data extracted from the page (JSON-LD, microdata, pagination
66    /// hints), when available. Modeled as free-form JSON.
67    pub structured_data: Option<Value>,
68    /// Total fetch time in milliseconds.
69    pub latency_ms: Option<i64>,
70}
71
72/// `data` for a successful [`Client::smartscraper`](crate::Client::smartscraper)
73/// call.
74#[derive(Debug, Clone, Deserialize)]
75pub struct SmartScraperData {
76    /// Extraction id (distinct from the top-level `request_id`).
77    pub request_id: Option<String>,
78    /// The extracted output — object, array, or string (when `plain_text`).
79    pub result: Option<Value>,
80    /// Total extraction time in milliseconds.
81    pub latency_ms: Option<i64>,
82}
83
84impl SmartScraperData {
85    /// Deserialize [`SmartScraperData::result`] into a concrete type `T`.
86    ///
87    /// Returns [`Error::Decode`] if `result` is `null` or does not match `T`.
88    pub fn result_as<T: DeserializeOwned>(&self) -> Result<T> {
89        match &self.result {
90            Some(v) => serde_json::from_value(v.clone())
91                .map_err(|e| Error::Decode(format!("failed to decode smartscraper result: {e}"))),
92            None => Err(Error::Decode("smartscraper result was null".to_string())),
93        }
94    }
95}
96
97/// SmartBrowse run lifecycle state. Unknown values decode to
98/// [`RunStatus::Unknown`] rather than failing.
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
100#[serde(rename_all = "lowercase")]
101#[non_exhaustive]
102pub enum RunStatus {
103    /// Accepted, not yet started.
104    Queued,
105    /// In progress.
106    Running,
107    /// Finished successfully.
108    Completed,
109    /// Finished with an error.
110    Failed,
111    /// Force-stopped.
112    Cancelled,
113    /// A state not known to this SDK version.
114    #[serde(other)]
115    Unknown,
116}
117
118impl RunStatus {
119    /// True for `completed`, `failed`, or `cancelled`.
120    pub fn is_terminal(&self) -> bool {
121        matches!(
122            self,
123            RunStatus::Completed | RunStatus::Failed | RunStatus::Cancelled
124        )
125    }
126}
127
128/// `data` returned when dispatching a SmartBrowse run (HTTP 202 `queued`).
129#[derive(Debug, Clone, Deserialize)]
130pub struct SmartBrowseDispatch {
131    /// Opaque run id to poll.
132    pub run_id: String,
133    /// The recipe that was run.
134    pub recipe_id: String,
135    /// Run lifecycle state (always `running` on dispatch).
136    pub run_status: RunStatus,
137    /// Relative poll URL, e.g. `/v1/smartbrowse/runs/{id}`.
138    pub poll_url: Option<String>,
139    /// Dispatch timestamp (RFC3339).
140    pub created_at: Option<String>,
141}
142
143/// `data` returned when polling a SmartBrowse run.
144#[derive(Debug, Clone, Deserialize)]
145pub struct SmartBrowseRun {
146    /// Opaque run id.
147    pub id: String,
148    /// The recipe that was run.
149    pub recipe_id: String,
150    /// Run lifecycle state.
151    pub run_status: RunStatus,
152    /// Pages extracted so far.
153    pub pages_extracted: Option<i64>,
154    /// Items extracted so far.
155    pub items_extracted: Option<i64>,
156    /// Credits accrued by the run (distinct from the free polling envelope).
157    pub credits_used: Option<i64>,
158    /// Start timestamp (RFC3339); absent while pending.
159    pub started_at: Option<String>,
160    /// Completion timestamp (RFC3339); absent until terminal.
161    pub completed_at: Option<String>,
162    /// Failure detail; set only on `failed`/`cancelled` runs.
163    pub error: Option<String>,
164    /// Full result payload; present once `completed`. Free-form JSON.
165    pub result: Option<Value>,
166    /// Creation timestamp (RFC3339).
167    pub created_at: Option<String>,
168}
169
170/// Summary of the account's most recent run, from [`SmartBrowseUsage`].
171#[derive(Debug, Clone, Deserialize)]
172pub struct SmartBrowseLastRun {
173    /// Opaque run id.
174    pub id: String,
175    /// Run lifecycle state.
176    pub status: RunStatus,
177    /// Pages extracted by the run.
178    pub pages_extracted: Option<i64>,
179    /// Page ceiling actually applied — `min(plan cap, balance / cost_per_page)`.
180    pub effective_cap: Option<i64>,
181    /// True when credits, not the plan, bound the page ceiling.
182    pub clamped_by_credits: Option<bool>,
183    /// Creation timestamp (RFC3339).
184    pub created_at: Option<String>,
185    /// Completion timestamp (RFC3339); absent while in flight.
186    pub completed_at: Option<String>,
187}
188
189/// `data` for [`SmartBrowse::usage`](crate::SmartBrowse::usage) — plan caps and
190/// rolling-30-day usage.
191#[derive(Debug, Clone, Deserialize)]
192pub struct SmartBrowseUsage {
193    /// Runs initiated in the last 30 days.
194    pub runs_used_30d: Option<i64>,
195    /// Plan ceiling on runs per rolling 30-day window.
196    pub runs_per_month_cap: Option<i64>,
197    /// Plan ceiling on pages per run.
198    pub pages_per_run_cap: Option<i64>,
199    /// Credit cost per extracted page (currently 2).
200    pub cost_per_page: Option<i64>,
201    /// Number of enabled recurring schedules.
202    pub schedules_count: Option<i64>,
203    /// Whether the plan allows recurring schedules.
204    pub schedules_allowed: Option<bool>,
205    /// The most recent run, or `None` if the account has never run.
206    pub last_run: Option<SmartBrowseLastRun>,
207}
208
209/// Options for [`SmartBrowse::wait_for_run`](crate::SmartBrowse::wait_for_run)
210/// and [`SmartBrowse::run_and_wait`](crate::SmartBrowse::run_and_wait).
211///
212/// The poll interval grows ×1.5 per poll, capped at 10s. The default timeout of
213/// 900s matches the service's 15-minute hard run cap.
214#[derive(Debug, Clone, Copy)]
215pub struct WaitOptions {
216    /// Initial delay between polls (default 2s).
217    pub poll_interval: Duration,
218    /// Overall deadline before giving up (default 900s).
219    pub timeout: Duration,
220}
221
222impl Default for WaitOptions {
223    fn default() -> Self {
224        Self {
225            poll_interval: Duration::from_secs(2),
226            timeout: Duration::from_secs(900),
227        }
228    }
229}
230
231impl WaitOptions {
232    /// Default options (2s initial interval, 900s timeout).
233    pub fn new() -> Self {
234        Self::default()
235    }
236
237    /// Set the initial poll interval.
238    pub fn poll_interval(mut self, interval: Duration) -> Self {
239        self.poll_interval = interval;
240        self
241    }
242
243    /// Set the overall deadline.
244    pub fn timeout(mut self, timeout: Duration) -> Self {
245        self.timeout = timeout;
246        self
247    }
248}