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}