firefox_webdriver/
identifiers.rs

1//! Type-safe identifier wrappers for Firefox WebDriver.
2//!
3//! This module provides newtype wrappers around primitive types to prevent
4//! accidentally mixing incompatible IDs at compile time.
5//!
6//! # ID System
7//!
8//! From ARCHITECTURE.md Section 1.4:
9//!
10//! | ID | Type | Source | Purpose |
11//! |----|------|--------|---------|
12//! | [`SessionId`] | `NonZeroU32` | Rust counter | Window identification |
13//! | [`TabId`] | `NonZeroU32` | Firefox | Tab identification |
14//! | [`FrameId`] | `u64` | Firefox | Frame identification (0 = main) |
15//! | [`RequestId`] | UUID v4 | Rust | WebSocket correlation |
16//! | [`ElementId`] | UUID v4 | Extension | DOM element reference |
17//! | [`ScriptId`] | UUID v4 | Extension | Preload script reference |
18//! | [`SubscriptionId`] | UUID v4 | Extension | Element observation |
19//! | [`InterceptId`] | UUID v4 | Extension | Network interception |
20//!
21//! # Example
22//!
23//! ```ignore
24//! use firefox_webdriver::{TabId, FrameId, ElementId};
25//!
26//! // Type safety prevents mixing IDs
27//! let tab_id = TabId::new(1).expect("valid tab id");
28//! let frame_id = FrameId::main();
29//! let element_id = ElementId::new("uuid-string");
30//!
31//! // This would not compile:
32//! // let wrong: TabId = frame_id; // Error: mismatched types
33//! ```
34
35// ============================================================================
36// Imports
37// ============================================================================
38
39use std::fmt;
40use std::num::NonZeroU32;
41use std::sync::atomic::{AtomicU32, Ordering};
42
43use serde::de::Error as DeError;
44use serde::{Deserialize, Deserializer, Serialize, Serializer};
45use uuid::Uuid;
46
47// ============================================================================
48// Constants
49// ============================================================================
50
51/// Starting value for session counter.
52const SESSION_COUNTER_START: u32 = 1;
53
54// ============================================================================
55// Global State
56// ============================================================================
57
58/// Global atomic counter for generating unique session IDs.
59static SESSION_COUNTER: AtomicU32 = AtomicU32::new(SESSION_COUNTER_START);
60
61// ============================================================================
62// SessionId
63// ============================================================================
64
65/// Identifier for a browser session (Firefox window/process).
66///
67/// Generated by an atomic counter starting at 1. Each call to [`SessionId::next()`]
68/// returns a unique, incrementing ID. Thread-safe and lock-free.
69///
70/// # Example
71///
72/// ```ignore
73/// let session1 = SessionId::next();
74/// let session2 = SessionId::next();
75/// assert!(session1.as_u32() < session2.as_u32());
76/// ```
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
78pub struct SessionId(NonZeroU32);
79
80impl SessionId {
81    /// Generates the next unique session ID.
82    ///
83    /// Uses an atomic counter that starts at 1 and increments.
84    /// Thread-safe and lock-free.
85    ///
86    /// # Panics
87    ///
88    /// Panics if counter overflows (after 2^32 - 1 sessions).
89    #[inline]
90    #[must_use]
91    pub fn next() -> Self {
92        let id = SESSION_COUNTER.fetch_add(1, Ordering::Relaxed);
93
94        // Use checked creation - counter starts at 1 so this should never fail
95        // unless we overflow, which would be a bug
96        let non_zero =
97            NonZeroU32::new(id).expect("session counter overflow - this is a bug, please report");
98
99        Self(non_zero)
100    }
101
102    /// Creates a SessionId from a u32 value.
103    ///
104    /// Used for parsing session IDs from READY messages.
105    ///
106    /// # Returns
107    ///
108    /// `Some(SessionId)` if `id > 0`, `None` otherwise.
109    #[inline]
110    #[must_use]
111    pub fn from_u32(id: u32) -> Option<Self> {
112        NonZeroU32::new(id).map(Self)
113    }
114
115    /// Returns the underlying `u32` value.
116    #[inline]
117    #[must_use]
118    pub const fn as_u32(&self) -> u32 {
119        self.0.get()
120    }
121}
122
123impl fmt::Display for SessionId {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        write!(f, "{}", self.0)
126    }
127}
128
129impl Serialize for SessionId {
130    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
131    where
132        S: Serializer,
133    {
134        serializer.serialize_u32(self.0.get())
135    }
136}
137
138impl<'de> Deserialize<'de> for SessionId {
139    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
140    where
141        D: Deserializer<'de>,
142    {
143        let id = u32::deserialize(deserializer)?;
144        NonZeroU32::new(id)
145            .map(Self)
146            .ok_or_else(|| DeError::custom("session_id cannot be 0"))
147    }
148}
149
150// ============================================================================
151// TabId
152// ============================================================================
153
154/// Identifier for a browser tab.
155///
156/// Firefox assigns tab IDs starting from 1. A value of 0 is invalid.
157///
158/// # Example
159///
160/// ```ignore
161/// let tab_id = TabId::new(1).expect("valid tab id");
162/// assert_eq!(tab_id.as_u32(), 1);
163///
164/// // Zero is invalid
165/// assert!(TabId::new(0).is_none());
166/// ```
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
168pub struct TabId(NonZeroU32);
169
170impl TabId {
171    /// Creates a new tab ID with validation.
172    ///
173    /// # Arguments
174    ///
175    /// * `id` - Tab ID value (must be > 0)
176    ///
177    /// # Returns
178    ///
179    /// `Some(TabId)` if `id > 0`, `None` otherwise.
180    #[inline]
181    #[must_use]
182    pub fn new(id: u32) -> Option<Self> {
183        NonZeroU32::new(id).map(Self)
184    }
185
186    /// Returns the underlying `u32` value.
187    #[inline]
188    #[must_use]
189    pub const fn as_u32(&self) -> u32 {
190        self.0.get()
191    }
192}
193
194impl fmt::Display for TabId {
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        write!(f, "{}", self.0)
197    }
198}
199
200impl Serialize for TabId {
201    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
202    where
203        S: Serializer,
204    {
205        serializer.serialize_u32(self.0.get())
206    }
207}
208
209impl<'de> Deserialize<'de> for TabId {
210    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
211    where
212        D: Deserializer<'de>,
213    {
214        let id = u32::deserialize(deserializer)?;
215        NonZeroU32::new(id)
216            .map(Self)
217            .ok_or_else(|| DeError::custom("tab_id cannot be 0"))
218    }
219}
220
221// ============================================================================
222// FrameId
223// ============================================================================
224
225/// Identifier for a frame context.
226///
227/// Unlike [`TabId`] and [`SessionId`], `FrameId(0)` is valid and represents
228/// the main (top-level) frame. Firefox frame IDs can be large 64-bit values
229/// for iframes.
230///
231/// # Example
232///
233/// ```ignore
234/// let main_frame = FrameId::main();
235/// assert!(main_frame.is_main());
236/// assert_eq!(main_frame.as_u64(), 0);
237///
238/// let iframe = FrameId::new(17179869185);
239/// assert!(!iframe.is_main());
240/// ```
241#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
242pub struct FrameId(u64);
243
244impl FrameId {
245    /// Creates a new frame identifier.
246    #[inline]
247    #[must_use]
248    pub const fn new(id: u64) -> Self {
249        Self(id)
250    }
251
252    /// Returns the main frame identifier (0).
253    #[inline]
254    #[must_use]
255    pub const fn main() -> Self {
256        Self(0)
257    }
258
259    /// Returns `true` if this is the main frame.
260    #[inline]
261    #[must_use]
262    pub const fn is_main(&self) -> bool {
263        self.0 == 0
264    }
265
266    /// Returns the underlying `u64` value.
267    #[inline]
268    #[must_use]
269    pub const fn as_u64(&self) -> u64 {
270        self.0
271    }
272}
273
274impl Default for FrameId {
275    #[inline]
276    fn default() -> Self {
277        Self::main()
278    }
279}
280
281impl fmt::Display for FrameId {
282    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283        write!(f, "{}", self.0)
284    }
285}
286
287impl From<u64> for FrameId {
288    #[inline]
289    fn from(id: u64) -> Self {
290        Self(id)
291    }
292}
293
294// ============================================================================
295// RequestId
296// ============================================================================
297
298/// Unique identifier for a WebSocket request.
299///
300/// Used to correlate requests with their responses in the async protocol.
301/// Generated as UUID v4 by Rust side.
302///
303/// # Special Values
304///
305/// - [`RequestId::ready()`] returns the nil UUID, used for the READY handshake.
306///
307/// # Example
308///
309/// ```ignore
310/// let request_id = RequestId::generate();
311/// assert!(!request_id.is_ready());
312///
313/// let ready_id = RequestId::ready();
314/// assert!(ready_id.is_ready());
315/// ```
316#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
317pub struct RequestId(Uuid);
318
319impl RequestId {
320    /// Generates a new random request ID.
321    #[inline]
322    #[must_use]
323    pub fn generate() -> Self {
324        Self(Uuid::new_v4())
325    }
326
327    /// Returns the special READY request ID (nil UUID).
328    ///
329    /// This ID is used for the initial handshake message from the extension.
330    #[inline]
331    #[must_use]
332    pub const fn ready() -> Self {
333        Self(Uuid::nil())
334    }
335
336    /// Returns `true` if this is the READY request ID.
337    #[inline]
338    #[must_use]
339    pub fn is_ready(&self) -> bool {
340        self.0.is_nil()
341    }
342
343    /// Returns a reference to the underlying UUID.
344    #[inline]
345    #[must_use]
346    pub const fn as_uuid(&self) -> &Uuid {
347        &self.0
348    }
349}
350
351impl Default for RequestId {
352    #[inline]
353    fn default() -> Self {
354        Self::generate()
355    }
356}
357
358impl fmt::Display for RequestId {
359    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
360        fmt::Display::fmt(&self.0, f)
361    }
362}
363
364impl From<Uuid> for RequestId {
365    #[inline]
366    fn from(uuid: Uuid) -> Self {
367        Self(uuid)
368    }
369}
370
371// ============================================================================
372// ElementId
373// ============================================================================
374
375/// Identifier for a DOM element.
376///
377/// Generated by the extension as UUID v4. References an element stored
378/// in content script's internal `Map<UUID, Element>`.
379///
380/// # Example
381///
382/// ```ignore
383/// let element_id = ElementId::new("550e8400-e29b-41d4-a716-446655440000");
384/// assert_eq!(element_id.as_str(), "550e8400-e29b-41d4-a716-446655440000");
385/// ```
386#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
387pub struct ElementId(String);
388
389impl ElementId {
390    /// Creates a new element identifier.
391    #[inline]
392    #[must_use]
393    pub fn new(id: impl Into<String>) -> Self {
394        Self(id.into())
395    }
396
397    /// Returns the ID as a string slice.
398    #[inline]
399    #[must_use]
400    pub fn as_str(&self) -> &str {
401        &self.0
402    }
403}
404
405impl fmt::Display for ElementId {
406    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
407        fmt::Display::fmt(&self.0, f)
408    }
409}
410
411impl AsRef<str> for ElementId {
412    #[inline]
413    fn as_ref(&self) -> &str {
414        &self.0
415    }
416}
417
418// ============================================================================
419// ScriptId
420// ============================================================================
421
422/// Identifier for a preload script.
423///
424/// Generated by the extension as UUID v4. Used to track registered
425/// content scripts that run before page load.
426#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
427pub struct ScriptId(String);
428
429impl ScriptId {
430    /// Creates a new script identifier.
431    #[inline]
432    #[must_use]
433    pub fn new(id: impl Into<String>) -> Self {
434        Self(id.into())
435    }
436
437    /// Returns the ID as a string slice.
438    #[inline]
439    #[must_use]
440    pub fn as_str(&self) -> &str {
441        &self.0
442    }
443}
444
445impl fmt::Display for ScriptId {
446    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
447        fmt::Display::fmt(&self.0, f)
448    }
449}
450
451impl AsRef<str> for ScriptId {
452    #[inline]
453    fn as_ref(&self) -> &str {
454        &self.0
455    }
456}
457
458// ============================================================================
459// SubscriptionId
460// ============================================================================
461
462/// Identifier for an element observation subscription.
463///
464/// Generated by the extension as UUID v4. Used to track MutationObserver
465/// subscriptions for `element.added` / `element.removed` events.
466#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
467pub struct SubscriptionId(String);
468
469impl SubscriptionId {
470    /// Creates a new subscription identifier.
471    #[inline]
472    #[must_use]
473    pub fn new(id: impl Into<String>) -> Self {
474        Self(id.into())
475    }
476
477    /// Returns the ID as a string slice.
478    #[inline]
479    #[must_use]
480    pub fn as_str(&self) -> &str {
481        &self.0
482    }
483}
484
485impl fmt::Display for SubscriptionId {
486    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
487        fmt::Display::fmt(&self.0, f)
488    }
489}
490
491impl AsRef<str> for SubscriptionId {
492    #[inline]
493    fn as_ref(&self) -> &str {
494        &self.0
495    }
496}
497
498// ============================================================================
499// InterceptId
500// ============================================================================
501
502/// Identifier for a network intercept.
503///
504/// Generated by the extension as UUID v4. Used to track network
505/// interception configurations for request/response interception.
506#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
507pub struct InterceptId(String);
508
509impl InterceptId {
510    /// Creates a new intercept identifier.
511    #[inline]
512    #[must_use]
513    pub fn new(id: impl Into<String>) -> Self {
514        Self(id.into())
515    }
516
517    /// Returns the ID as a string slice.
518    #[inline]
519    #[must_use]
520    pub fn as_str(&self) -> &str {
521        &self.0
522    }
523}
524
525impl fmt::Display for InterceptId {
526    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
527        fmt::Display::fmt(&self.0, f)
528    }
529}
530
531impl AsRef<str> for InterceptId {
532    #[inline]
533    fn as_ref(&self) -> &str {
534        &self.0
535    }
536}
537
538// ============================================================================
539// Tests
540// ============================================================================
541
542#[cfg(test)]
543mod tests {
544    use super::*;
545
546    #[test]
547    fn test_session_id_increments() {
548        let id1 = SessionId::next();
549        let id2 = SessionId::next();
550        assert!(id1.as_u32() < id2.as_u32());
551    }
552
553    #[test]
554    fn test_session_id_display() {
555        let id = SessionId::next();
556        let display = id.to_string();
557        assert!(!display.is_empty());
558    }
559
560    #[test]
561    fn test_session_id_from_u32() {
562        assert!(SessionId::from_u32(0).is_none());
563        assert!(SessionId::from_u32(1).is_some());
564        assert_eq!(SessionId::from_u32(42).unwrap().as_u32(), 42);
565    }
566
567    #[test]
568    fn test_tab_id_rejects_zero() {
569        assert!(TabId::new(0).is_none());
570        assert!(TabId::new(1).is_some());
571    }
572
573    #[test]
574    fn test_tab_id_value() {
575        let tab = TabId::new(42).expect("valid tab id");
576        assert_eq!(tab.as_u32(), 42);
577    }
578
579    #[test]
580    fn test_frame_id_main() {
581        let main = FrameId::main();
582        assert!(main.is_main());
583        assert_eq!(main.as_u64(), 0);
584    }
585
586    #[test]
587    fn test_frame_id_iframe() {
588        let iframe = FrameId::new(17179869185);
589        assert!(!iframe.is_main());
590        assert_eq!(iframe.as_u64(), 17179869185);
591    }
592
593    #[test]
594    fn test_frame_id_default() {
595        let default = FrameId::default();
596        assert!(default.is_main());
597    }
598
599    #[test]
600    fn test_frame_id_from_u64() {
601        let frame: FrameId = 123u64.into();
602        assert_eq!(frame.as_u64(), 123);
603    }
604
605    #[test]
606    fn test_request_id_ready() {
607        let ready = RequestId::ready();
608        assert!(ready.is_ready());
609        assert!(ready.as_uuid().is_nil());
610    }
611
612    #[test]
613    fn test_request_id_generated() {
614        let id = RequestId::generate();
615        assert!(!id.is_ready());
616        assert!(!id.as_uuid().is_nil());
617    }
618
619    #[test]
620    fn test_request_id_uniqueness() {
621        let id1 = RequestId::generate();
622        let id2 = RequestId::generate();
623        assert_ne!(id1, id2);
624    }
625
626    #[test]
627    fn test_element_id() {
628        let id = ElementId::new("test-uuid");
629        assert_eq!(id.as_str(), "test-uuid");
630        assert_eq!(id.as_ref(), "test-uuid");
631        assert_eq!(id.to_string(), "test-uuid");
632    }
633
634    #[test]
635    fn test_script_id() {
636        let id = ScriptId::new("script-123");
637        assert_eq!(id.as_str(), "script-123");
638    }
639
640    #[test]
641    fn test_subscription_id() {
642        let id = SubscriptionId::new("sub-456");
643        assert_eq!(id.as_str(), "sub-456");
644        assert_eq!(id.as_ref(), "sub-456");
645    }
646
647    #[test]
648    fn test_intercept_id() {
649        let id = InterceptId::new("intercept-789");
650        assert_eq!(id.as_str(), "intercept-789");
651        assert_eq!(id.as_ref(), "intercept-789");
652    }
653
654    #[test]
655    fn test_serde_session_id() {
656        let id = SessionId::next();
657        let json = serde_json::to_string(&id).expect("serialize");
658        let parsed: SessionId = serde_json::from_str(&json).expect("deserialize");
659        assert_eq!(id, parsed);
660    }
661
662    #[test]
663    fn test_serde_tab_id() {
664        let id = TabId::new(42).expect("valid");
665        let json = serde_json::to_string(&id).expect("serialize");
666        let parsed: TabId = serde_json::from_str(&json).expect("deserialize");
667        assert_eq!(id, parsed);
668    }
669
670    #[test]
671    fn test_serde_frame_id() {
672        let id = FrameId::new(12345);
673        let json = serde_json::to_string(&id).expect("serialize");
674        let parsed: FrameId = serde_json::from_str(&json).expect("deserialize");
675        assert_eq!(id, parsed);
676    }
677
678    #[test]
679    fn test_serde_element_id() {
680        let id = ElementId::new("elem-uuid");
681        let json = serde_json::to_string(&id).expect("serialize");
682        let parsed: ElementId = serde_json::from_str(&json).expect("deserialize");
683        assert_eq!(id, parsed);
684    }
685}