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;