vertigo/driver_module/
driver.rs

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