vertigo/driver_module/
driver.rs

1use crate::{
2    css::css_manager::CssManager,
3    fetch::request_builder::{RequestBody, RequestBuilder},
4    Context, Css, Dependencies, DropResource, FutureBox, Instant, JsJson, WebsocketMessage,
5};
6use std::cell::RefCell;
7use std::{future::Future, pin::Pin, rc::Rc};
8
9use crate::driver_module::dom::DriverDom;
10use crate::{driver_module::api::ApiImport, driver_module::utils::futures_spawn::spawn_local};
11
12use super::api::DomAccess;
13
14/// Placeholder where to put public build path at runtime (default /build)
15pub const VERTIGO_PUBLIC_BUILD_PATH_PLACEHOLDER: &str = "%%VERTIGO_PUBLIC_BUILD_PATH%%";
16
17/// Placeholder where to put public mount point at runtime (default /)
18pub const VERTIGO_MOUNT_POINT_PLACEHOLDER: &str = "%%VERTIGO_MOUNT_POINT%%";
19
20#[derive(Debug, Clone, Copy)]
21pub enum FetchMethod {
22    GET,
23    HEAD,
24    POST,
25    PUT,
26    DELETE,
27    CONNECT,
28    OPTIONS,
29    TRACE,
30    PATCH,
31}
32
33impl FetchMethod {
34    pub fn to_str(&self) -> String {
35        match self {
36            Self::GET => "GET",
37            Self::HEAD => "HEAD",
38            Self::POST => "POST",
39            Self::PUT => "PUT",
40            Self::DELETE => "DELETE",
41            Self::CONNECT => "CONNECT",
42            Self::OPTIONS => "OPTIONS",
43            Self::TRACE => "TRACE",
44            Self::PATCH => "PATCH",
45        }
46        .into()
47    }
48}
49
50type Executable = dyn Fn(Pin<Box<dyn Future<Output = ()> + 'static>>);
51type PlainHandler = dyn Fn(&str) -> Option<String>;
52
53pub struct DriverInner {
54    pub(crate) api: ApiImport,
55    pub(crate) dependencies: &'static Dependencies,
56    pub(crate) css_manager: CssManager,
57    pub(crate) dom: &'static DriverDom,
58    spawn_executor: Rc<Executable>,
59    _subscribe: DropResource,
60    _plains_handler: RefCell<Option<Rc<PlainHandler>>>,
61}
62
63impl DriverInner {
64    pub fn new() -> &'static Self {
65        let dependencies: &'static Dependencies = Box::leak(Box::default());
66
67        let api = ApiImport::default();
68
69        let spawn_executor = {
70            let api = api.clone();
71
72            Rc::new(move |fut: Pin<Box<dyn Future<Output = ()> + 'static>>| {
73                spawn_local(api.clone(), fut);
74            })
75        };
76
77        let dom = DriverDom::new(&api);
78        let css_manager = {
79            let driver_dom = dom;
80            CssManager::new(move |selector, value| driver_dom.insert_css(selector, value))
81        };
82
83        let subscribe = dependencies.hooks.on_after_transaction(move || {
84            dom.flush_dom_changes();
85        });
86
87        Box::leak(Box::new(DriverInner {
88            api,
89            dependencies,
90            css_manager,
91            dom,
92            spawn_executor,
93            _subscribe: subscribe,
94            _plains_handler: RefCell::new(None),
95        }))
96    }
97}
98
99/// Result from request made using [RequestBuilder].
100///
101/// Variants:
102/// - `Ok(status_code, response)` if request succeeded,
103/// - `Err(response)` if request failed (because of network error for example).
104pub type FetchResult = Result<(u32, RequestBody), String>;
105
106/// Set of functions to communicate with the browser.
107#[derive(Clone, Copy)]
108pub struct Driver {
109    pub(crate) inner: &'static DriverInner,
110}
111
112impl Default for Driver {
113    fn default() -> Self {
114        let driver = DriverInner::new();
115
116        Driver { inner: driver }
117    }
118}
119
120impl Driver {
121    /// Gets a cookie by name
122    pub fn cookie_get(&self, cname: &str) -> String {
123        self.inner.api.cookie_get(cname)
124    }
125
126    /// Gets a JsJson cookie by name
127    pub fn cookie_get_json(&self, cname: &str) -> JsJson {
128        self.inner.api.cookie_get_json(cname)
129    }
130
131    /// Sets a cookie under provided name
132    pub fn cookie_set(&self, cname: &str, cvalue: &str, expires_in: u64) {
133        self.inner.api.cookie_set(cname, cvalue, expires_in)
134    }
135
136    /// Sets a cookie under provided name
137    pub fn cookie_set_json(&self, cname: &str, cvalue: JsJson, expires_in: u64) {
138        self.inner.api.cookie_set_json(cname, cvalue, expires_in)
139    }
140
141    /// Go back in client's (browser's) history
142    pub fn history_back(&self) {
143        self.inner.api.history_back();
144    }
145
146    /// Replace current location
147    pub fn history_replace(&self, new_url: &str) {
148        self.inner.api.replace_history_location(new_url)
149    }
150
151    /// Make `func` fire every `time` seconds.
152    #[must_use]
153    pub fn set_interval(&self, time: u32, func: impl Fn() + 'static) -> DropResource {
154        self.inner.api.interval_set(time, func)
155    }
156
157    /// Gets current value of monotonic clock.
158    pub fn now(&self) -> Instant {
159        Instant::now(self.inner.api.clone())
160    }
161
162    /// Gets current UTC timestamp
163    pub fn utc_now(&self) -> i64 {
164        self.inner.api.utc_now()
165    }
166
167    /// Gets browsers time zone offset in seconds
168    ///
169    /// Compatible with chrono's `FixedOffset::east_opt` method.
170    pub fn timezone_offset(&self) -> i32 {
171        self.inner.api.timezone_offset()
172    }
173
174    /// Create new RequestBuilder for GETs (more complex version of [fetch](struct.Driver.html#method.fetch))
175    #[must_use]
176    pub fn request_get(&self, url: impl Into<String>) -> RequestBuilder {
177        RequestBuilder::get(url)
178    }
179
180    /// Create new RequestBuilder for POSTs (more complex version of [fetch](struct.Driver.html#method.fetch))
181    #[must_use]
182    pub fn request_post(&self, url: impl Into<String>) -> RequestBuilder {
183        RequestBuilder::post(url)
184    }
185
186    #[must_use]
187    pub fn sleep(&self, time: u32) -> FutureBox<()> {
188        let (sender, future) = FutureBox::new();
189        self.inner.api.set_timeout_and_detach(time, move || {
190            sender.publish(());
191        });
192
193        future
194    }
195
196    pub fn get_random(&self, min: u32, max: u32) -> u32 {
197        self.inner.api.get_random(min, max)
198    }
199
200    pub fn get_random_from<K: Clone>(&self, list: &[K]) -> Option<K> {
201        let len = list.len();
202
203        if len < 1 {
204            return None;
205        }
206
207        let max_index = len - 1;
208
209        let index = self.get_random(0, max_index as u32);
210        Some(list[index as usize].clone())
211    }
212
213    /// Initiate a websocket connection. Provided callback should handle a single [WebsocketMessage].
214    #[must_use]
215    pub fn websocket<F: Fn(WebsocketMessage) + 'static>(
216        &self,
217        host: impl Into<String>,
218        callback: F,
219    ) -> DropResource {
220        self.inner.api.websocket(host, callback)
221    }
222
223    /// Spawn a future - thus allowing to fire async functions in, for example, event handler. Handy when fetching resources from internet.
224    pub fn spawn(&self, future: impl Future<Output = ()> + 'static) {
225        let future = Box::pin(future);
226        let spawn_executor = self.inner.spawn_executor.clone();
227        spawn_executor(future);
228    }
229
230    /// Fire provided function in a way that all changes in [dependency graph](struct.Dependencies.html) made by this function
231    /// will trigger only one run of updates, just like the changes were done all at once.
232    pub fn transaction<R, F: FnOnce(&Context) -> R>(&self, func: F) -> R {
233        self.inner.dependencies.transaction(func)
234    }
235
236    /// Allows to access different objects in the browser (See [js!](crate::js) macro for convenient use).
237    pub fn dom_access(&self) -> DomAccess {
238        self.inner.api.dom_access()
239    }
240
241    /// Function added for diagnostic purposes. It allows you to check whether a block with a transaction is missing somewhere.
242    pub fn on_after_transaction(&self, callback: impl Fn() + 'static) -> DropResource {
243        self.inner.dependencies.hooks.on_after_transaction(callback)
244    }
245
246    /// Return true if the code is executed client-side (in the browser).
247    ///
248    /// ```rust
249    /// use vertigo::{dom, get_driver};
250    ///
251    /// let component = if get_driver().is_browser() {
252    ///     dom! { <div>"My dynamic component"</div> }
253    /// } else {
254    ///     dom! { <div>"Loading... (if not loaded check if JavaScript is enabled)"</div> }
255    /// };
256    /// ```
257    pub fn is_browser(&self) -> bool {
258        self.inner.api.is_browser()
259    }
260
261    pub fn is_server(&self) -> bool {
262        !self.is_browser()
263    }
264
265    /// Get any env variable set upon starting vertigo server.
266    pub fn env(&self, name: impl Into<String>) -> Option<String> {
267        let name = name.into();
268        self.inner.api.get_env(name)
269    }
270
271    /// Get public path to build directory where the browser can access WASM and other build files.
272    pub fn public_build_path(&self, path: impl Into<String>) -> String {
273        let path = path.into();
274        if self.is_browser() {
275            // In the browser use env variable attached during SSR
276            if let Some(public_path) = self.env("vertigo-public-path") {
277                path.replace(VERTIGO_PUBLIC_BUILD_PATH_PLACEHOLDER, &public_path)
278            } else {
279                // Fallback to default dest_dir
280                path.replace(VERTIGO_PUBLIC_BUILD_PATH_PLACEHOLDER, "/build")
281            }
282        } else {
283            // On the server, leave it, it will be replaced during SSR
284            path
285        }
286    }
287
288    /// Convert relative route to public path (with mount point attached)
289    pub fn route_to_public(&self, path: impl Into<String>) -> String {
290        let path = path.into();
291        if self.is_browser() {
292            // In the browser use env variable attached during SSR
293            let mount_point = self
294                .env("vertigo-mount-point")
295                .unwrap_or_else(|| "/".to_string());
296            if mount_point != "/" {
297                [mount_point, path].concat()
298            } else {
299                path
300            }
301        } else {
302            // On the server, prepend it with mount point token
303            [VERTIGO_MOUNT_POINT_PLACEHOLDER, &path].concat()
304        }
305    }
306
307    /// Convert path in the url to relative route in the app.
308    pub fn route_from_public(&self, path: impl Into<String>) -> String {
309        let path: String = path.into();
310        self.inner.api.route_from_public(path)
311    }
312
313    /// Register handler that intercepts defined urls and generates plaintext responses during SSR.
314    ///
315    /// Should return `None` in the handler if regular HTML should be generated by the App.
316    ///
317    /// ```rust
318    /// use vertigo::get_driver;
319    ///
320    /// get_driver().plains(|url| {
321    ///    if url == "/robots.txt" {
322    ///       Some("User-Agent: *\nDisallow: /search".to_string())
323    ///    } else {
324    ///       None
325    ///    }
326    /// });
327    /// ```
328    pub fn plains(&mut self, callback: impl Fn(&str) -> Option<String> + 'static) {
329        let mut mut_plains = self.inner._plains_handler.borrow_mut();
330        *mut_plains = Some(Rc::new(callback));
331    }
332
333    /// Try to intercept urls for generating plaintext responses during SSR (used internally by [main!](crate::main) macro).
334    /// See [Driver::plains()] method.
335    pub fn try_get_plain(&self) {
336        if self.is_server() {
337            let url = self.inner.api.get_history_location();
338            match self.inner._plains_handler.try_borrow() {
339                Ok(callback_ref) => {
340                    if let Some(callback) = callback_ref.as_deref() {
341                        if let Some(body) = callback(&url) {
342                            self.inner.api.plain_response(body)
343                        }
344                    }
345                }
346                Err(err) => log::error!("Error invoking plains: {err}"),
347            }
348        } else {
349            log::info!("Browser mode, not invoking try_get_plain");
350        }
351    }
352
353    /// Allow to set custom HTTP status code during SSR
354    ///
355    /// ```rust
356    /// use vertigo::get_driver;
357    ///
358    /// get_driver().set_status(404)
359    /// ```
360    pub fn set_status(&self, status: u16) {
361        if self.is_server() {
362            self.inner.api.set_status(status);
363        }
364    }
365
366    /// Adds this CSS to manager producing a class name, which is returned
367    ///
368    /// There shouldn't be need to use it manually. It's used by `css!` macro.
369    pub fn class_name_for(&mut self, css: &Css) -> String {
370        self.inner.css_manager.get_class_name(css)
371    }
372
373    /// Register css bundle
374    ///
375    /// There shouldn't be need to use it manually. It's used by `main!` macro.
376    pub fn register_bundle(&self, bundle: impl Into<String>) {
377        self.inner.css_manager.register_bundle(bundle.into())
378    }
379}