viewpoint_core/network/route_builders/
mod.rs

1//! Builder types for route handling.
2//!
3//! This module provides builders for fulfilling and continuing intercepted
4//! network requests. The `FetchBuilder` and `FetchedResponse` types are in
5//! the `route_fetch` module.
6
7use std::future::Future;
8use std::path::Path;
9use std::pin::Pin;
10
11use viewpoint_cdp::protocol::fetch::{ContinueRequestParams, FulfillRequestParams, HeaderEntry};
12
13use super::route::Route;
14use super::route_fetch::FetchedResponse;
15use crate::error::NetworkError;
16
17/// Builder for fulfilling a request with a custom response.
18#[derive(Debug)]
19pub struct FulfillBuilder<'a> {
20    pub(super) route: &'a Route,
21    pub(super) status: u16,
22    pub(super) status_text: Option<String>,
23    pub(super) headers: Vec<HeaderEntry>,
24    pub(super) body: Option<Vec<u8>>,
25}
26
27impl<'a> FulfillBuilder<'a> {
28    pub(super) fn new(route: &'a Route) -> Self {
29        Self {
30            route,
31            status: 200,
32            status_text: None,
33            headers: Vec::new(),
34            body: None,
35        }
36    }
37
38    /// Set the HTTP status code.
39    #[must_use]
40    pub fn status(mut self, code: u16) -> Self {
41        self.status = code;
42        self
43    }
44
45    /// Set the HTTP status text.
46    #[must_use]
47    pub fn status_text(mut self, text: impl Into<String>) -> Self {
48        self.status_text = Some(text.into());
49        self
50    }
51
52    /// Set a response header.
53    #[must_use]
54    pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
55        self.headers.push(HeaderEntry {
56            name: name.into(),
57            value: value.into(),
58        });
59        self
60    }
61
62    /// Set multiple response headers.
63    #[must_use]
64    pub fn headers(mut self, headers: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>) -> Self {
65        for (name, value) in headers {
66            self.headers.push(HeaderEntry {
67                name: name.into(),
68                value: value.into(),
69            });
70        }
71        self
72    }
73
74    /// Set the Content-Type header.
75    #[must_use]
76    pub fn content_type(self, mime_type: impl Into<String>) -> Self {
77        self.header("Content-Type", mime_type)
78    }
79
80    /// Set the response body as text.
81    #[must_use]
82    pub fn body(mut self, body: impl Into<String>) -> Self {
83        self.body = Some(body.into().into_bytes());
84        self
85    }
86
87    /// Set the response body as bytes.
88    #[must_use]
89    pub fn body_bytes(mut self, body: impl Into<Vec<u8>>) -> Self {
90        self.body = Some(body.into());
91        self
92    }
93
94    /// Set the response body as JSON.
95    #[must_use]
96    pub fn json<T: serde::Serialize>(self, value: &T) -> Self {
97        let json = serde_json::to_string(value).unwrap_or_default();
98        self.content_type("application/json").body(json)
99    }
100
101    /// Set the response body from a file.
102    ///
103    /// # Errors
104    ///
105    /// Returns an error if the file cannot be read.
106    pub async fn path(mut self, path: impl AsRef<Path>) -> Result<Self, NetworkError> {
107        let body = tokio::fs::read(path.as_ref())
108            .await
109            .map_err(|e| NetworkError::IoError(e.to_string()))?;
110        self.body = Some(body);
111
112        // Try to set content type based on file extension
113        if let Some(ext) = path.as_ref().extension().and_then(|e| e.to_str()) {
114            let mime_type = match ext.to_lowercase().as_str() {
115                "html" | "htm" => "text/html",
116                "css" => "text/css",
117                "js" => "application/javascript",
118                "json" => "application/json",
119                "png" => "image/png",
120                "jpg" | "jpeg" => "image/jpeg",
121                "gif" => "image/gif",
122                "svg" => "image/svg+xml",
123                "pdf" => "application/pdf",
124                "txt" => "text/plain",
125                "xml" => "application/xml",
126                _ => "application/octet-stream",
127            };
128            return Ok(self.content_type(mime_type));
129        }
130
131        Ok(self)
132    }
133
134    /// Modify an existing response.
135    ///
136    /// Use this to fulfill with a response that was fetched via `route.fetch()`.
137    #[must_use]
138    pub fn response(mut self, response: &FetchedResponse<'_>) -> Self {
139        self.status = response.status;
140        for (name, value) in &response.headers {
141            self.headers.push(HeaderEntry {
142                name: name.clone(),
143                value: value.clone(),
144            });
145        }
146        // Include the body from the fetched response
147        if let Some(ref body) = response.body {
148            self.body = Some(body.clone());
149        }
150        self
151    }
152
153    /// Send the response.
154    ///
155    /// # Errors
156    ///
157    /// Returns an error if the response cannot be sent.
158    pub async fn send(self) -> Result<(), NetworkError> {
159        use base64::Engine;
160
161        let body = self.body.map(|b| {
162            base64::engine::general_purpose::STANDARD.encode(&b)
163        });
164
165        let params = FulfillRequestParams {
166            request_id: self.route.request_id().to_string(),
167            response_code: i32::from(self.status),
168            response_headers: if self.headers.is_empty() {
169                None
170            } else {
171                Some(self.headers)
172            },
173            binary_response_headers: None,
174            body,
175            response_phrase: self.status_text,
176        };
177
178        self.route.send_fulfill(params).await
179    }
180}
181
182/// Builder for continuing a request with optional modifications.
183#[derive(Debug)]
184pub struct ContinueBuilder<'a> {
185    pub(super) route: &'a Route,
186    pub(super) url: Option<String>,
187    pub(super) method: Option<String>,
188    pub(super) headers: Vec<HeaderEntry>,
189    pub(super) post_data: Option<Vec<u8>>,
190}
191
192impl<'a> ContinueBuilder<'a> {
193    pub(super) fn new(route: &'a Route) -> Self {
194        Self {
195            route,
196            url: None,
197            method: None,
198            headers: Vec::new(),
199            post_data: None,
200        }
201    }
202
203    /// Override the request URL.
204    #[must_use]
205    pub fn url(mut self, url: impl Into<String>) -> Self {
206        self.url = Some(url.into());
207        self
208    }
209
210    /// Override the request method.
211    #[must_use]
212    pub fn method(mut self, method: impl Into<String>) -> Self {
213        self.method = Some(method.into());
214        self
215    }
216
217    /// Add or override a request header.
218    #[must_use]
219    pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
220        self.headers.push(HeaderEntry {
221            name: name.into(),
222            value: value.into(),
223        });
224        self
225    }
226
227    /// Set multiple request headers.
228    #[must_use]
229    pub fn headers(mut self, headers: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>) -> Self {
230        for (name, value) in headers {
231            self.headers.push(HeaderEntry {
232                name: name.into(),
233                value: value.into(),
234            });
235        }
236        self
237    }
238
239    /// Override the request POST data.
240    ///
241    /// The data will be base64-encoded for the CDP command.
242    #[must_use]
243    pub fn post_data(mut self, data: impl Into<Vec<u8>>) -> Self {
244        self.post_data = Some(data.into());
245        self
246    }
247
248    /// Continue the request (applying any modifications).
249    ///
250    /// # Errors
251    ///
252    /// Returns an error if the request cannot be continued.
253    pub async fn send(self) -> Result<(), NetworkError> {
254        use base64::Engine;
255
256        let post_data = self.post_data.map(|d| {
257            base64::engine::general_purpose::STANDARD.encode(&d)
258        });
259
260        let params = ContinueRequestParams {
261            request_id: self.route.request_id().to_string(),
262            url: self.url,
263            method: self.method,
264            post_data,
265            headers: if self.headers.is_empty() {
266                None
267            } else {
268                Some(self.headers)
269            },
270            intercept_response: None,
271        };
272
273        self.route.send_continue(params).await
274    }
275}
276
277// Allow `route.continue_().await` without calling `.send()`
278impl ContinueBuilder<'_> {
279    /// Await the continue operation.
280    pub async fn await_continue(self) -> Result<(), NetworkError> {
281        self.send().await
282    }
283}
284
285impl<'a> std::future::IntoFuture for ContinueBuilder<'a> {
286    type Output = Result<(), NetworkError>;
287    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + 'a>>;
288
289    fn into_future(self) -> Self::IntoFuture {
290        Box::pin(self.send())
291    }
292}
293
294// FetchBuilder and FetchedResponse are in route_fetch.rs