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;