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(Vec<serde_json::Value>) -> Pin<Box<dyn Future<Output = Result<serde_json::Value, String>> + Send>>
17        + Send
18        + Sync,
19>;
20
21/// Stored binding information for context-level functions.
22#[derive(Clone)]
23pub struct ContextBinding {
24    /// Function name.
25    pub name: String,
26    /// The callback function.
27    pub callback: ContextBindingCallback,
28}
29
30/// Registry for context-level exposed functions.
31///
32/// Functions registered here will be exposed to all pages in the context,
33/// including pages created after the function is exposed.
34#[derive(Default)]
35pub struct ContextBindingRegistry {
36    /// Registered bindings indexed by function name.
37    bindings: RwLock<HashMap<String, ContextBinding>>,
38}
39
40impl ContextBindingRegistry {
41    /// Create a new context binding registry.
42    pub fn new() -> Self {
43        Self {
44            bindings: RwLock::new(HashMap::new()),
45        }
46    }
47
48    /// Register a function to be exposed to all pages.
49    pub async fn expose_function<F, Fut>(&self, name: &str, callback: F)
50    where
51        F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
52        Fut: Future<Output = Result<serde_json::Value, String>> + Send + 'static,
53    {
54        debug!("Registering context-level binding: {}", name);
55
56        let boxed_callback: ContextBindingCallback = Arc::new(move |args| {
57            Box::pin(callback(args))
58        });
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    /// ```ignore
105    /// // Expose a function to all pages
106    /// context.expose_function("add", |args| async move {
107    ///     let x = args[0].as_i64().unwrap_or(0);
108    ///     let y = args[1].as_i64().unwrap_or(0);
109    ///     Ok(serde_json::json!(x + y))
110    /// }).await;
111    ///
112    /// // Create a page - function is available
113    /// let page = context.new_page().await?;
114    /// ```
115    pub async fn expose_function<F, Fut>(&self, name: &str, callback: F)
116    where
117        F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
118        Fut: Future<Output = Result<serde_json::Value, String>> + Send + 'static,
119    {
120        self.binding_registry.expose_function(name, callback).await;
121    }
122
123    /// Remove an exposed function from the context.
124    ///
125    /// Note: This only affects future pages. Existing pages will still have
126    /// the function available until they are reloaded.
127    pub async fn remove_exposed_function(&self, name: &str) -> bool {
128        self.binding_registry.remove_function(name).await
129    }
130}
131
132#[cfg(test)]
133mod tests;