Skip to main content

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