Skip to main content

vertigo/driver_module/
driver.rs

1use std::{future::Future, pin::Pin, rc::Rc};
2use vertigo_macro::{AutoJsJson, store};
3
4use crate::{
5    Context, Css, DomNode, DropResource, Instant, InstantType, JsJson, WebsocketMessage,
6    css::get_css_manager,
7    dev::{
8        FutureBox,
9        command::{LocationSetMode, LocationTarget},
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    struct_mut::ValueMut,
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 = crate::reactive::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
164    ///
165    /// This is a more complex version of [fetch](struct.Driver.html#method.fetch)
166    #[must_use]
167    pub fn request_get(&self, url: impl Into<String>) -> RequestBuilder {
168        RequestBuilder::get(url)
169    }
170
171    /// Create new RequestBuilder for POSTs (more complex version of [fetch](struct.Driver.html#method.fetch))
172    #[must_use]
173    pub fn request_post(&self, url: impl Into<String>) -> RequestBuilder {
174        RequestBuilder::post(url)
175    }
176
177    /// Create new RequestBuilder for PATCHes
178    #[must_use]
179    pub fn request_patch(&self, url: impl Into<String>) -> RequestBuilder {
180        RequestBuilder::patch(url)
181    }
182
183    /// Create new RequestBuilder for PUTs
184    #[must_use]
185    pub fn request_put(&self, url: impl Into<String>) -> RequestBuilder {
186        RequestBuilder::put(url)
187    }
188
189    /// Create new RequestBuilder for DELETEs
190    #[must_use]
191    pub fn request_delete(&self, url: impl Into<String>) -> RequestBuilder {
192        RequestBuilder::delete(url)
193    }
194
195    #[must_use]
196    pub fn sleep(&self, time: u32) -> FutureBox<()> {
197        let (sender, future) = FutureBox::new();
198
199        api_timers().set_timeout_and_detach(time, move || {
200            sender.publish(());
201        });
202
203        future
204    }
205
206    pub fn get_random(&self, min: u32, max: u32) -> u32 {
207        api_browser_command().get_random(min, max)
208    }
209
210    pub fn get_random_from<K: Clone>(&self, list: &[K]) -> Option<K> {
211        let len = list.len();
212
213        if len < 1 {
214            return None;
215        }
216
217        let max_index = len - 1;
218
219        let index = self.get_random(0, max_index as u32);
220        Some(list[index as usize].clone())
221    }
222
223    /// Initiate a websocket connection. Provided callback should handle a single [WebsocketMessage].
224    #[must_use]
225    pub fn websocket<F: Fn(WebsocketMessage) + 'static>(
226        &self,
227        host: impl Into<String>,
228        callback: F,
229    ) -> DropResource {
230        api_websocket().websocket(host, callback)
231    }
232
233    /// Spawn a future - thus allowing to fire async functions in, for example, event handler. Handy when fetching resources from internet.
234    pub fn spawn(&self, future: impl Future<Output = ()> + 'static) {
235        let future = Box::pin(future);
236        let spawn_executor = self.spawn_executor.clone();
237        spawn_executor(future);
238    }
239
240    /// Fire provided function in a way that all reactive updates made by this function
241    /// run once, as if the changes were done all at once.
242    pub fn transaction<R, F: FnOnce(&Context) -> R>(&self, func: F) -> R {
243        crate::reactive::transaction(func)
244    }
245
246    /// Allows to access different objects in the browser (See [js!](crate::js) macro for convenient use).
247    pub fn dom_access(&self) -> DomAccess {
248        DomAccess::default()
249    }
250
251    /// Function added for diagnostic purposes. It allows you to check whether a block with a transaction is missing somewhere.
252    pub fn on_after_transaction(&self, callback: impl Fn() + 'static) -> DropResource {
253        crate::reactive::on_after_transaction(callback)
254    }
255
256    /// Return true if the code is executed client-side (in the browser).
257    ///
258    /// ```rust
259    /// use vertigo::{dom, get_driver};
260    ///
261    /// let component = if get_driver().is_browser() {
262    ///     dom! { <div>"My dynamic component"</div> }
263    /// } else {
264    ///     dom! { <div>"Loading... (if not loaded check if JavaScript is enabled)"</div> }
265    /// };
266    /// ```
267    pub fn is_browser(&self) -> bool {
268        api_browser_command().is_browser()
269    }
270
271    pub fn is_server(&self) -> bool {
272        !self.is_browser()
273    }
274
275    /// Get any env variable set upon starting vertigo server.
276    pub fn env(&self, name: impl Into<String>) -> Option<String> {
277        let name = name.into();
278        api_browser_command().get_env(name)
279    }
280
281    /// Get public path to build directory where the browser can access WASM and other build files.
282    pub fn public_build_path(&self, path: impl Into<String>) -> String {
283        let path = path.into();
284        if self.is_browser() {
285            // In the browser use env variable attached during SSR
286            if let Some(public_path) = self.env("vertigo-public-path") {
287                path.replace(VERTIGO_PUBLIC_BUILD_PATH_PLACEHOLDER, &public_path)
288            } else {
289                // Fallback to default dest_dir
290                path.replace(VERTIGO_PUBLIC_BUILD_PATH_PLACEHOLDER, "/build")
291            }
292        } else {
293            // On the server, leave it, it will be replaced during SSR
294            path
295        }
296    }
297
298    /// Convert relative route to public path (with mount point attached)
299    pub fn route_to_public(&self, path: impl Into<String>) -> String {
300        let path = path.into();
301        if self.is_browser() {
302            // In the browser use env variable attached during SSR
303            let mount_point = self
304                .env("vertigo-mount-point")
305                .unwrap_or_else(|| "/".to_string());
306            if mount_point != "/" {
307                [mount_point, path].concat()
308            } else {
309                path
310            }
311        } else {
312            // On the server, prepend it with mount point token
313            [VERTIGO_MOUNT_POINT_PLACEHOLDER, &path].concat()
314        }
315    }
316
317    /// Convert path in the url to relative route in the app.
318    pub fn route_from_public(&self, path: impl Into<String>) -> String {
319        let path: String = path.into();
320
321        if api_browser_command().is_browser() {
322            // In the browser use env variable attached during SSR
323            let mount_point = api_browser_command()
324                .get_env("vertigo-mount-point")
325                .unwrap_or_else(|| "/".to_string());
326            if mount_point != "/" {
327                path.trim_start_matches(&mount_point).to_string()
328            } else {
329                path
330            }
331        } else {
332            // On the server no need to do anything
333            path
334        }
335    }
336
337    /// Register handler that intercepts defined urls and generates plaintext responses during SSR.
338    ///
339    /// Should return `None` in the handler if regular HTML should be generated by the App.
340    ///
341    /// ```rust
342    /// use vertigo::get_driver;
343    ///
344    /// get_driver().plains(|url| {
345    ///    if url == "/robots.txt" {
346    ///       Some("User-Agent: *\nDisallow: /search".to_string())
347    ///    } else {
348    ///       None
349    ///    }
350    /// });
351    /// ```
352    pub fn plains(&self, callback: impl Fn(&str) -> Option<String> + 'static) {
353        api_server_handler().plains(callback);
354    }
355
356    /// Allow to set custom HTTP status code during SSR
357    ///
358    /// ```rust
359    /// use vertigo::get_driver;
360    ///
361    /// get_driver().set_status(404)
362    /// ```
363    pub fn set_status(&self, status: u16) {
364        if self.is_server() {
365            api_browser_command().set_status(status);
366        }
367    }
368
369    /// Adds this CSS to manager producing a class name, which is returned
370    ///
371    /// There shouldn't be need to use it manually. It's used by `css!` macro.
372    pub fn class_name_for(&self, css: &Css) -> String {
373        get_css_manager().get_class_name(css)
374    }
375
376    /// Register css bundle
377    ///
378    /// There shouldn't be need to use it manually. It's used by `main!` macro.
379    pub fn register_bundle(&self, bundle: impl Into<String>) {
380        get_css_manager().register_bundle(bundle.into())
381    }
382}