viewpoint_core/context/binding/
mod.rs

1//! Context-level exposed function bindings.
2//!
3//! This module provides functionality for exposing Rust functions to JavaScript
4//! across all pages in a browser context.
5
6use std::collections::HashMap;
7use std::future::Future;
8use std::pin::Pin;
9use std::sync::Arc;
10
11use tokio::sync::RwLock;
12use tracing::debug;
13
14/// Type alias for the binding callback function.
15pub type ContextBindingCallback = Arc<
16    dyn Fn(
17            Vec<serde_json::Value>,
18        ) -> Pin<Box<dyn Future<Output = Result<serde_json::Value, String>> + Send>>
19        + Send
20        + Sync,
21>;
22
23/// Stored binding information for context-level functions.
24#[derive(Clone)]
25pub struct ContextBinding {
26    /// Function name.
27    pub name: String,
28    /// The callback function.
29    pub callback: ContextBindingCallback,
30}
31
32/// Registry for context-level exposed functions.
33///
34/// Functions registered here will be exposed to all pages in the context,
35/// including pages created after the function is exposed.
36#[derive(Default)]
37pub struct ContextBindingRegistry {
38    /// Registered bindings indexed by function name.
39    bindings: RwLock<HashMap<String, ContextBinding>>,
40}
41
42impl ContextBindingRegistry {
43    /// Create a new context binding registry.
44    pub fn new() -> Self {
45        Self {
46            bindings: RwLock::new(HashMap::new()),
47        }
48    }
49
50    /// Register a function to be exposed to all pages.
51    pub async fn expose_function<F, Fut>(&self, name: &str, callback: F)
52    where
53        F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
54        Fut: Future<Output = Result<serde_json::Value, String>> + Send + 'static,
55    {
56        debug!("Registering context-level binding: {}", name);
57
58        let boxed_callback: ContextBindingCallback = Arc::new(move |args| Box::pin(callback(args)));
59
60        let binding = ContextBinding {
61            name: name.to_string(),
62            callback: boxed_callback,
63        };
64
65        let mut bindings = self.bindings.write().await;
66        bindings.insert(name.to_string(), binding);
67    }
68
69    /// Remove an exposed function.
70    pub async fn remove_function(&self, name: &str) -> bool {
71        debug!("Removing context-level binding: {}", name);
72        let mut bindings = self.bindings.write().await;
73        bindings.remove(name).is_some()
74    }
75
76    /// Get all registered bindings.
77    pub async fn get_all(&self) -> Vec<ContextBinding> {
78        let bindings = self.bindings.read().await;
79        bindings.values().cloned().collect()
80    }
81
82    /// Check if a function is registered.
83    pub async fn has(&self, name: &str) -> bool {
84        let bindings = self.bindings.read().await;
85        bindings.contains_key(name)
86    }
87}
88
89use super::BrowserContext;
90
91impl BrowserContext {
92    /// Expose a Rust function to JavaScript in all pages of this context.
93    ///
94    /// The function will be available as `window.<name>()` in JavaScript.
95    /// When called from JavaScript, the function arguments are serialized to JSON,
96    /// the Rust callback is executed, and the result is returned to JavaScript.
97    ///
98    /// Note: Functions exposed at the context level need to be explicitly applied
99    /// to each page. This method registers the function for future pages, but
100    /// you need to call `expose_function` on existing pages separately.
101    ///
102    /// # Example
103    ///
104    /// ```no_run
105    /// use viewpoint_core::Browser;
106    ///
107    /// # async fn example() -> Result<(), viewpoint_core::CoreError> {
108    /// let browser = Browser::launch().headless(true).launch().await?;
109    /// let context = browser.new_context().await?;
110    ///
111    /// // Expose a function to all pages
112    /// context.expose_function("add", |args| async move {
113    ///     let x = args[0].as_i64().unwrap_or(0);
114    ///     let y = args[1].as_i64().unwrap_or(0);
115    ///     Ok(serde_json::json!(x + y))
116    /// }).await;
117    ///
118    /// // Create a page - function is available
119    /// let page = context.new_page().await?;
120    /// # Ok(())
121    /// # }
122    /// ```
123    pub async fn expose_function<F, Fut>(&self, name: &str, callback: F)
124    where
125        F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
126        Fut: Future<Output = Result<serde_json::Value, String>> + Send + 'static,
127    {
128        self.binding_registry.expose_function(name, callback).await;
129    }
130
131    /// Remove an exposed function from the context.
132    ///
133    /// Note: This only affects future pages. Existing pages will still have
134    /// the function available until they are reloaded.
135    pub async fn remove_exposed_function(&self, name: &str) -> bool {
136        self.binding_registry.remove_function(name).await
137    }
138}
139
140#[cfg(test)]
141mod tests;