firefox_webdriver/driver/
builder.rs

1//! Builder pattern for driver configuration.
2//!
3//! Provides a fluent API for configuring and creating [`Driver`] instances.
4//!
5//! # Example
6//!
7//! ```no_run
8//! use firefox_webdriver::Driver;
9//!
10//! # async fn example() -> firefox_webdriver::Result<()> {
11//! let driver = Driver::builder()
12//!     .binary("/usr/bin/firefox")
13//!     .extension("./extension")
14//!     .build()
15//!     .await?;
16//! # Ok(())
17//! # }
18//! ```
19
20// ============================================================================
21// Imports
22// ============================================================================
23
24use std::path::PathBuf;
25
26use crate::error::{Error, Result};
27
28use super::core::Driver;
29use super::profile::ExtensionSource;
30
31// ============================================================================
32// DriverBuilder
33// ============================================================================
34
35/// Builder for configuring a [`Driver`] instance.
36///
37/// Use [`Driver::builder()`] to create a new builder.
38#[derive(Debug, Default, Clone)]
39pub struct DriverBuilder {
40    /// Path to Firefox binary.
41    binary: Option<PathBuf>,
42    /// Extension source.
43    extension: Option<ExtensionSource>,
44}
45
46// ============================================================================
47// DriverBuilder Implementation
48// ============================================================================
49
50impl DriverBuilder {
51    /// Creates a new driver builder with no configuration.
52    #[inline]
53    #[must_use]
54    pub fn new() -> Self {
55        Self::default()
56    }
57
58    /// Sets the path to the Firefox binary executable.
59    ///
60    /// # Arguments
61    ///
62    /// * `path` - Path to Firefox binary (e.g., "/usr/bin/firefox")
63    #[inline]
64    #[must_use]
65    pub fn binary(mut self, path: impl Into<PathBuf>) -> Self {
66        self.binary = Some(path.into());
67        self
68    }
69
70    /// Sets the path to the WebDriver extension.
71    ///
72    /// Automatically detects whether the path is a directory (unpacked)
73    /// or file (packed .xpi).
74    ///
75    /// # Arguments
76    ///
77    /// * `path` - Path to extension directory or .xpi file
78    #[inline]
79    #[must_use]
80    pub fn extension(mut self, path: impl Into<PathBuf>) -> Self {
81        let path = path.into();
82        self.extension = Some(ExtensionSource::from(path));
83        self
84    }
85
86    /// Sets the extension from a base64-encoded string.
87    ///
88    /// Useful for embedding the extension in the binary.
89    ///
90    /// # Arguments
91    ///
92    /// * `data` - Base64-encoded .xpi content
93    #[inline]
94    #[must_use]
95    pub fn extension_base64(mut self, data: impl Into<String>) -> Self {
96        self.extension = Some(ExtensionSource::base64(data));
97        self
98    }
99
100    /// Sets the extension source directly.
101    ///
102    /// # Arguments
103    ///
104    /// * `source` - Extension source variant
105    #[inline]
106    #[must_use]
107    pub fn extension_source(mut self, source: ExtensionSource) -> Self {
108        self.extension = Some(source);
109        self
110    }
111
112    /// Builds the driver with validation.
113    ///
114    /// This is an async operation because it binds the WebSocket server.
115    ///
116    /// # Errors
117    ///
118    /// - [`Error::Config`] if binary or extension not set
119    /// - [`Error::FirefoxNotFound`] if binary path doesn't exist
120    /// - [`Error::Config`] if extension path doesn't exist
121    /// - [`Error::Io`] if WebSocket server binding fails
122    pub async fn build(self) -> Result<Driver> {
123        let binary = self.validate_binary()?;
124        let extension = self.validate_extension()?;
125
126        Driver::new(binary, extension).await
127    }
128}
129
130// ============================================================================
131// Validation
132// ============================================================================
133
134impl DriverBuilder {
135    /// Validates the binary path configuration.
136    fn validate_binary(&self) -> Result<PathBuf> {
137        let binary = self.binary.clone().ok_or_else(|| {
138            Error::config(
139                "Firefox binary path is required. Use .binary() to set it.\n\
140                 Example: Driver::builder().binary(\"/usr/bin/firefox\")",
141            )
142        })?;
143
144        if !binary.exists() {
145            return Err(Error::firefox_not_found(&binary));
146        }
147
148        Ok(binary)
149    }
150
151    /// Validates the extension configuration.
152    fn validate_extension(&self) -> Result<ExtensionSource> {
153        let extension = self.extension.clone().ok_or_else(|| {
154            Error::config(
155                "Extension is required. Use .extension() or .extension_base64() to set it.\n\
156                 Example: Driver::builder().extension(\"./extension\")",
157            )
158        })?;
159
160        // Validate file-based extensions exist
161        if let Some(path) = extension.path()
162            && !path.exists()
163        {
164            return Err(Error::config(format!(
165                "Extension not found at: {}\n\
166                 Ensure the extension directory or .xpi file exists.",
167                path.display()
168            )));
169        }
170
171        Ok(extension)
172    }
173}
174
175// ============================================================================
176// Tests
177// ============================================================================
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn test_new_creates_empty_builder() {
185        let builder = DriverBuilder::new();
186        assert!(builder.binary.is_none());
187        assert!(builder.extension.is_none());
188    }
189
190    #[test]
191    fn test_default_creates_empty_builder() {
192        let builder = DriverBuilder::default();
193        assert!(builder.binary.is_none());
194        assert!(builder.extension.is_none());
195    }
196
197    #[test]
198    fn test_binary_sets_path() {
199        let builder = DriverBuilder::new().binary("/usr/bin/firefox");
200        assert_eq!(builder.binary, Some(PathBuf::from("/usr/bin/firefox")));
201    }
202
203    #[test]
204    fn test_extension_sets_source() {
205        let builder = DriverBuilder::new().extension("./extension");
206        assert!(builder.extension.is_some());
207    }
208
209    #[test]
210    fn test_extension_base64_sets_source() {
211        let builder = DriverBuilder::new().extension_base64("UEsDBBQ...");
212        assert!(builder.extension.is_some());
213
214        if let Some(ExtensionSource::Base64(data)) = builder.extension {
215            assert_eq!(data, "UEsDBBQ...");
216        } else {
217            panic!("Expected Base64 extension source");
218        }
219    }
220
221    #[test]
222    fn test_extension_source_sets_directly() {
223        let source = ExtensionSource::packed("./ext.xpi");
224        let builder = DriverBuilder::new().extension_source(source.clone());
225        assert_eq!(builder.extension, Some(source));
226    }
227
228    #[test]
229    fn test_build_fails_without_binary() {
230        let rt = tokio::runtime::Runtime::new().unwrap();
231        let result = rt.block_on(DriverBuilder::new().extension("./extension").build());
232        assert!(result.is_err());
233
234        let err = result.unwrap_err();
235        assert!(err.to_string().contains("binary"));
236    }
237
238    #[test]
239    fn test_build_fails_without_extension() {
240        let rt = tokio::runtime::Runtime::new().unwrap();
241        let result = rt.block_on(DriverBuilder::new().binary("/bin/sh").build());
242        assert!(result.is_err());
243
244        let err = result.unwrap_err();
245        assert!(err.to_string().contains("Extension"));
246    }
247
248    #[test]
249    fn test_build_fails_with_nonexistent_binary() {
250        let rt = tokio::runtime::Runtime::new().unwrap();
251        let result = rt.block_on(
252            DriverBuilder::new()
253                .binary("/nonexistent/firefox")
254                .extension_base64("data")
255                .build(),
256        );
257
258        assert!(result.is_err());
259    }
260
261    #[test]
262    fn test_builder_is_clone() {
263        let builder = DriverBuilder::new().binary("/usr/bin/firefox");
264        let cloned = builder.clone();
265        assert_eq!(builder.binary, cloned.binary);
266    }
267}